diff --git a/packages/http-server/src/server.ts b/packages/http-server/src/server.ts index bbb9fb0f8..f414f0e32 100644 --- a/packages/http-server/src/server.ts +++ b/packages/http-server/src/server.ts @@ -42,9 +42,10 @@ function parseRequestBody(request: IncomingMessage) { if ( // If the body size is null, it means the body itself is null so the promise can resolve with a null value request.headers['content-length'] === '0' || - (request.headers['content-type'] === undefined && - request.headers['transfer-encoding'] === undefined && - request.headers['content-length'] === undefined) + // Per HTTP 1.1 - these 2 headers are the valid way to indicate that a body exists: + // > The presence of a message body in a request is signaled by a Content-Length or Transfer-Encoding header field. + // https://httpwg.org/specs/rfc9112.html#message.body + (request.headers['transfer-encoding'] === undefined && request.headers['content-length'] === undefined) ) { return Promise.resolve(null); } diff --git a/packages/http/src/forwarder/errors.ts b/packages/http/src/forwarder/errors.ts index f331ccc5b..6478bdad5 100644 --- a/packages/http/src/forwarder/errors.ts +++ b/packages/http/src/forwarder/errors.ts @@ -5,3 +5,10 @@ export const UPSTREAM_NOT_IMPLEMENTED: Omit = { title: 'The server does not support the functionality required to fulfill the request', status: 501, }; + +export const PROXY_UNSUPPORTED_REQUEST_BODY: Omit = { + type: 'PROXY_UNSUPPORTED_REQUEST_BODY', + title: + 'The Prism proxy does not support sending a GET/HEAD request with a message body to an upstream server. See: https://github.com/stoplightio/prism/issues/2259', + status: 501, +}; diff --git a/packages/http/src/forwarder/index.ts b/packages/http/src/forwarder/index.ts index d541fcaba..724eebf31 100644 --- a/packages/http/src/forwarder/index.ts +++ b/packages/http/src/forwarder/index.ts @@ -16,7 +16,7 @@ import { parseResponse } from '../utils/parseResponse'; import { hopByHopHeaders } from './resources'; import { createUnauthorisedResponse, createUnprocessableEntityResponse } from '../mocker'; import { ProblemJsonError } from '../types'; -import { UPSTREAM_NOT_IMPLEMENTED } from './errors'; +import { PROXY_UNSUPPORTED_REQUEST_BODY, UPSTREAM_NOT_IMPLEMENTED } from './errors'; import * as createHttpProxyAgent from 'http-proxy-agent'; import * as createHttpsProxyAgent from 'https-proxy-agent'; import { toURLSearchParams } from '../utils/url'; @@ -44,7 +44,7 @@ const forward: IPrismComponents serializeBody(input.body)), + TE.chainEitherK(() => serializeBodyForFetch(input, logger)), TE.chain(body => TE.tryCatch(async () => { const partialUrl = new URL(baseUrl); @@ -98,13 +98,23 @@ const forward: IPrismComponents { + const upperMethod = input.method.toUpperCase(); + if (['GET', 'HEAD'].includes(upperMethod) && ![null, undefined].includes(input.body as any)) { + logger.warn(`Upstream ${upperMethod} call to ${input.url.path} has request body`); + return E.left(ProblemJsonError.fromTemplate(PROXY_UNSUPPORTED_REQUEST_BODY)); + } + + return serializeBody(input.body); +} + export function serializeBody(body: unknown): E.Either { if (typeof body === 'string') { return E.right(body); } - + if (body) return pipe(J.stringify(body), E.mapLeft(E.toError)); - + return E.right(undefined); } @@ -113,13 +123,13 @@ function logForwardRequest({ logger, url, request }: { logger: Logger; url: stri logger.info(`${prefix}Forwarding "${request.method}" request to ${url}...`); logRequest({ logger, prefix, ...pick(request, 'body', 'headers') }); } - + function forwardResponseLogger(logger: Logger) { return (response: Response) => { const prefix = chalk.grey('< '); - + logger.info(`${prefix}Received forward response`); - + const { status: statusCode } = response; logResponse({ @@ -127,7 +137,7 @@ function forwardResponseLogger(logger: Logger) { statusCode, ...pick(response, 'body', 'headers'), prefix, - }); + }); return response; }; diff --git a/test-harness/specs/proxy/proxy.get-null-body.txt b/test-harness/specs/proxy/proxy.get-null-body.txt new file mode 100644 index 000000000..92c4885a2 --- /dev/null +++ b/test-harness/specs/proxy/proxy.get-null-body.txt @@ -0,0 +1,18 @@ +====test==== +Making a GET request with a Content-Type header through the proxy server ignores null body +====spec==== +swagger: "2.0" +paths: + /status/204: + get: + produces: + - text/plain + responses: + 204: + description: No Content +====server==== +proxy -p 4010 ${document} http://httpbin.org +====command==== +curl -i http://localhost:4010/status/204 -X GET --header 'Content-Type: application/json' +====expect==== +HTTP/1.1 204 No Content diff --git a/test-harness/specs/proxy/proxy.get-with-body.txt b/test-harness/specs/proxy/proxy.get-with-body.txt new file mode 100644 index 000000000..bdc90c7b4 --- /dev/null +++ b/test-harness/specs/proxy/proxy.get-with-body.txt @@ -0,0 +1,20 @@ +====test==== +Making a GET request with request body through the proxy server rejects with 501 +====spec==== +swagger: "2.0" +paths: + /status/204: + get: + produces: + - text/plain + responses: + 204: + description: No Content +====server==== +proxy -p 4010 ${document} http://httpbin.org +====command==== +curl -i http://localhost:4010/status/204 -X GET --header 'Content-Type: application/json' --data '{}' +====expect==== +HTTP/1.1 501 Not Implemented + +{"type":"https://stoplight.io/prism/errors#PROXY_UNSUPPORTED_REQUEST_BODY","title":"The Prism proxy does not support sending a GET/HEAD request with a message body to an upstream server. See: https://github.com/stoplightio/prism/issues/2259","status":501,"detail":""} \ No newline at end of file