@whook/whook
Version:
Build strong and efficient REST web services.
135 lines • 4.71 kB
JavaScript
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