milliparsec
Version:
tiniest body parser in the universe
125 lines (124 loc) • 5.07 kB
JavaScript
import { Buffer, File } from 'node:buffer';
export const hasBody = (method) => ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method);
const defaultPayloadLimit = 102400; // 100KB
const defaultErrorFn = (payloadLimit) => new Error(`Payload too large. Limit: ${payloadLimit} bytes`);
// Main function
export const p = (fn, payloadLimit = defaultPayloadLimit, payloadLimitErrorFn = defaultErrorFn) => async (req, _res, next) => {
try {
const body = [];
for await (const chunk of req) {
const totalSize = body.reduce((total, buffer) => total + buffer.byteLength, 0);
if (totalSize > payloadLimit)
throw payloadLimitErrorFn(payloadLimit);
body.push(chunk);
}
return fn(Buffer.concat(body));
}
catch (e) {
next === null || next === void 0 ? void 0 : next(e);
}
};
/**
* Parse payload with a custom function
* @param fn
*/
const custom = (fn) => async (req, _res, next) => {
if (hasBody(req.method))
req.body = await p(fn)(req, _res, next);
next === null || next === void 0 ? void 0 : next();
};
/**
* Parse JSON payload
* @param options
*/
const json = ({ payloadLimit, payloadLimitErrorFn } = {}) => async (req, res, next) => {
if (hasBody(req.method)) {
req.body = await p((x) => {
const str = td.decode(x);
return str ? JSON.parse(str) : {};
}, payloadLimit, payloadLimitErrorFn)(req, res, next);
}
next === null || next === void 0 ? void 0 : next();
};
/**
* Parse raw payload
* @param options
*/
const raw = ({ payloadLimit, payloadLimitErrorFn } = {}) => async (req, _res, next) => {
if (hasBody(req.method)) {
req.body = await p((x) => x, payloadLimit, payloadLimitErrorFn)(req, _res, next);
}
next === null || next === void 0 ? void 0 : next();
};
const td = new TextDecoder();
/**
* Stringify request payload
* @param param0
* @returns
*/
const text = ({ payloadLimit, payloadLimitErrorFn } = {}) => async (req, _res, next) => {
if (hasBody(req.method)) {
req.body = await p((x) => td.decode(x), payloadLimit, payloadLimitErrorFn)(req, _res, next);
}
next === null || next === void 0 ? void 0 : next();
};
/**
* Parse urlencoded payload
* @param options
*/
const urlencoded = ({ payloadLimit, payloadLimitErrorFn } = {}) => async (req, _res, next) => {
if (hasBody(req.method)) {
req.body = await p((x) => Object.fromEntries(new URLSearchParams(x.toString()).entries()), payloadLimit, payloadLimitErrorFn)(req, _res, next);
}
next === null || next === void 0 ? void 0 : next();
};
const getBoundary = (contentType) => {
const match = /boundary=(.+);?/.exec(contentType);
return match ? `--${match[1]}` : null;
};
const defaultFileSizeLimitErrorFn = (limit) => new Error(`File too large. Limit: ${limit} bytes`);
const defaultFileSizeLimit = 200 * 1024 * 1024;
const parseMultipart = (body, boundary, { fileCountLimit, fileSizeLimit = defaultFileSizeLimit, fileSizeLimitErrorFn = defaultFileSizeLimitErrorFn }) => {
const parts = body.split(new RegExp(`${boundary}(--)?`)).filter((part) => !!part && /content-disposition/i.test(part));
const parsedBody = {};
if (fileCountLimit && parts.length > fileCountLimit)
throw new Error(`Too many files. Limit: ${fileCountLimit}`);
// biome-ignore lint/complexity/noForEach: for...of fails
parts.forEach((part) => {
const [headers, ...lines] = part.split('\r\n').filter((part) => !!part);
const data = lines.join('\r\n').trim();
if (data.length > fileSizeLimit)
throw fileSizeLimitErrorFn(fileSizeLimit);
// Extract the name and filename from the headers
const name = /name="(.+?)"/.exec(headers)[1];
const filename = /filename="(.+?)"/.exec(headers);
if (filename) {
const contentTypeMatch = /Content-Type: (.+)/i.exec(data);
const fileContent = data.slice(contentTypeMatch[0].length + 2);
const file = new File([fileContent], filename[1], { type: contentTypeMatch[1] });
parsedBody[name] = parsedBody[name] ? [...parsedBody[name], file] : [file];
return;
}
parsedBody[name] = parsedBody[name] ? [...parsedBody[name], data] : [data];
return;
});
return parsedBody;
};
/**
* Parse multipart form data (supports files as well)
*
* Does not restrict total payload size by default.
* @param options
*/
const multipart = ({ payloadLimit = Number.POSITIVE_INFINITY, payloadLimitErrorFn, ...opts } = {}) => async (req, res, next) => {
if (hasBody(req.method)) {
req.body = await p((x) => {
const boundary = getBoundary(req.headers['content-type']);
if (boundary)
return parseMultipart(td.decode(x), boundary, opts);
return {};
}, payloadLimit, payloadLimitErrorFn)(req, res, next);
}
next === null || next === void 0 ? void 0 : next();
};
export { custom, json, raw, text, urlencoded, multipart };