@whook/whook
Version:
Build strong and efficient REST web services.
114 lines • 4.28 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';
/* 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 Decoder = DECODERS?.[bodySpec.charset];
if (!Decoder) {
return Promise.reject(new YHTTPError(500, 'E_DECODER_LACK', bodySpec.charset));
}
inputStream.on('error', (err) => {
reject(YHTTPError.wrap(err, 400, 'E_REQUEST_FAILURE'));
});
inputStream.pipe(new Decoder()).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, 400, 'E_BAD_BODY', body.toString());
}
}
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 Encoder = ENCODERS?.['utf-8'];
if (!Encoder) {
throw new YError('E_ENCODER_LACK', 'utf-8');
}
const stream = new Encoder();
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