Skip to content

Commit

Permalink
Fixing uncaught exception when using limitError (#1600)
Browse files Browse the repository at this point in the history
The `limitHandler` of `express-fileupload` called asynchronously for a
failed file before finishing the upload.
In the end `express-fileupload` calls `next()`.
All that leads to calling `next()` multiple times which leads to
uncaught exception due to the way the error handling in express is
organised.
The `next()` must be called once after completing the file processing.
  • Loading branch information
RobinTail authored Mar 2, 2024
1 parent 479a45d commit 2d08bbc
Show file tree
Hide file tree
Showing 6 changed files with 60 additions and 8 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

## Version 17

### v17.1.2

- Fixed Uncaught Exception when using `limitError` feature.
- The exception was caused by excessive `next()` call from `express-fileupload` after handling the `limitError`.
- The issue did not affect the actual response since it had already been sent.
- In general, the problem arose due to asynchronous processing.
- The version introduces an upload failure handler instead of relying on the `limitHandler` of `express-fileupload`.
- Thus, handling the failed uploads is carried out after completing them.
- The specified `limitError` is only applicable to the `fileSize` limit, other limits do not trigger errors.
- The `limitError` feature introduced in v17.1.0.

### v17.1.1

- Fixed wrong status code sending in case of upload failures when `limitError` is `HttpError`.
Expand Down
2 changes: 1 addition & 1 deletion src/config-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ type UploadOptions = Pick<
| "limits"
> & {
/**
* @desc The error to throw when the file exceeds the configured limits (handled by errorHandler).
* @desc The error to throw when the file exceeds the configured fileSize limit (handled by errorHandler).
* @see limits
* @override limitHandler
* @example createHttpError(413, "The file is too large")
Expand Down
12 changes: 12 additions & 0 deletions src/server-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,15 @@ export const createNotFoundHandler =
});
}
};

export const createUploadFailueHandler =
(error: Error): RequestHandler =>
(req, {}, next) => {
const failedFile = Object.values(req?.files || [])
.flat()
.find(({ truncated }) => truncated);
if (failedFile) {
return next(error);
}
next();
};
5 changes: 4 additions & 1 deletion src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { Routing, initRouting } from "./routing";
import {
createNotFoundHandler,
createParserFailureHandler,
createUploadFailueHandler,
} from "./server-helpers";

const makeCommonEntities = async (config: CommonConfig) => {
Expand Down Expand Up @@ -58,9 +59,11 @@ export const createServer = async (config: ServerConfig, routing: Routing) => {
...derivedConfig,
abortOnLimit: false,
parseNested: true,
limitHandler: limitError && (({}, {}, next) => next(limitError)),
}),
);
if (limitError) {
app.use(createUploadFailueHandler(limitError));
}
}
if (config.server.rawParser) {
app.use(config.server.rawParser);
Expand Down
31 changes: 31 additions & 0 deletions tests/unit/server-helpers.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
createNotFoundHandler,
createParserFailureHandler,
createUploadFailueHandler,
} from "../../src/server-helpers";
import { describe, expect, test, vi } from "vitest";
import winston from "winston";
Expand Down Expand Up @@ -137,4 +138,34 @@ describe("Server helpers", () => {
);
});
});

describe("createUploadFailueHandler()", () => {
const error = new Error("Too heavy");

test.each([
{
files: { one: { truncated: true } },
},
{
files: { one: [{ truncated: false }, { truncated: true }] },
},
])("should handle truncated files by calling next with error %#", (req) => {
const handler = createUploadFailueHandler(error);
const next = vi.fn();
handler(req as unknown as Request, {} as Response, next);
expect(next).toHaveBeenCalledWith(error);
});

test.each([
{},
{ files: {} },
{ files: { one: { truncated: false } } },
{ file: { one: [{ truncated: false }] } },
])("should call next when all uploads succeeded %#", (req) => {
const handler = createUploadFailueHandler(error);
const next = vi.fn();
handler(req as unknown as Request, {} as Response, next);
expect(next).toHaveBeenCalledWith();
});
});
});
7 changes: 1 addition & 6 deletions tests/unit/server.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,18 +222,13 @@ describe("Server", () => {
},
};
await createServer(configMock, routingMock);
expect(appMock.use).toHaveBeenCalledTimes(4);
expect(appMock.use).toHaveBeenCalledTimes(5);
expect(fileUploadMock).toHaveBeenCalledTimes(1);
expect(fileUploadMock).toHaveBeenCalledWith({
abortOnLimit: false,
parseNested: true,
limits: { fileSize: 1024 },
limitHandler: expect.any(Function),
});
// limitHandler and limitError test
const nextMock = vi.fn();
fileUploadMock.mock.calls[0][0].limitHandler({}, {}, nextMock);
expect(nextMock).toHaveBeenCalledWith(new Error("Too heavy"));
});

test("should enable raw on request", async () => {
Expand Down

0 comments on commit 2d08bbc

Please sign in to comment.