From 03b46ff021b84e17d5f19844a8385b2395b7d5b7 Mon Sep 17 00:00:00 2001 From: "Daniel A. White" Date: Thu, 17 Feb 2022 09:30:05 -0500 Subject: [PATCH] allow for empty request bodies to skip validation, fixes #1939 (#1990) --- .../__tests__/body-params-validation.spec.ts | 43 +++++++++++++++++++ packages/http/src/validator/index.ts | 35 ++++++++++++--- ...e-content-type-if-body-included.oas3_1.txt | 26 +++++++++++ ...nore-content-type-if-empty-body.oas3_1.txt | 22 ++++++++++ 4 files changed, 119 insertions(+), 7 deletions(-) create mode 100644 test-harness/specs/validate-body-params/enforce-content-type-if-body-included.oas3_1.txt create mode 100644 test-harness/specs/validate-body-params/ignore-content-type-if-empty-body.oas3_1.txt diff --git a/packages/http-server/src/__tests__/body-params-validation.spec.ts b/packages/http-server/src/__tests__/body-params-validation.spec.ts index e4f28baa3..4ac908699 100644 --- a/packages/http-server/src/__tests__/body-params-validation.spec.ts +++ b/packages/http-server/src/__tests__/body-params-validation.spec.ts @@ -373,6 +373,37 @@ describe('body params validation', () => { }, }, }, + { + id: '?http-operation-id?', + method: 'get', + path: '/empty-body', + responses: [ + { + code: '200', + headers: [], + contents: [ + { + mediaType: 'text/plain', + schema: { + type: 'string', + $schema: 'http://json-schema.org/draft-07/schema#', + }, + examples: [], + encodings: [], + }, + ], + }, + ], + servers: [], + request: { + headers: [], + query: [], + cookie: [], + path: [], + }, + tags: [], + security: [], + }, ]); }); @@ -452,6 +483,16 @@ describe('body params validation', () => { const response = await makeRequest('/json-body-optional', { method: 'POST' }); expect(response.status).toBe(200); }); + + describe('and no content is specified', () => { + test('returns 200', async () => { + const response = await makeRequest('/empty-body', { + method: 'GET', + headers: { 'content-type': 'application/json' }, + }); + expect(response.status).toBe(200); + }); + }); }); describe('when body with unsupported content-type is used', () => { @@ -459,6 +500,7 @@ describe('body params validation', () => { const response = await makeRequest('/json-body-optional', { method: 'POST', headers: { 'content-type': 'application/xml' }, + body: 'some xml', }); expect(response.status).toBe(415); }); @@ -472,6 +514,7 @@ describe('body params validation', () => { headers: { 'content-type': 'application/csv', }, + body: 'type,name\nfoo,foobar', }); expect(response.status).toBe(415); await expect(response.json()).resolves.toMatchObject({ type: 'foo' }); diff --git a/packages/http/src/validator/index.ts b/packages/http/src/validator/index.ts index 844725d3d..09c42539f 100644 --- a/packages/http/src/validator/index.ts +++ b/packages/http/src/validator/index.ts @@ -16,7 +16,7 @@ import { sequenceOption, sequenceValidation } from '../combinators'; import { pipe } from 'fp-ts/function'; import { inRange, isMatch } from 'lodash'; import { URI } from 'uri-template-lite'; -import { IHttpRequest, IHttpResponse } from '../types'; +import { IHttpRequest, IHttpResponse, IHttpNameValue } from '../types'; import { findOperationResponse } from './utils/spec'; import { validateBody, validateHeaders, validatePath, validateQuery } from './validators'; import { NonEmptyArray } from 'fp-ts/NonEmptyArray'; @@ -68,29 +68,50 @@ const tryValidateInputBody = ( requestBody: IHttpOperationRequestBody, bundle: unknown, body: unknown, - mediaType: string + headers: IHttpNameValue ) => pipe( checkBodyIsProvided(requestBody, body), E.chain(() => { + const headersNormalized = caseless(headers || {}); + + const contentLength = parseInt(headersNormalized.get('content-length')) || 0; + if (contentLength === 0) { + // generously allow this content type if there isn't a body actually provided + return E.right(body); + } + + const mediaType = headersNormalized.get('content-type'); if (isMediaTypeSupportedInContents(mediaType, requestBody.contents)) { return E.right(body); } - const supportedContentTypes = (requestBody.contents || []).map(x => x.mediaType); + const specRequestBodyContents = requestBody.contents || []; + let message: string; + + if (specRequestBodyContents.length === 0) { + message = 'No supported content types, but request included a non-empty body'; + } else { + const supportedContentTypes = specRequestBodyContents.map(x => x.mediaType); + message = `Supported content types: ${supportedContentTypes.join(',')}`; + } + return E.left>([ { - message: `Supported content types: ${supportedContentTypes.join(',')}`, + message, code: 415, severity: DiagnosticSeverity.Error, }, ]); }), - E.chain(() => validateInputIfBodySpecIsProvided(body, mediaType, requestBody.contents, bundle)) + E.chain(() => { + const mediaType = caseless(headers || {}).get('content-type'); + + return validateInputIfBodySpecIsProvided(body, mediaType, requestBody.contents, bundle); + }) ); export const validateInput: ValidatorFn = ({ resource, element }) => { - const mediaType = caseless(element.headers || {}).get('content-type'); const { request } = resource; const { body } = element; @@ -101,7 +122,7 @@ export const validateInput: ValidatorFn = ({ resou e => E.right, unknown>(e), request => sequenceValidation( - request.body ? tryValidateInputBody(request.body, bundle, body, mediaType) : E.right(undefined), + request.body ? tryValidateInputBody(request.body, bundle, body, element.headers || {}) : E.right(undefined), request.headers ? validateHeaders(element.headers || {}, request.headers, bundle) : E.right(undefined), request.query ? validateQuery(element.url.query || {}, request.query, bundle) : E.right(undefined), request.path diff --git a/test-harness/specs/validate-body-params/enforce-content-type-if-body-included.oas3_1.txt b/test-harness/specs/validate-body-params/enforce-content-type-if-body-included.oas3_1.txt new file mode 100644 index 000000000..a0eed4735 --- /dev/null +++ b/test-harness/specs/validate-body-params/enforce-content-type-if-body-included.oas3_1.txt @@ -0,0 +1,26 @@ +====test==== +Given a request body when the spec hasn't specified one +then return 415 +====spec==== +openapi: '3.1.0' +paths: + /path: + post: + responses: + 200: + content: + text/plain: + example: ok + 415: + content: + text/plain: + example: no body allowed +====server==== +mock -p 4010 ${document} +====command==== +curl -i -X POST http://localhost:4010/path -H "Content-Type: text/plain" --data "empty" +====expect==== +HTTP/1.1 415 Bad Request +content-type: text/plain + +no body allowed diff --git a/test-harness/specs/validate-body-params/ignore-content-type-if-empty-body.oas3_1.txt b/test-harness/specs/validate-body-params/ignore-content-type-if-empty-body.oas3_1.txt new file mode 100644 index 000000000..310334faa --- /dev/null +++ b/test-harness/specs/validate-body-params/ignore-content-type-if-empty-body.oas3_1.txt @@ -0,0 +1,22 @@ +====test==== +Given a request declaring a content-type but has no body +then return 200 +====spec==== +openapi: '3.1.0' +paths: + /path: + post: + responses: + 200: + content: + text/plain: + example: ok +====server==== +mock -p 4010 ${document} +====command==== +curl -i -X POST http://localhost:4010/path -H "Content-Type: text/plain" +====expect==== +HTTP/1.1 200 OK +content-type: text/plain + +ok