Is there an existing issue that is already proposing this?
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.7 → 11.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
Is there an existing issue that is already proposing this?
Potential Commit/PR that introduced the regression
Diff 11.2.7...11.3.0 in
lib/swagger-module.ts— the JSON, YAML andswagger-ui-init.jsroute handlers were converted from sync toasyncto support an asyncpatchDocumentOnRequesthook.Versions
11.2.7→11.3.0(regression introduced); still present in11.4.2(latest).Describe the regression
After upgrading
@nestjs/swaggerfrom 11.2.7 to 11.3.0+, Swagger UI renders blank in the browser when usingFastifyAdaptertogether with@fastify/compress(a very common setup).Symptom (visible only in the browser / with an
Accept-Encoding: br|gziprequest — plaincurlwithout compression looks fine):GET /docs-json→ HTTP 200, headers includecontent-encoding: br(orgzip),content-length: 0→ empty bodyGET /docs/swagger-ui-init.js→ same, empty bodyswagger-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 andswagger-ui-init.jsroute handlers are nowasyncbut still callres.send(...)without returning anything:When
@fastify/compressis registered, itsonSendhook expects to receive the payload through the normal Fastify lifecycle. Async handlers should return the payload (orreply) so the hook can pipe it through brotli/gzip. Because the handler resolves toundefined, the compression hook serializesundefinedand emits an empty compressed body, even thoughreply.send()was called manually inside.Express (
@nestjs/platform-express) is not affected because itsres.send()ends the response synchronously without involving any post-handler hook pipeline. The bug is also invisible on Fastify alone (no@fastify/compressregistered), 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 innerreturn res.send(...)already present on lines 123 and 137 (inside thepatchDocumentOnRequestbranches) confirms the intended pattern.Minimum reproduction code
Confirmed fix (patched
node_modules/@nestjs/swagger/dist/swagger-module.jslocally, addedreturnbefore eachres.send(...)in the 4 affected async handlers, restarted the app):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.jsshould return their bodies regardless ofAccept-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 inlib/swagger-module.ts:Affected lines in 11.4.2: 128, 142, 188, 203. The
patchDocumentOnRequestbranches (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.20fastify: 5.8.5@fastify/compress: 8.3.1