UNPKG

@whook/whook

Version:

Build strong and efficient REST web services.

135 lines 4.71 kB
import { YHTTPError } from 'yhttperror'; import FirstChunkStream from 'first-chunk-stream'; import { YError } from 'yerror'; import { ensureResolvedObject } from 'ya-open-api-types'; import { pickFirstHeaderValue } from './headers.js'; import { Readable } from 'node:stream'; export const DEFAULT_REQUEST_BODY_SPEC = { contentType: 'text/plain', contentLength: 0, charset: 'utf-8', }; export const DEFAULT_RESPONSE_BODY_SPEC = { contentTypes: ['text/plain'], charsets: ['utf-8'], }; /* Architecture Note #2.11.3: Request body According to the OpenAPI specification there are two kinds of requests: - **validated contents:** it implies to buffer their content and parse them to finally validate it. In that case, we provide it as a plain JS object to the routes handlers. - **streamable contents:** often used for large files, those contents must be parsed and validated into the handler itself. */ export async function getBody({ DECODERS, PARSERS, API, bufferLimit, }, operation, inputStream, bodySpec) { const bodyIsEmpty = !(bodySpec.contentType && bodySpec.contentLength); const requestBody = 'requestBody' in operation && operation.requestBody ? await ensureResolvedObject(API, operation.requestBody) : undefined; const schemaObject = (await ensureResolvedObject(API, requestBody && requestBody.content && requestBody.content[bodySpec.contentType] && requestBody.content[bodySpec.contentType].schema && requestBody.content[bodySpec.contentType] .schema)); const bodyIsBinary = typeof schemaObject === 'object' && schemaObject !== null && 'type' in schemaObject && schemaObject.type === 'string' && 'format' in schemaObject && schemaObject.format === 'binary'; if (bodyIsEmpty) { return; } if (bodyIsBinary) { return inputStream; } if (!PARSERS?.[bodySpec.contentType]) { return Promise.reject(new YHTTPError(500, 'E_PARSER_LACK', [bodySpec.contentType])); } if (bodySpec.contentLength > bufferLimit) { throw new YHTTPError(400, 'E_REQUEST_CONTENT_TOO_LARGE', [ bodySpec.contentLength, bufferLimit, ]); } const body = await new Promise((resolve, reject) => { const createDecoder = DECODERS?.[bodySpec.charset]; if (!createDecoder) { return Promise.reject(new YHTTPError(500, 'E_DECODER_LACK', [bodySpec.charset])); } inputStream.on('error', (err) => { reject(YHTTPError.wrap(err, 'E_REQUEST_FAILURE', undefined, 400)); }); inputStream.pipe(createDecoder()).pipe(new FirstChunkStream({ chunkSize: bufferLimit + 1, }, async (chunk) => { if (bufferLimit >= chunk.length) { resolve(Buffer.from(chunk)); return chunk; } reject(new YHTTPError(400, 'E_REQUEST_CONTENT_TOO_LARGE', [ chunk.length, bufferLimit, ])); return FirstChunkStream.stop; })); }); if (body.length !== bodySpec.contentLength) { throw new YHTTPError(400, 'E_BAD_BODY_LENGTH', [ body.length, bodySpec.contentLength, ]); } try { return await new Promise((resolve, reject) => { try { resolve(PARSERS[bodySpec.contentType](body.toString(), bodySpec)); } catch (err) { reject(err); } }); } catch (err) { throw YHTTPError.wrap(err, 'E_BAD_BODY', [body.toString()], 400); } } export async function sendBody({ ENCODERS, STRINGIFIERS, }, response) { if (!response.body) { return response; } if (response.body instanceof Readable) { return response; } const responseContentType = pickFirstHeaderValue('content-type', response.headers || {}) || 'text/plain'; if (!STRINGIFIERS?.[responseContentType]) { throw new YError('E_STRINGIFYER_LACK', [ response.headers?.['content-type'], response, ]); } const createEncoder = ENCODERS?.['utf-8']; if (!createEncoder) { throw new YError('E_ENCODER_LACK', ['utf-8']); } const stream = createEncoder(); const content = STRINGIFIERS[responseContentType](response.body); stream.write(content); stream.end(); return { ...response, headers: { 'content-type': `${response.headers?.['content-type']}; charset=utf-8`, ...response.headers, }, body: stream, }; } //# sourceMappingURL=body.js.map