diff --git a/packages/platform-express/adapters/express-adapter.ts b/packages/platform-express/adapters/express-adapter.ts index 9c047833ef..359b0607ba 100644 --- a/packages/platform-express/adapters/express-adapter.ts +++ b/packages/platform-express/adapters/express-adapter.ts @@ -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 + ); +} diff --git a/packages/platform-express/test/adapters/express-adapter.spec.ts b/packages/platform-express/test/adapters/express-adapter.spec.ts index 8b5e7d68ab..4bdc24e9b0 100644 --- a/packages/platform-express/test/adapters/express-adapter.spec.ts +++ b/packages/platform-express/test/adapters/express-adapter.spec.ts @@ -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); + }, + ); }); });