UNPKG

@whook/whook

Version:

Build strong and efficient REST web services.

165 lines 6.72 kB
import { YHTTPError } from 'yhttperror'; import { parse as parseContentType } from 'content-type'; // @ts-expect-error No types 🤷 import preferredCharsets from 'negotiator/lib/charset.js'; // @ts-expect-error No types 🤷 import preferredMediaType from 'negotiator/lib/encoding.js'; import { YError } from 'yerror'; import { pickFirstHeaderValue, pickAllHeaderValues } from './headers.js'; import { ensureResolvedObject } from 'ya-open-api-types'; export async function extractConsumableMediaTypes(API, operation) { if (!('requestBody' in operation && operation.requestBody)) { return []; } const requestBody = await ensureResolvedObject(API, operation.requestBody); if (!('content' in requestBody && requestBody.content)) { return []; } // Per spec contents, the `content` property should always // be present so not checking before using it // https://swagger.io/specification/#requestBodyObject return Object.keys(requestBody.content); } export async function extractProduceableMediaTypes(API, operation) { if (!('responses' in operation && operation.responses)) { return []; } const produceableMediaTypes = []; for (const response of Object.values(operation.responses)) { if (!(typeof response === 'object' && response)) { continue; } const resolvedResponse = await ensureResolvedObject(API, response); if (!('content' in resolvedResponse && resolvedResponse.content)) { continue; } produceableMediaTypes.push(...Object.keys(resolvedResponse.content || {})); } return [...new Set(produceableMediaTypes)]; } export function extractBodySpec(request, consumableMediaTypes, consumableCharsets) { const contentLengthValues = pickAllHeaderValues('content-length', request.headers); const bodySpec = { contentType: '', contentLength: contentLengthValues.length ? Number(contentLengthValues[0]) : 0, charset: 'utf-8', }; if (request.headers['content-type']) { const baseContentType = pickFirstHeaderValue('content-type', request.headers); if (typeof baseContentType === 'string') { const parsedContentType = parseContentType(baseContentType); // See https://stackoverflow.com/questions/25201083/regex-to-match-and-validate-internet-media-type if (!parsedContentType.type.match(/(application|audio|font|example|image|message|model|multipart|text|video|x-(?:[0-9A-Za-z!#$%&'*+.^_`|~-]+))\/([0-9A-Za-z!#$%&'*+.^_`|~-]+)/)) { throw new YHTTPError(400, 'E_BAD_CONTENT_TYPE', [ request.headers['content-type'], ]); } bodySpec.contentType = parsedContentType.type; if (parsedContentType.parameters && parsedContentType.parameters.charset) { if (['utf-8', 'utf8'].includes(parsedContentType.parameters.charset.toLowerCase())) { bodySpec.charset = 'utf-8'; } else { throw new YError('E_UNSUPPORTED_CHARSET', [ parsedContentType.parameters.charset, ]); } } if (parsedContentType.parameters && parsedContentType.parameters.boundary) { bodySpec.boundary = parsedContentType.parameters.boundary; } } else { throw new YError('E_EMPTY_CONTENT_TYPE'); } } checkBodyCharset(bodySpec, consumableCharsets); checkBodyMediaType(bodySpec, consumableMediaTypes); return bodySpec; } export function checkBodyCharset(bodySpec, consumableCharsets) { if (bodySpec.contentLength && bodySpec.charset && !consumableCharsets.includes(bodySpec.charset)) { throw new YHTTPError(406, 'E_UNSUPPORTED_CHARSET', [ bodySpec.charset, consumableCharsets, ]); } } export function checkBodyMediaType(bodySpec, consumableMediaTypes) { if (bodySpec.contentLength && bodySpec.contentType && !consumableMediaTypes.includes(bodySpec.contentType)) { throw new YHTTPError(415, 'E_UNSUPPORTED_MEDIA_TYPE', [ bodySpec.contentType, consumableMediaTypes, ]); } } export function extractResponseSpec(operation, request, supportedMediaTypes, supportedCharsets) { const accept = pickFirstHeaderValue('accept', request.headers) || '*'; const responseSpec = { charsets: request.headers['accept-charset'] ? preferredCharsets(pickFirstHeaderValue('accept-charset', request.headers), supportedCharsets) : supportedCharsets, contentTypes: preferredMediaType(accept.replace(/(^|,)\*\/\*($|,|;)/g, '$1*$2'), supportedMediaTypes), }; return responseSpec; } export function checkResponseMediaType(request, responseSpec, produceableMediaTypes) { if (0 === responseSpec.contentTypes.length) { throw new YHTTPError(406, 'E_UNACCEPTABLE_MEDIA_TYPE', [ request.headers.accept, produceableMediaTypes, ]); } } export function checkResponseCharset(request, responseSpec, produceableCharsets) { if (0 === responseSpec.charsets.length) { throw new YHTTPError(406, 'E_UNACCEPTABLE_CHARSET', [ request.headers['accept-charset'], responseSpec.charsets, produceableCharsets, ]); } } export async function executeHandler(definition, handler, parameters) { const responsePromise = handler(parameters, definition); if (!(responsePromise && responsePromise.then)) { throw new YHTTPError(500, 'E_NO_RESPONSE_PROMISE', [ definition.operation.operationId, definition.method, definition.path, ]); } const response = await responsePromise; if (!response) { throw new YHTTPError(500, 'E_NO_RESPONSE', [ definition.operation.operationId, definition.method, definition.path, ]); } if ('undefined' === typeof response.status) { throw new YHTTPError(500, 'E_NO_RESPONSE_STATUS', [ definition.operation.operationId, definition.method, definition.path, ]); } if ('number' !== typeof response.status) { throw new YHTTPError(500, 'E_NON_NUMERIC_STATUS', [ definition.operation.operationId, definition.method, definition.path, ]); } response.headers = response.headers || {}; return response; } //# sourceMappingURL=router.js.map