From 9314b098bb6d5641417a9e0434e5d27487d71dc1 Mon Sep 17 00:00:00 2001 From: Carmine DiMascio Date: Sat, 6 Mar 2021 21:03:30 -0500 Subject: [PATCH] fix: #551 unhandled promise rejection on invalid api spec (#556) --- src/openapi.validator.ts | 38 +++++++++++----- test/invalid.apispec.spec.ts | 88 ++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 12 deletions(-) create mode 100644 test/invalid.apispec.spec.ts diff --git a/src/openapi.validator.ts b/src/openapi.validator.ts index 74f67a63..33eadbd2 100644 --- a/src/openapi.validator.ts +++ b/src/openapi.validator.ts @@ -1,5 +1,4 @@ import ono from 'ono'; -import ajv = require('ajv'); import * as express from 'express'; import * as _uniq from 'lodash.uniq'; import * as middlewares from './middlewares'; @@ -93,22 +92,37 @@ export class OpenApiValidator { installMiddleware(spec: Promise): OpenApiRequestHandler[] { const middlewares: OpenApiRequestHandler[] = []; - const pContext = spec.then((spec) => { - const apiDoc = spec.apiDoc; - const ajvOpts = this.ajvOpts.preprocessor; - const resOpts = this.options.validateResponses as ValidateRequestOpts; - const sp = new SchemaPreprocessor(apiDoc, ajvOpts, resOpts).preProcess(); - return { - context: new OpenApiContext(spec, this.options.ignorePaths), - responseApiDoc: sp.apiDocRes, - }; - }); + const pContext = spec + .then((spec) => { + const apiDoc = spec.apiDoc; + const ajvOpts = this.ajvOpts.preprocessor; + const resOpts = this.options.validateResponses as ValidateRequestOpts; + const sp = new SchemaPreprocessor( + apiDoc, + ajvOpts, + resOpts, + ).preProcess(); + return { + context: new OpenApiContext(spec, this.options.ignorePaths), + responseApiDoc: sp.apiDocRes, + error: null, + }; + }) + .catch((e) => { + return { + context: null, + responseApiDoc: null, + error: e, + }; + }); let inited = false; // install path params middlewares.push((req, res, next) => pContext - .then(({ context }) => { + .then(({ context, error }) => { + // Throw if any error occurred during spec load. + if (error) throw error; if (!inited) { // Would be nice to pass the current Router object here if the route // is attach to a Router and not the app. diff --git a/test/invalid.apispec.spec.ts b/test/invalid.apispec.spec.ts new file mode 100644 index 00000000..c6ae05b0 --- /dev/null +++ b/test/invalid.apispec.spec.ts @@ -0,0 +1,88 @@ +import * as express from 'express'; +import { Server } from 'http'; +import * as request from 'supertest'; +import * as OpenApiValidator from '../src'; +import { OpenAPIV3, OpenApiValidatorOpts } from '../src/framework/types'; +import { startServer } from './common/app.common'; + +describe('invalid api spec', () => { + it('should propagate spec errors when validateApiSpec is true', async () => { + const apiSpec = createApiSpec(); + const app = await createApp({ + apiSpec, + }); + await request(app).get('/dev/hello/echo').expect(500); + app.server.close(); + }); + it('should fail gracefully when validateApiSpec is false', async () => { + const apiSpec = createApiSpec(); + const app = await createApp({ + apiSpec, + validateApiSpec: false, + }); + await request(app).get('/dev/hello/echo').expect(500); + app.server.close(); + }); +}); + +async function createApp( + opts: OpenApiValidatorOpts, +): Promise { + const app = express(); + + app.use(OpenApiValidator.middleware(opts)); + app.use( + express.Router().get('/dev/hello/echo', (req, res) => { + res.status(200).send(req.params.value); + }), + ); + + await startServer(app, 3001); + return app; +} + +function createApiSpec(): OpenAPIV3.Document { + return { + openapi: '3.0.3', + info: { + title: 'The API', + version: '1.0.0', + description: 'Welcome to the API.', + }, + servers: [{ url: 'http://localhost:54321/v1', description: 'Running' }], + components: { + securitySchemes: { + AuthJWT: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + description: + 'Enter a JSON Web Token (JWT) to be sent with each request in the HTTP **Authorization** header.', + }, + }, + }, + security: [{ AuthJWT: [] }], + paths: { + '/dev/hello/echo': { + get: { + security: [], + summary: 'Responds with the request.', + description: '', + responses: { '200': { description: 'OK' } }, + parameters: { q: 'string' }, // <-- THE INCORRECT BIT + tags: ['dev/hello'], + }, + }, + '/dev/hello/err': { + get: { + security: [], + summary: 'Responds with an error.', + description: '', + responses: { '500': { description: 'Error' } }, + tags: ['dev/hello'], + }, + }, + }, + tags: [{ name: 'dev/hello', description: 'API introduction' }], + }; +}