I'm using Express.js with Firebase Admin SDK and deploying my backend as a Firebase Cloud Function. I'm trying to upload an image file from a frontend form (using FormData in jQuery) to the backend using multer, and then save that file to Firebase Storage.
The route is set up using upload.single("file"), and locally everything works perfectly — req.file contains the uploaded file, and it gets saved to Firebase Storage as expected.
However, after deploying to Firebase Functions, the exact same code results in req.file being undefined. All the other fields in req.body are received correctly, so the problem seems isolated to multer not processing the file in the deployed function.
require("dotenv").config();
const functions = require("firebase-functions");
const express = require("express");
const bodyParser = require("body-parser");
const cookieParser = require("cookie-parser");
const path = require("path");
const cors = require("cors");
const nodemailer = require("nodemailer");
const session = require('express-session');
const { FirestoreStore } = require('@google-cloud/connect-firestore');
const admin = require('firebase-admin');
const { verifyToken, checkRole} = require("./src/Middleware/authAdminMiddleware");
const routes = require("./src/routes/routes");
const app = express();
const { db } = require("./db");
let firestore = admin.firestore()
console.log(process.env.NODE_ENV === "production" ? true : false)
app.use(cookieParser());
app.use(
session({
store: new FirestoreStore({
dataset: firestore,
kind: 'sessions', // Firestore collection name
}),
secret: process.env.SESSION_SECRET || functions.config()?.session?.secret || 'fallback-dev-secret',
resave: false,
saveUninitialized: false,
proxy: true,
name: '__session',
cookie: {
secure: process.env.NODE_ENV === "production", // true in production
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000, // 1 day
sameSite: process.env.NODE_ENV === "production" ? 'none' : 'lax',
domain: process.env.NODE_ENV === 'production'
? '.ellavitaclothing.com' // Leading dot important
: "localhost" // Omit domain for localhost
},
})
)
// 🔹 Middleware Setup
app.use(
cors({
origin: [
"http://localhost:4000",
"https://ellavitaclothing.com",
"https://clothing-brand-92418.web.app",
],
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization", "Cookie"],
credentials: true,
})
);
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use("/", routes);
app.all("*", (req, res) => {
res.status(404).render("pages/404");
});
const PORT = 4000;
if (process.env.SERVER_TYPE == "PRODUCTION") {
console.log("Firebase Deploy");
} else {
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
}
exports.app = functions.https.onRequest(app);
the above is my index file code
route
router.post("/admin/blog-create", upload.single("file") , createBlogController);
constroller code
const createBlogController = async (req, res) => {
try {
const { inputBlogTitle, inputCategory, shortDescription, blogSlug, selectedTagIds, dop, detailDescription } = req.body;
const file = req.file;
const result = await createBlogModel({ inputBlogTitle, inputCategory, shortDescription, blogSlug, selectedTagIds, dop, detailDescription, file });
if (result.success) {
return res.status(200).json({ success: "1", message: "Blog Successfully Created." });
} else {
return res.status(500).json({ success: "0", message: "Failed to create blog" });
}
} catch (error) {
console.error("Error in createBlogController:", error);
res.status(500).json({ success: "0", message: error.message });
}
};
madel code
const { db } = require("../../../db");
const admin = require("firebase-admin");
const { getStorage } = require("firebase-admin/storage");
const createBlogModel = async ({ inputBlogTitle, inputCategory, shortDescription, blogSlug, selectedTagIds, dop, detailDescription, file }) => {
try {
// 1. 🔥 Duplicate Slug Check
const slugSnapshot = await db.collection("blog")
.where("blog_slug", "==", blogSlug)
.where("delete", "==", false)
.limit(1)
.get();
if (!slugSnapshot.empty) {
throw new Error("Blog with the same title already exists. Please choose another one.");
}
if (!file) {
throw new Error("Image file is required");
}
// 2. Upload file to Firebase Storage
const bucket = getStorage().bucket();
const newFileName = `img/blog-${Date.now()}-${file.originalname}`;
const storageFile = bucket.file(newFileName);
await storageFile.save(file.buffer, {
metadata: { contentType: file.mimetype }
});
const url = await storageFile.getSignedUrl({ action: 'read', expires: '03-09-2491' });
// 3. Create Blog Document
const blogRef = await db.collection("blog").add({
blog_title: inputBlogTitle,
blog_category: db.collection("category").doc(inputCategory),
short_description: shortDescription,
blog_slug: blogSlug,
tags: selectedTagIds.map(tagId => db.doc(`tags/${tagId}`)),
dop: dop,
detail_description: detailDescription,
url: url[0],
delete: false,
createdAt: admin.firestore.Timestamp.now(),
updatedAt: admin.firestore.Timestamp.now()
});
const blogId = blogRef.id;
// 4. Insert into AlgoliaSearch
await db.collection("algoliaSearch").doc(blogId).set({
id: blogId,
name: inputBlogTitle,
slug: blogSlug,
shortDesc: shortDescription,
desc: detailDescription,
ref: inputCategory,
type: "blog"
});
db.collection("category").doc(inputCategory).set({
blog_count: admin.firestore.FieldValue.increment(1)
}, { merge: true })
return { success: true };
} catch (error) {
console.error("Error in createBlogModel:", error);
throw new Error(error.message);
}
};
frontend code
$("#submit").click(async function (event) {
event.preventDefault();
var formData = new FormData();
formData.append('inputBlogTitle', $("#inputBlogTitle").val());
formData.append('inputCategory', $("#inputCategory").val());
formData.append('shortDescription', $("#shortDescription").val());
formData.append('blogSlug', $("#blogSlug").val());
formData.append('dop', new Date().toISOString().split('T')[0]);
formData.append('detailDescription', $("#div_editor1").val());
selectedTags.forEach(tag => {
formData.append('selectedTagIds[]', tag.id);
});
const fileInput = document.getElementById("inputImage");
if (fileInput.files.length > 0) {
formData.append('file', fileInput.files[0]);
} else {
Swal.fire("Error", "Please select an image.", "error");
return;
}
console.log(formData)
for (let [key, value] of formData.entries()) {
console.log(`${key}:`, value);
}
let response = await fetch("/admin/blog-create", {
method: "POST",
body: formData
});
let data = await response.json();
if (data.success === "1") {
sweetAlertPopup("Blog Successfully Added.", "/admin/blog");
} else {
Swal.fire("Error", data.message, "error");
}
});
<form class="d-flex flex-column gap-20 border border-3 p-20 rounded">
<div class="d-flex gap-20">
<div class="w-100">
<label class="form-label fw-bold text-neutral-900" for="title">Post Title: </label>
<input type="text" class="form-control border border-neutral-200 radius-8" id="inputBlogTitle" placeholder="Enter Post Title">
</div>
<div class="w-100">
<label class="form-label fw-bold text-neutral-900" for="title">Post Slug: </label>
<input type="text" class="form-control border border-neutral-200 radius-8" id="blogSlug" placeholder="Post Slug" disabled>
</div>
</div>
<div class="d-flex gap-20">
<div class="w-100">
<label class="form-label fw-bold text-neutral-900">Post Category: </label>
<select class="form-control border border-neutral-200 radius-8" id="inputCategory">
<option value="select category">Select Category</option>
</select>
</div>
<div class="w-100">
<label class="form-label">Tags (Select up to 5)</label>
<div class="dropdown">
<button class="tagsCusBtn btn text-left btn-light form-select dropdown-toggle w-100" type="button" id="tagDropdownBtn" data-bs-toggle="dropdown" aria-expanded="false">
Select Tags
</button>
<ul class="dropdown-menu w-100" id="tagDropdown" style="max-height: 200px; overflow-y: auto;">
<!-- Tags from Firestore will be added here -->
</ul>
</div>
<div id="selectedTagsContainer" class="mt-2 d-flex gap-2"></div>
<small id="tagError" style="color: red; display: none;">You can select up to 5 tags only!</small>
</div>
</div>
<div class="w-100">
<label class="form-label fw-bold text-neutral-900" for="title">Post Short Description: </label>
<textarea type="text" class="form-control border border-neutral-200 radius-8" id="shortDescription" placeholder="Enter Post Title"> </textarea>
</div>
<div class="col">
<div class="card h-100 p-0">
<div class="card-header border-bottom bg-base py-16 px-24">
<h6 class="text-lg fw-semibold mb-0">Image Upload</h6>
</div>
<div class="card-body p-24">
<div class="upload-image-wrapper d-flex align-items-center gap-3">
<div class="uploaded-img d-none position-relative h-120-px w-120-px border input-form-light radius-8 overflow-hidden border-dashed bg-neutral-50">
<button type="button" class="uploaded-img__remove position-absolute top-0 end-0 z-1 text-2xxl line-height-1 me-8 mt-8 d-flex">
<iconify-icon icon="radix-icons:cross-2" class="text-xl text-danger-600"></iconify-icon>
</button>
<img id="uploaded-img__preview" class="w-100 h-100 object-fit-cover" src="/adminassets/nimages/user.png" alt="image">
</div>
<label class="upload-file h-120-px w-120-px border input-form-light radius-8 overflow-hidden border-dashed bg-neutral-50 bg-hover-neutral-200 d-flex align-items-center flex-column justify-content-center gap-1" for="inputImage">
<iconify-icon icon="solar:camera-outline" class="text-xl text-secondary-light"></iconify-icon>
<span class="fw-semibold text-secondary-light">Upload</span>
<input id="inputImage" type="file" hidden>
</label>
</div>
</div>
</div>
</div>
<div class="col-lg-12">
<br>
<label class="form-label fw-bold text-neutral-900">Detail Description</label><span id="detailDescriotionError"></span>
<textarea id="div_editor1" rows="10" style="background-color:transparent;" class="form-control" name="">
</textarea>
</div>
<button class="btn btn-primary-600 radius-8" id="submit">Submit</button>
</form>