Closed Xhale1 closed 4 months ago
The problem with .swagger()
before .ready()
is that it generates incorrect schema, and there is no way to make it correct before the ready flow completed.
Whats your actual bug? Could you add a reproduce?
@mcollina I'm having the same issue with 8.10.1 the ready function takes a callback that will be called after all plugins are loaded. 8.10.0 handled this registration method correctly, it's only after the patch update that this error occurs.
see: https://fastify.dev/docs/latest/Reference/Server/#ready
downgrading to 8.10.0 resolves the issue for us
Can you provide steps to reproduce? We often need a reproducible example, e.g. some code that allows someone else to recreate your problem by just copying and pasting it. If it involves more than a couple of different file, create a new repository on GitHub and add a link to that.
@mcollina @Xhale1 did post the steps to reproduce when they opened the issue. it's just changing the call to ready and swagger to
fastify.ready((err) => {
if (err) throw err;
fastify.swagger();
});
on any app you have
@osbornm the following works correctly:
'use strict'
const fastify = require('fastify')()
fastify.register(require('../index'), {
openapi: {
info: {
title: 'Test swagger',
description: 'testing the fastify swagger api',
version: '0.1.0'
},
servers: [{
url: 'http://localhost'
}],
components: {
securitySchemes: {
apiKey: {
type: 'apiKey',
name: 'apiKey',
in: 'header'
}
}
}
},
hideUntagged: true,
exposeRoute: true
})
fastify.register(async function (fastify) {
fastify.put('/some-route/:id', {
schema: {
description: 'post some data',
tags: ['user', 'code'],
summary: 'qwerty',
security: [{ apiKey: [] }],
params: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'user id'
}
}
},
body: {
type: 'object',
properties: {
hello: { type: 'string' },
obj: {
type: 'object',
properties: {
some: { type: 'string' }
}
}
}
},
response: {
201: {
description: 'Succesful response',
type: 'object',
properties: {
hello: { type: 'string' }
}
},
default: {
description: 'Default response',
type: 'object',
properties: {
foo: { type: 'string' }
}
}
}
}
}, (req, reply) => { reply.send({ hello: `Hello ${req.body.hello}` }) })
fastify.post('/some-route/:id', {
schema: {
description: 'post some data',
summary: 'qwerty',
security: [{ apiKey: [] }],
params: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'user id'
}
}
},
body: {
type: 'object',
properties: {
hello: { type: 'string' },
obj: {
type: 'object',
properties: {
some: { type: 'string' }
}
}
}
},
response: {
201: {
description: 'Succesful response',
type: 'object',
properties: {
hello: { type: 'string' }
}
}
}
}
}, (req, reply) => { reply.send({ hello: `Hello ${req.body.hello}` }) })
})
fastify.ready(err => {
if (err) throw err
console.log(fastify.swagger())
})
Error persists when fastify-swagger is loaded by @fastify/autoload:
export default fp<SwaggerOptions>(async fastify => {
fastify.register(fastifySwagger, {
openapi: {...}
})
await fastify.register(fastifySwaggerUi, {
routePrefix: 'docs',
uiConfig: {
docExpansion: 'full',
deepLinking: false
},
uiHooks: {
onRequest: function (request, reply, next) {
next()
},
preHandler: function (request, reply, next) {
next()
}
},
staticCSP: true,
transformStaticCSP: header => header,
transformSpecification: swaggerObject => {
return swaggerObject
},
transformSpecificationClone: true
})
})
Stack trace:
[App] [14:02:08.165] ERROR (298847): .swagger() must be called after .ready() [App] reqId: "req-7" [App] req: { [App] "method": "GET", [App] "url": "/docs/json", [App] "hostname": "localhost:9000", [App] "remoteAddress": "::1", [App] "remotePort": 49798 [App] } [App] res: { [App] "statusCode": 500 [App] } [App] err: { [App] "type": "Error", [App] "message": ".swagger() must be called after .ready()", [App] "stack": [App] Error: .swagger() must be called after .ready()
Can you provide steps to reproduce? We often need a reproducible example, e.g. some code that allows someone else to recreate your problem by just copying and pasting it. If it involves more than a couple of different file, create a new repository on GitHub and add a link to that.
Note that if it throws that error, it means that your swagger document is incomplete or invalid. There is already a bug in your code, you just don't know about it.
@mcollina I believe in order to reproduce the issue you would need to register the swagger plugin from within an encapsulated context (perhaps using the wrong terminology here). Your example would become:
'use strict'
const fastify = require('fastify')()
fastify.register(async function (fastify) {
fastify.register(require('../index'), {
openapi: {
info: {
title: 'Test swagger',
description: 'testing the fastify swagger api',
version: '0.1.0'
},
servers: [{
url: 'http://localhost'
}],
components: {
securitySchemes: {
apiKey: {
type: 'apiKey',
name: 'apiKey',
in: 'header'
}
}
}
},
hideUntagged: true,
exposeRoute: true
})
fastify.put('/some-route/:id', {
schema: {
description: 'post some data',
tags: ['user', 'code'],
summary: 'qwerty',
security: [{ apiKey: [] }],
params: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'user id'
}
}
},
body: {
type: 'object',
properties: {
hello: { type: 'string' },
obj: {
type: 'object',
properties: {
some: { type: 'string' }
}
}
}
},
response: {
201: {
description: 'Succesful response',
type: 'object',
properties: {
hello: { type: 'string' }
}
},
default: {
description: 'Default response',
type: 'object',
properties: {
foo: { type: 'string' }
}
}
}
}
}, (req, reply) => { reply.send({ hello: `Hello ${req.body.hello}` }) })
fastify.post('/some-route/:id', {
schema: {
description: 'post some data',
summary: 'qwerty',
security: [{ apiKey: [] }],
params: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'user id'
}
}
},
body: {
type: 'object',
properties: {
hello: { type: 'string' },
obj: {
type: 'object',
properties: {
some: { type: 'string' }
}
}
}
},
response: {
201: {
description: 'Succesful response',
type: 'object',
properties: {
hello: { type: 'string' }
}
}
}
}
}, (req, reply) => { reply.send({ hello: `Hello ${req.body.hello}` }) })
fastify.ready(err => {
if (err) throw err
console.log(fastify.swagger())
})
})
Unfortunately your code is hitting the race condition that would generate invalid schemas.
Here is an updated version fixed:
'use strict'
const fastify = require('fastify')()
fastify.register(async function (fastify) {
// await is necessary here to make sure its onReady hook is registered
// before the one of this module
await fastify.register(require('./index'), {
openapi: {
info: {
title: 'Test swagger',
description: 'testing the fastify swagger api',
version: '0.1.0'
},
servers: [{
url: 'http://localhost'
}],
components: {
securitySchemes: {
apiKey: {
type: 'apiKey',
name: 'apiKey',
in: 'header'
}
}
}
},
hideUntagged: true,
exposeRoute: true
})
fastify.put('/some-route/:id', {
schema: {
description: 'post some data',
tags: ['user', 'code'],
summary: 'qwerty',
security: [{ apiKey: [] }],
params: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'user id'
}
}
},
body: {
type: 'object',
properties: {
hello: { type: 'string' },
obj: {
type: 'object',
properties: {
some: { type: 'string' }
}
}
}
},
response: {
201: {
description: 'Succesful response',
type: 'object',
properties: {
hello: { type: 'string' }
}
},
default: {
description: 'Default response',
type: 'object',
properties: {
foo: { type: 'string' }
}
}
}
}
}, (req, reply) => { reply.send({ hello: `Hello ${req.body.hello}` }) })
fastify.post('/some-route/:id', {
schema: {
description: 'post some data',
summary: 'qwerty',
security: [{ apiKey: [] }],
params: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'user id'
}
}
},
body: {
type: 'object',
properties: {
hello: { type: 'string' },
obj: {
type: 'object',
properties: {
some: { type: 'string' }
}
}
}
},
response: {
201: {
description: 'Succesful response',
type: 'object',
properties: {
hello: { type: 'string' }
}
}
}
}
}, (req, reply) => { reply.send({ hello: `Hello ${req.body.hello}` }) })
// Use onReady hook instead of ready event to make sure the hook from
// @fastify/swagger is triggered before this one
fastify.addHook('onReady', () => {
console.log(fastify.swagger())
})
})
// Load all the plugins
fastify.ready()
This does:
await app.register()
so that the code that follows has the onReady
hook correctly setupapp.addHook('onReady')
instead of .ready()
, which only make sense top-level, which is the partial source of the confusionapp.ready()
after registering the plugin to start everything.I think we should update the README.
Hi @mcollina ,
Thanks for the clarification. I'm not sure I got it completely though.
My question — can there be such @fastify/swagger
integration, that it's done in non-sync way for the main fastify
instance configuration and still work with the routes which are declared before/after it?
Here is an example, based on what you've showcased here, but I put fastify.post
outside of the plugin:
import Fastify from "fastify";
import fastifySwagger from "@fastify/swagger";
import fastifySwaggerUI from "@fastify/swagger-ui";
const fastify = Fastify()
fastify.post('/some-route/:id', {
schema: {
description: 'post some data',
tags: ['user', 'code'],
summary: 'qwerty',
security: [{ apiKey: [] }],
params: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'user id'
}
}
},
body: {
type: 'object',
properties: {
hello: { type: 'string' },
obj: {
type: 'object',
properties: {
some: { type: 'string' }
}
}
}
},
response: {
201: {
description: 'Succesful response',
type: 'object',
properties: {
hello: { type: 'string' }
}
},
default: {
description: 'Default response',
type: 'object',
properties: {
foo: { type: 'string' }
}
}
}
}
}, (req, reply) => { reply.send({ hello: `Hello ${req.body.hello}` }) })
fastify.register(async function (fastify) {
// await is necessary here to make sure its onReady hook is registered
// before the one of this module
await fastify.register(fastifySwagger, {
openapi: {
info: {
title: 'Test swagger',
description: 'testing the fastify swagger api',
version: '0.1.0'
},
servers: [{
url: 'http://localhost'
}],
components: {
securitySchemes: {
apiKey: {
type: 'apiKey',
name: 'apiKey',
in: 'header'
}
}
}
},
hideUntagged: true,
exposeRoute: true
})
fastify.put('/some-route/:id', {
schema: {
description: 'post some data',
tags: ['user', 'code'],
summary: 'qwerty',
security: [{ apiKey: [] }],
params: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'user id'
}
}
},
body: {
type: 'object',
properties: {
hello: { type: 'string' },
obj: {
type: 'object',
properties: {
some: { type: 'string' }
}
}
}
},
response: {
201: {
description: 'Succesful response',
type: 'object',
properties: {
hello: { type: 'string' }
}
},
default: {
description: 'Default response',
type: 'object',
properties: {
foo: { type: 'string' }
}
}
}
}
}, (req, reply) => { reply.send({ hello: `Hello ${req.body.hello}` }) })
await fastify.register(fastifySwaggerUI, {
routePrefix: '/documentation',
uiConfig: {
docExpansion: 'full',
deepLinking: false
},
uiHooks: {
onRequest: function (request, reply, next) { next() },
preHandler: function (request, reply, next) { next() }
},
staticCSP: true,
transformStaticCSP: (header) => header,
transformSpecification: (swaggerObject, request, reply) => { return swaggerObject },
transformSpecificationClone: true
})
// Use onReady hook instead of ready event to make sure the hook from
// @fastify/swagger is triggered before this one
fastify.addHook('onReady', () => {
console.log(fastify.swagger())
})
})
// Load all the plugins
fastify.ready()
// Load all the plugins
await fastify.listen({ port: parseInt(process.env.PORT) || 3030 });
When I run node index.mjs
, then only PUT
route is available in Swagger UI
Other words, how can I properly register @fastify/swagger
, that it catches all the routes, in case I work with the runtime without top level await
?
Thanks
This is expected. Moving it outside of the plugin would make it impossible for swagger to listen to the events. It's what enable us to have multiple instance of swagger in the same app.
@PavelPolyakov hi, you need to use fastify-plugin
to make your example work. This way, the swagger plugin is available to other contexts.
Here is how I've setup my application (I use @scalar/fastify-api-reference for the UI):
./app.ts
import Fastify, { FastifyInstance } from "fastify";
import { privateContext } from "./modules/private";
import { publicContext } from "./modules/public";
import apiDocs from "./plugins/fastify-swagger/swagger";
export async function buildApp(): Promise<any> {
const app: FastifyInstance = Fastify({
trustProxy: true,
});
await app.register(apiDocs)
app.register(publicContext)
app.register(privateContext)
app.ready()
return app
}
./plugins/fastify-swagger/swagger.ts
import fp from 'fastify-plugin';
async function apiDocs(fastify: any, options: any) {
await fastify.register(require('@fastify/swagger'), {
swagger: {
info: {
title: 'Test swagger',
description: 'Swagger API',
version: '0.1.0'
},
externalDocs: {
url: 'https://swagger.io',
description: 'Find more info here'
},
host: 'localhost',
schemes: ['http'],
consumes: ['application/json'],
produces: ['application/json'],
tags: [
{ name: 'user', description: 'User related end-points' },
{ name: 'code', description: 'Code related end-points' }
],
definitions: {
User: {
type: 'object',
required: ['id', 'email'],
properties: {
id: { type: 'string', format: 'uuid' },
firstName: { type: 'string' },
lastName: { type: 'string' },
email: { type: 'string', format: 'email' }
}
}
},
securityDefinitions: {
apiKey: {
type: 'apiKey',
name: 'apiKey',
in: 'header'
}
}
}
})
await fastify.register(require('@scalar/fastify-api-reference'), {
routePrefix: '/reference',
configuration: {
theme: 'default',
},
})
fastify.addHook('onReady', () => {
console.log(fastify.swagger())
})
}
export default fp(apiDocs, { name: 'Swagger' });
./modules/public/index.ts
import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { businessStatistics } from "./statistics";
export async function publicContext(app: FastifyInstance) {
app.register(async function publicContext(app: any) {
app.register(businessStatistics)
})
}
Prerequisites
Fastify version
4.23.2
Plugin version
8.10.1
Node.js version
18.17.1
Operating system
macOS
Operating system version (i.e. 20.04, 11.3, 10)
13.5.2
Description
Upon updating from 8.10.0 to 8.10.1, I'm now receiving this error when trying to start my Fastify server:
Steps to Reproduce
Register the fastify swagger plugin in it's own encapsulation context (not sure if this is the exact right term, basically just it's own function with other routes), and use the following code to register it:
Expected Behavior
Fastify swagger is registered after fastify is ready.