Skip to content

fix(plugin): keep auto-inferred default response when only error Api*Response decorators are present#3867

Merged
kamilmysliwiec merged 3 commits into
nestjs:masterfrom
PeterTheOne:fix-error-only-response-decorators-suppressing-default
Apr 27, 2026
Merged

fix(plugin): keep auto-inferred default response when only error Api*Response decorators are present#3867
kamilmysliwiec merged 3 commits into
nestjs:masterfrom
PeterTheOne:fix-error-only-response-decorators-suppressing-default

Conversation

@PeterTheOne

@PeterTheOne PeterTheOne commented Apr 21, 2026

Copy link
Copy Markdown

Summary

Fixes #3862 — regression introduced in 11.2.7 via PR #3803.

PR #3803 added a guard that suppresses the auto-inferred default response whenever any Api*Response decorator is present. The guard doesn't distinguish success (2xx) from error (4xx/5xx) decorators, so a handler with only @ApiUnauthorizedResponse() (or any other error-status decorator) loses its auto-inferred 2xx in the OpenAPI spec. Downstream client generators (NSwag, openapi-generator, ...) then treat the real 2xx response as an exception at runtime.

This PR scopes the guard to decorators whose status code is below 400 so error factories no longer suppress the default. The redirect case the original PR fixed (@ApiFoundResponse on a @Redirect() handler) continues to work — 302 is still below 400.

Approach

Named Api*Response factories — status codes are derived from the decorator name via the same Api${PascalCase(HttpStatus key)}Response convention the factories are generated from in api-response.decorator.ts. Unknown names (ApiDefaultResponse, user-defined Api*Response) fall back to the previous behavior (treated as explicit), so no existing explicit-response pattern regresses.

Mirrors the forward-direction idiom in api-response.decorator.ts:148:

// forward: generating factories
.filter((key) => !isNaN(Number(HttpStatus[key])))
// reverse: decoder in this PR
const status = Number(HttpStatus[statusKey as keyof typeof HttpStatus]);
return isNaN(status) || status < 400;

Plain @ApiResponse({ status }) — the status argument is inspected: numeric literals (status: 500), the shorthand strings '1XX'/'2XX'/'3XX', and 'default' are classified by their code. Non-literal expressions (e.g. HttpStatus.OK) can't be evaluated at compile time and keep the pre-PR behavior (treated as explicit).

Behavior matrix

Handler decorators Before (11.2.7–11.3.2) After this PR
@Post + @ApiUnauthorizedResponse only 401 ❌ 201 + 401 ✅
@Get + @ApiNotFoundResponse only 404 ❌ 200 + 404 ✅
@Post + @ApiResponse({ status: 500 }) only 500 ❌ 201 + 500 ✅
@Post + @ApiCreatedResponse + @ApiUnauthorizedResponse 201 + 401 ✅ 201 + 401 ✅ (unchanged)
@Get @Redirect @ApiFoundResponse (#1639) 302 only ✅ 302 only ✅ (unchanged)

Tests

Disclaimers

  • Transparency: this PR was prepared in collaboration with an AI coding assistant. I reviewed and approved every change. Happy to iterate on feedback.
  • Given the subtlety of the 11.2.7 regression (silent OpenAPI spec diff, only surfaces in downstream client generators), reviewers may want to independently verify no other response-generation paths are affected.


type ClassMetadata = Record<string, ts.ObjectLiteralExpression>;

function isSuccessOrRedirectApiResponseArg(decorator: ts.Decorator): boolean {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can we move it to another dedicated file, or at least make it a method of the ControllerClassVisitor class

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — moved it to a private method on ControllerClassVisitor in fe80eb3. Felt like overkill to spin up a dedicated file for a ~20-line helper with one call site, but happy to extract further if you'd prefer.

@kamilmysliwiec

Copy link
Copy Markdown
Member

just checking to see if you're still willing to work on this PR @PeterTheOne, or if you would rather want us to focus on #3863 instead

@PeterTheOne

Copy link
Copy Markdown
Author

@kamilmysliwiec I did your nit comment. I don't mind if we go with the other PR, I just want this fixed asap :)

@kamilmysliwiec

Copy link
Copy Markdown
Member

excellent @PeterTheOne thank you! would you mind resolving PR conflicts? if so, ill get this merged and released asap

Peter Grassberger added 3 commits April 27, 2026 11:39
…Response decorators are present

PR nestjs#3803 added a guard that suppresses the auto-inferred default response
whenever any `Api*Response` decorator is present on a handler. The guard
doesn't distinguish success (2xx) from error (4xx/5xx) decorators, so a
handler with only `@ApiUnauthorizedResponse()` (or any other error-status
decorator) loses its auto-inferred 2xx in the OpenAPI spec. Downstream
client generators (NSwag, openapi-generator, ...) then treat the real
2xx response as an exception at runtime.

Scope the guard to decorators whose status code is below 400 so error
factories no longer suppress the default. The success/redirect case the
original PR fixed (`@ApiFoundResponse` on a `@Redirect()` handler)
continues to work — 302 is still below 400.

Status codes are derived from the decorator name via the same
`Api${PascalCase(HttpStatus key)}Response` convention the factories are
generated from in `api-response.decorator.ts`. Unknown names
(`ApiDefaultResponse`, user-defined) fall back to the previous behavior
(treated as explicit).

Closes nestjs#3862
…t/500 cases

Extend the fix so that a plain `@ApiResponse({ status: N })` is also classified
by its status code: a 4xx/5xx status no longer suppresses the auto-inferred 2xx.
Non-literal `status` expressions (e.g. `HttpStatus.OK`) can't be evaluated at
compile time and continue to be treated as explicit, preserving pre-existing
behavior.

Extend the regression fixture with:

  - `@Post @apiresponse({ status: 500 })` — asserts the default 201 is
    auto-added (the new arg-inspection case).
  - `@Get @reDIrect @ApiFoundResponse` — asserts no default is added, so the
    fix continues to honor nestjs#1639 for redirect handlers.

The existing `app.controller` fixture had baked in the 11.2.7 behavior on the
`create()` handler (multiple error-status `@ApiResponse` decorators); update it
to the restored pre-11.2.7 output (default 201 alongside the error entries).
…thod

Address PR review nit: move the standalone helper into ControllerClassVisitor
as a private method, since it's only used by addDecoratorToNode and doesn't
need its own module.
@PeterTheOne PeterTheOne force-pushed the fix-error-only-response-decorators-suppressing-default branch from fe80eb3 to f01f6aa Compare April 27, 2026 11:43
@PeterTheOne

Copy link
Copy Markdown
Author

Rebased onto current master, conflicts resolved (just a test-placement collision with the new @Query optional test). All plugin tests pass — should be ready to merge.

@kamilmysliwiec kamilmysliwiec merged commit b0a35f3 into nestjs:master Apr 27, 2026
1 check passed
@kamilmysliwiec

Copy link
Copy Markdown
Member

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Regression in 11.2.7: plugin drops auto-inferred 2xx response when only error @Api*Response decorators are present (PR #3803)

2 participants