fix(express): map http-errors errors to http exceptions (#18214)

http-errors errors raised before the handler, such as a body that is too large, now reach exception filters as an HttpException with their status, as on Fastify. The message of an error with expose: false is replaced by a status text.

Fixes #18213
This commit is contained in:
okxint authored and GitHub committed 2026-10-21 10:35:14 +02:00
1 parent 68d3a8f835
commit 7ea87653af
2 files changed
+55

No files matched your search

@@ -1,5 +1,6 @@
import {
BadRequestException,
HttpException,
HttpStatus,
InternalServerErrorException,
Logger,
@@ -675,6 +676,20 @@ export class ExpressAdapter extends AbstractHttpAdapter<
// encoding, e.g. '%FF' (#8915)
case error instanceof SyntaxError || error instanceof URIError:
return new BadRequestException(error.message);
// http-errors (used by body-parser, Express itself, etc.) carry a numeric statusCode
// and an expose flag. Without this case, errors like PayloadTooLargeError reach custom
// exception filters as a raw http-errors instance rather than an HttpException, so a
// @Catch(HttpException) filter never runs and a catch-all filter reports 500 (#18213).
case isHttpError(error):
return new HttpException(
// An error that must not reach the client only gets a status text,
// like the built-in exception filter answers it
error.expose === false
? (http.STATUS_CODES[error.statusCode] ?? 'Internal server error')
: error.message,
error.statusCode,
{ cause: error },
);
default:
return error;
}
@@ -734,3 +749,19 @@ export class ExpressAdapter extends AbstractHttpAdapter<
this.setHeaderIfNotExists(response, 'Content-Length', headers.length);
}
}
/**
* Returns true when `err` is an "http-errors"-shaped error (body-parser, Express, etc.).
* Mirrors the same check in BaseExceptionFilter.isHttpError().
*/
function isHttpError(
err: unknown,
): err is { statusCode: number; message: string; expose: boolean } {
return (
isObject(err) &&
err instanceof Error &&
typeof (err as any).expose === 'boolean' &&
typeof (err as any).statusCode === 'number' &&
(err as any).status === (err as any).statusCode
);
}
@@ -1,11 +1,13 @@
import {
BadRequestException,
HttpException,
RequestMethod,
StreamableFile,
UnauthorizedException,
} from '@nestjs/common';
import { ExpressAdapter } from '@nestjs/platform-express';
import express from 'express';
import createHttpError from 'http-errors';
import { PassThrough, Readable, Writable } from 'stream';
describe('ExpressAdapter', () => {
@@ -528,5 +530,27 @@ describe('ExpressAdapter', () => {
const result = expressAdapter.mapException(error);
expect(result).toBe(error);
});
it('should map an http-errors error to an HttpException with its status', () => {
const error = createHttpError(413, 'request entity too large');
const result = expressAdapter.mapException(error) as HttpException;
expect(result).toBeInstanceOf(HttpException);
expect(result.getStatus()).toBe(413);
expect(result.getResponse()).toBe('request entity too large');
expect(result.cause).toBe(error);
});
it.each([
[500, 'Internal Server Error'],
[599, 'Internal server error'],
])(
'should not expose the message of an http-errors error with status %i',
(status, message) => {
const error = createHttpError(status, 'internal detail');
const result = expressAdapter.mapException(error) as HttpException;
expect(result.getStatus()).toBe(status);
expect(result.getResponse()).toBe(message);
},
);
});
});