Back to blog

Handling file uploads and failures with Express

Another little snippet to remind myself later.

If I want to handle uploading files via a form with enctype="multipart/form-data", I need to include a third-party library. In this case, Multer is the best choice.

The usage is pretty simple. At it’s most basic:

import express from 'express';
import multer from 'multer';
import path from 'node:path';

const app = express();

const upload = multer({
    dest: path.join(".", "public", "uploads")
});

app.post('/upload', upload.array('fieldname'), (req, res, next) => {
    // Array of files are stored in req.files
});

Using it with Middleware

Multer works well for what it does, but there’s one issue with the default usage: if you have middleware that relies on form data, multer has to run before that middleware, or the multipart/form-data will not have been parsed. This is an issue when dealing with CSRF, because Multer doesn’t do dry-runs - if it runs, the files get uploaded to the server.

Option 1: Remove files on error

The first approach is pretty simple: include middleware after both Multer and your middleware that relies on form data. Check the response status, and if it’s an error response (4xx or 5xx), iterate over req.files and remove each file manually.

e.g.

import fs from 'fs';

const removeFilesOnError = (req: Express.Request, res: Express.Response, next: Express.NextFunction) => {
    if (res.statusCode >= 400) {
        if (req.files) {
            (req.files as Express.Multer.File[]).forEach(file => {
                fs.unlinkSync(file.path);
            });
        } else if(req.file) {
            fs.unlinkSync(req.file.path);
        }
    }
};

app.post('/upload', upload.array('fieldname'), myOtherMiddleware, removeFilesOnError, (req, res, next) => {
    //
});

Option 2: Use MemoryStorage, and manually store files

By default, Multer uses DiskStorage, which automatically stores the files on your server. There is an option to use MemoryStorage, which adds a Buffer to each file containing the file contents. We can use this to only then store our files on a successful request:

const storage = multer.memoryStorage();
const upload = multer({
    storage,
});

const saveOnSuccess = (req: Express.Request, res: Express.Response, next: Express.NextFunction) => {

    if (res.statusCode < 400) {
        if (req.files) {
            (req.files as Express.Multer.File[]).map(file => {
                // req.container is a Dependency Injection container I've defined separately
                // Substitute this line with any random string generator
                const filename = req.container.get_random_string();

                const storagePath = path.join(".", "public", "uploads", filename);
                fs.createWriteStream(storagePath).write(file.buffer).close();
                file.path = storagePath;

                return file;
            });
        }
    }
};

app.post('/upload', upload.array('fieldname'), myOtherMiddleware, saveOnSuccess, (req, res, next) => {
    // req.files will contain an array of files where file.path is the saved location
});

Of the two approaches I’ve listed, I think this is the one I prefer. They’re both fairly easy to implement, but this one allows me a bit more control over when files get saved - there’s no risk of a later error leaving malicious files on my server. The only caveat is that a large number of files (or a few very large files) will cause the application to run out of memory.

A third option could be to allow uploads via DiskStorage, but move them into an isolated storage until it’s determined that the request was successful, at which point they can be published.

Finally, a note to myself for when I inevitably face this issue again in the future:

Do not attempt to write your own Mutlipart Form middleware, it won’t work and you’ll lose at least a full day.