Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error: .swagger() must be called after .ready() #758

Closed
2 tasks done
Xhale1 opened this issue Sep 19, 2023 · 14 comments
Closed
2 tasks done

Error: .swagger() must be called after .ready() #758

Xhale1 opened this issue Sep 19, 2023 · 14 comments
Labels
documentation Improvements or additions to documentation good first issue Good for newcomers

Comments

@Xhale1
Copy link

Xhale1 commented Sep 19, 2023

Prerequisites

  • I have written a descriptive issue title
  • I have searched existing issues to ensure the bug has not already been reported

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:

Error: .swagger() must be called after .ready()

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:

// this function is called from server.ts with `fastify.register(routes)`
export default async function routes(fastify: FastifyInstance) {
  fastify.register(fastifySwagger, { 
    // ... options
 });
  
  // ... other code redacted
  // primarily just registering routes
  
  fastify.ready((err) => {
    if (err) throw err;
    fastify.swagger();
  });
}

Expected Behavior

Fastify swagger is registered after fastify is ready.

@mcollina
Copy link
Member

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?

@osbornm
Copy link

osbornm commented Sep 21, 2023

@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

@osbornm
Copy link

osbornm commented Sep 21, 2023

downgrading to 8.10.0 resolves the issue for us

@mcollina
Copy link
Member

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.

@osbornm
Copy link

osbornm commented Sep 21, 2023

@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

@mcollina
Copy link
Member

@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())
})

@smorcuend
Copy link

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()

@mcollina
Copy link
Member

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.

@Xhale1
Copy link
Author

Xhale1 commented Sep 25, 2023

@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())
  })
})

@mcollina
Copy link
Member

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:

  1. use await app.register() so that the code that follows has the onReady hook correctly setup
  2. use app.addHook('onReady') instead of .ready(), which only make sense top-level, which is the partial source of the confusion
  3. calls app.ready() after registering the plugin to start everything.

@mcollina
Copy link
Member

I think we should update the README.

@mcollina mcollina added good first issue Good for newcomers documentation Improvements or additions to documentation labels Sep 27, 2023
@PavelPolyakov
Copy link
Contributor

PavelPolyakov commented Jan 24, 2024

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
image

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

@mcollina
Copy link
Member

mcollina commented Feb 1, 2024

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.

@RicardoViteriR
Copy link

RicardoViteriR commented Mar 7, 2024

@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)

    })

}

@Uzlopak Uzlopak closed this as not planned Won't fix, can't repro, duplicate, stale Mar 7, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation good first issue Good for newcomers
Projects
None yet
Development

No branches or pull requests

7 participants