Skip to content

Fastify: /docs-json returns 200 with empty body since 11.3.0 (async handler regression) #3908

@tibohaffner

Description

@tibohaffner

Is there an existing issue that is already proposing this?

  • I have searched the existing issues

Potential Commit/PR that introduced the regression

Diff 11.2.7...11.3.0 in lib/swagger-module.ts — the JSON, YAML and swagger-ui-init.js route handlers were converted from sync to async to support an async patchDocumentOnRequest hook.

Versions

11.2.711.3.0 (regression introduced); still present in 11.4.2 (latest).

Describe the regression

After upgrading @nestjs/swagger from 11.2.7 to 11.3.0+, Swagger UI renders blank in the browser when using FastifyAdapter together with @fastify/compress (a very common setup).

Symptom (visible only in the browser / with an Accept-Encoding: br|gzip request — plain curl without compression looks fine):

  • GET /docs-json → HTTP 200, headers include content-encoding: br (or gzip), content-length: 0 → empty body
  • GET /docs/swagger-ui-init.js → same, empty body
  • Static UI assets (swagger-ui-bundle.js, swagger-ui.css, …) serve correctly because they go through @fastify/static, not through the affected route handlers.

The Swagger UI HTML page is served correctly and there are no errors in the browser console — only the JSON document and the init script come back empty, so swagger-ui has nothing to initialize.

Root cause

In lib/swagger-module.ts (11.3.0+), the JSON, YAML and swagger-ui-init.js route handlers are now async but still call res.send(...) without returning anything:

httpAdapter.get(normalizeRelPath(options.jsonDocumentUrl), async (req, res) => {
  res.type('application/json');
  const document = getBuiltDocument();
  // ...
  res.send(JSON.stringify(documentToSerialize)); // <-- no return
});

When @fastify/compress is registered, its onSend hook expects to receive the payload through the normal Fastify lifecycle. Async handlers should return the payload (or reply) so the hook can pipe it through brotli/gzip. Because the handler resolves to undefined, the compression hook serializes undefined and emits an empty compressed body, even though reply.send() was called manually inside.

Express (@nestjs/platform-express) is not affected because its res.send() ends the response synchronously without involving any post-handler hook pipeline. The bug is also invisible on Fastify alone (no @fastify/compress registered), which is why the unit tests didn't catch it.

The same pattern affects 5 handlers in lib/swagger-module.ts (11.4.2 line numbers): 128, 142, 188, 203 — and the existing inner return res.send(...) already present on lines 123 and 137 (inside the patchDocumentOnRequest branches) confirms the intended pattern.

Minimum reproduction code

import fastifyCompress from '@fastify/compress';
import { NestFactory } from '@nestjs/core';
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create<NestFastifyApplication>(AppModule, new FastifyAdapter());
  await app.register(fastifyCompress); // <-- the trigger
  const config = new DocumentBuilder().setTitle('App').setVersion('1.0').build();
  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('docs', app, document);
  await app.listen(3000);
}
bootstrap();
$ curl -is -H 'Accept-Encoding: br' http://localhost:3000/docs-json
HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
vary: accept-encoding
content-encoding: br
content-length: 0

# (empty body)

Confirmed fix (patched node_modules/@nestjs/swagger/dist/swagger-module.js locally, added return before each res.send(...) in the 4 affected async handlers, restarted the app):

$ curl -s -H 'Accept-Encoding: br' http://localhost:3000/docs-json | wc -c
11854   # brotli-compressed payload, decompresses to the full 198 KB OpenAPI doc

Removing @fastify/compress (or downgrading to @nestjs/swagger@11.2.7) also restores correct behavior.

Expected behavior

GET /docs-json, /docs-yaml, and /docs/swagger-ui-init.js should return their bodies regardless of Accept-Encoding, matching the 11.2.7 behavior and the Express adapter's behavior on 11.3.0+.

Suggested fix

Return the result of res.send(...) from each async handler in lib/swagger-module.ts:

async (req, res) => {
  res.type('application/json');
  // ...
  return res.send(JSON.stringify(documentToSerialize));
}

Affected lines in 11.4.2: 128, 142, 188, 203. The patchDocumentOnRequest branches (lines 123, 137) already use this pattern.

Other

  • @nestjs/swagger: 11.3.0 → 11.4.2 (broken with @fastify/compress), 11.2.7 (working)
  • @nestjs/platform-fastify: 11.1.20
  • fastify: 5.8.5
  • @fastify/compress: 8.3.1
  • Node: 24.x
  • OS: macOS 24.6.0
  • Package manager: pnpm 10.33.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions