diff --git a/examples/9-nestjs/src/filters/openapi-exception.filter.ts b/examples/9-nestjs/src/filters/openapi-exception.filter.ts index ba98e883..5e7413e8 100644 --- a/examples/9-nestjs/src/filters/openapi-exception.filter.ts +++ b/examples/9-nestjs/src/filters/openapi-exception.filter.ts @@ -8,7 +8,7 @@ export class OpenApiExceptionFilter implements ExceptionFilter { const ctx = host.switchToHttp(); const response = ctx.getResponse(); - response.status(error.status).json(error); + response.status(error.status).header(error.headers).json(error); } } @@ -22,4 +22,7 @@ interface ValidationError { }>; path?: string; name: string; + headers: { + [header: string]: string; + }; } diff --git a/src/framework/types.ts b/src/framework/types.ts index 577f2caa..5c69eb4b 100644 --- a/src/framework/types.ts +++ b/src/framework/types.ts @@ -533,17 +533,23 @@ export interface ValidationErrorItem { error_code?: string; } +interface ErrorHeaders { + Allow?: string; +} + export class HttpError extends Error implements ValidationError { status!: number; - message!: string; - errors!: ValidationErrorItem[]; path?: string; name!: string; + message!: string; + headers?: ErrorHeaders; + errors!: ValidationErrorItem[]; constructor(err: { status: number; path: string; name: string; message?: string; + headers?: ErrorHeaders; errors?: ValidationErrorItem[]; }) { super(err.name); @@ -551,15 +557,13 @@ export class HttpError extends Error implements ValidationError { this.status = err.status; this.path = err.path; this.message = err.message; - this.errors = - err.errors == undefined - ? [ - { - path: err.path, - message: err.message, - }, - ] - : err.errors; + this.headers = err.headers; + this.errors = err.errors ?? [ + { + path: err.path, + message: err.message, + }, + ]; } public static create(err: { @@ -634,6 +638,7 @@ export class MethodNotAllowed extends HttpError { constructor(err: { path: string; message?: string; + headers?: ErrorHeaders; overrideStatus?: number; }) { super({ @@ -641,6 +646,7 @@ export class MethodNotAllowed extends HttpError { path: err.path, name: 'Method Not Allowed', message: err.message, + headers: err.headers, }); } } diff --git a/src/middlewares/openapi.metadata.ts b/src/middlewares/openapi.metadata.ts index 6c9ebf82..e1d341c1 100644 --- a/src/middlewares/openapi.metadata.ts +++ b/src/middlewares/openapi.metadata.ts @@ -10,6 +10,7 @@ import { OpenApiRequestMetadata, OpenAPIV3, } from '../framework/types'; +import { httpMethods } from './parsers/schema.preprocessor'; export function applyOpenApiMetadata( openApiContext: OpenApiContext, @@ -30,6 +31,11 @@ export function applyOpenApiMetadata( throw new MethodNotAllowed({ path: req.path, message: `${req.method} method not allowed`, + headers: { + Allow: Object.keys(openApiContext.openApiRouteMap[openApiRoute]) + .filter((key) => httpMethods.has(key.toLowerCase())) + .join(', '), + }, }); } req.openapi = { diff --git a/src/middlewares/openapi.request.validator.ts b/src/middlewares/openapi.request.validator.ts index 050efadc..097e47a5 100644 --- a/src/middlewares/openapi.request.validator.ts +++ b/src/middlewares/openapi.request.validator.ts @@ -117,12 +117,12 @@ export class RequestValidator { req.params = openapi.pathParams ?? req.params; } - const schemaPoperties = validator.allSchemaProperties; + const schemaProperties = validator.allSchemaProperties; const mutator = new RequestParameterMutator( this.ajv, apiDoc, path, - schemaPoperties, + schemaProperties, ); mutator.modifyRequest(req); @@ -130,7 +130,7 @@ export class RequestValidator { if (!allowUnknownQueryParameters) { this.processQueryParam( req.query, - schemaPoperties.query, + schemaProperties.query, securityQueryParam, ); } @@ -151,15 +151,15 @@ export class RequestValidator { }; const schemaBody = validator?.schemaBody; const discriminator = schemaBody?.properties?.body?._discriminator; - const discriminatorValdiator = this.discriminatorValidator( + const discriminatorValidator = this.discriminatorValidator( req, discriminator, ); - const validatorBody = discriminatorValdiator ?? validator.validatorBody; + const validatorBody = discriminatorValidator ?? validator.validatorBody; const valid = validator.validatorGeneral(data); const validBody = validatorBody( - discriminatorValdiator ? data.body : data, + discriminatorValidator ? data.body : data, ); if (valid && validBody) { @@ -185,7 +185,7 @@ export class RequestValidator { private discriminatorValidator(req, discriminator) { if (discriminator) { const { options, property, validators } = discriminator; - const discriminatorValue = req.body[property]; // TODO may not alwasy be in this position + const discriminatorValue = req.body[property]; // TODO may not always be in this position if (options.find((o) => o.option === discriminatorValue)) { return validators[discriminatorValue]; } else { diff --git a/src/middlewares/openapi.security.ts b/src/middlewares/openapi.security.ts index d09979b9..c69a8e34 100644 --- a/src/middlewares/openapi.security.ts +++ b/src/middlewares/openapi.security.ts @@ -4,8 +4,6 @@ import { SecurityHandlers, OpenApiRequestMetadata, OpenApiRequestHandler, - NotFound, - MethodNotAllowed, InternalServerError, HttpError, } from '../framework/types'; @@ -29,7 +27,7 @@ export function security( securityHandlers: SecurityHandlers, ): OpenApiRequestHandler { return async (req, res, next) => { - // TODO move the folllowing 3 check conditions to a dedicated upstream middleware + // TODO move the following 3 check conditions to a dedicated upstream middleware if (!req.openapi) { // this path was not found in open api and // this path is not defined under an openapi base path @@ -38,7 +36,7 @@ export function security( } const openapi = req.openapi; - // use the local security object or fallbac to api doc's security or undefined + // use the local security object or fallback to api doc's security or undefined const securities: OpenAPIV3.SecurityRequirementObject[] = openapi.schema.security ?? apiDoc.security; @@ -152,7 +150,7 @@ class SecuritySchemes { : null; const promises = this.securities.map(async (s) => { if (Util.isEmptyObject(s)) { - // anonumous security + // anonymous security return [{ success: true }]; } return Promise.all( diff --git a/src/middlewares/parsers/schema.preprocessor.ts b/src/middlewares/parsers/schema.preprocessor.ts index c67f8028..25629695 100644 --- a/src/middlewares/parsers/schema.preprocessor.ts +++ b/src/middlewares/parsers/schema.preprocessor.ts @@ -61,7 +61,7 @@ if (!Array.prototype['flatMap']) { }; Object.defineProperty(Array.prototype, 'flatMap', { enumerable: false }); } -const httpMethods = new Set([ +export const httpMethods = new Set([ 'get', 'put', 'post', diff --git a/src/openapi.validator.ts b/src/openapi.validator.ts index 33eadbd2..9bd66260 100644 --- a/src/openapi.validator.ts +++ b/src/openapi.validator.ts @@ -142,7 +142,7 @@ export class OpenApiValidator { middlewares.push((req, res, next) => pContext .then(({ context, responseApiDoc }) => { - metamw = metamw || this.metadataMiddlware(context, responseApiDoc); + metamw = metamw || this.metadataMiddleware(context, responseApiDoc); return metamw(req, res, next); }) .catch(next), @@ -252,7 +252,7 @@ export class OpenApiValidator { } } - private metadataMiddlware( + private metadataMiddleware( context: OpenApiContext, responseApiDoc: OpenAPIV3.Document, ) { @@ -371,12 +371,12 @@ export class OpenApiValidator { } }); defaultSerDes.forEach((currentDefaultSerDes) => { - let defautSerDesOverride = options.serDes.find( + let defaultSerDesOverride = options.serDes.find( (currentOptionSerDes) => { return currentDefaultSerDes.format === currentOptionSerDes.format; }, ); - if (!defautSerDesOverride) { + if (!defaultSerDesOverride) { options.serDes.push(currentDefaultSerDes); } }); diff --git a/test/allow.header.spec.ts b/test/allow.header.spec.ts new file mode 100644 index 00000000..bc4ccd9b --- /dev/null +++ b/test/allow.header.spec.ts @@ -0,0 +1,85 @@ +import { expect } from 'chai'; +import * as express from 'express'; +import { Server } from 'http'; +import * as request from 'supertest'; +import * as packageJson from '../package.json'; +import * as OpenApiValidator from '../src'; +import { OpenAPIV3 } from '../src/framework/types'; +import { startServer } from './common/app.common'; + +describe(packageJson.name, () => { + let app = null; + + before(async () => { + app = await createApp(); + }); + + after(() => { + app.server.close(); + }); + + it('adds allow header to 405 - Method Not Allowed', async () => + request(app) + .put('/v1/pets/greebo') + .expect(405) + .then((response) => { + expect(response.header).to.include({ allow: 'POST, GET' }); + })); +}); + +async function createApp(): Promise { + const app = express(); + + app.use( + OpenApiValidator.middleware({ + apiSpec: createApiSpec(), + validateRequests: true, + }), + ); + app.use( + express + .Router() + .get('/v1/pets/:petId', () => ['cat', 'dog']) + .post('/v1/pets/:petId', (req, res) => res.json(req.body)), + ); + + await startServer(app, 3001); + return app; +} + +function createApiSpec(): OpenAPIV3.Document { + return { + openapi: '3.0.3', + info: { + title: 'Petstore API', + version: '1.0.0', + }, + servers: [ + { + url: '/v1/', + }, + ], + paths: { + '/pets/{petId}': { + parameters: [ + { + in: 'path', + name: 'petId', + required: true, + schema: { type: 'string' }, + }, + ], + get: { + responses: { + '200': { description: 'GET Pet' }, + }, + }, + post: { + responses: { + '200': { description: 'POST Pet' }, + }, + }, + }, + }, + }; +}