fix(express): send the standard body for an exception wrapping an error (#18199)

* test(express): cover an exception whose response is an error

ExpressAdapter.reply() hands an Error body, as in
new UnauthorizedException(err), to response.json(), which serializes
the enumerable properties of the Error (such as the request config of
an HTTP client error, with its authorization header) with the status of
the exception. The FastifyAdapter sends the standard body of the status
instead.

The new unit and e2e specs expect the body the FastifyAdapter sends, so
they fail until the ExpressAdapter does the same.

* fix(express): send the standard body for an exception wrapping an error

ExpressAdapter.reply() handed an Error body, as in
new UnauthorizedException(err), to response.json(), which serializes the
enumerable properties of the Error with the status of the exception. For
an HTTP client error that is its request config, including the
authorization header the request was sent with.

An Error body sent with an error status now gets the standard body of
that status instead, e.g. {"statusCode":401,"message":"Unauthorized"},
as the FastifyAdapter sends since 614d1ac7e, so both adapters answer
alike.
This commit is contained in:
Luke Son authored and GitHub committed 2026-10-20 08:40:06 +02:00
1 parent 4f5d894b47
commit 876a182c60
3 files changed
+40 -1

No files matched your search

+11 -1
View File
@@ -39,7 +39,7 @@ describe('Error messages', () => {
let app: INestApplication;
beforeEach(async () => {
const module = await Test.createTestingModule({
controllers: [ErrorsController],
controllers: [ErrorsController, ErrorBodyController],
}).compile();
app = module.createNestApplication();
@@ -89,6 +89,16 @@ describe('Error messages', () => {
});
});
it(`/GET (Unauthorized when the body of the HttpException is an Error)`, async () => {
return request(server)
.get('/error-body')
.expect(HttpStatus.UNAUTHORIZED)
.expect({
statusCode: 401,
message: 'Unauthorized',
});
});
afterEach(async () => {
await app.close();
});
@@ -200,6 +200,17 @@ export class ExpressAdapter extends AbstractHttpAdapter<
.pipe<Writable>(response)
.on('error', (err: Error) => body.errorLogger(err));
}
// response.json() serializes the enumerable properties of an Error, e.g.
// the request config of an HTTP client error.
if (
body instanceof Error &&
response.statusCode >= HttpStatus.BAD_REQUEST
) {
body = {
statusCode: response.statusCode,
message: http.STATUS_CODES[response.statusCode] ?? 'Error',
};
}
const responseContentType = response.getHeader('Content-Type');
if (
typeof responseContentType === 'string' &&
@@ -2,6 +2,7 @@ import {
BadRequestException,
RequestMethod,
StreamableFile,
UnauthorizedException,
} from '@nestjs/common';
import { ExpressAdapter } from '@nestjs/platform-express';
import express from 'express';
@@ -339,6 +340,23 @@ describe('ExpressAdapter', () => {
expect(response.json).toHaveBeenCalledWith(body);
});
it('should send the standard body of the status for an Error body', () => {
const response = Object.assign(createResponse(), { statusCode: 200 });
response.status.mockImplementation(code => (response.statusCode = code));
const error = Object.assign(new Error('invalid signature'), {
config: { headers: { authorization: 'Bearer secret' } },
});
const exception = new UnauthorizedException(error);
expressAdapter.reply(response, exception.getResponse(), 401);
expect(response.json).toHaveBeenCalledExactlyOnceWith({
statusCode: 401,
message: 'Unauthorized',
});
expect(response.send).not.toHaveBeenCalled();
});
it('should keep the content type of a text body sent with an error status', () => {
const response = createResponse();
response.getHeader.mockReturnValue('text/html');