milliparsec
Version:
tiniest body parser in the universe
154 lines (153 loc) • 5.93 kB
JavaScript
import { Buffer, File } from 'node:buffer';
import { checkType, hasBody } from "./utils.js";
export * from "./types.js";
const defaultPayloadLimit = 102400; // 100KiB
const defaultErrorFn = (payloadLimit) => new Error(`Payload too large. Limit: ${payloadLimit} bytes`);
// Main function
export const p = (fn, payloadLimit = defaultPayloadLimit, payloadLimitErrorFn = defaultErrorFn) => (req, _res, next) => new Promise((resolve) => {
const body = [];
let totalSize = 0;
req.on('data', (chunk) => {
totalSize += chunk.byteLength;
if (totalSize > payloadLimit) {
req.removeAllListeners();
next === null || next === void 0 ? void 0 : next(payloadLimitErrorFn(payloadLimit));
resolve(undefined);
}
else {
body.push(chunk);
}
});
req.on('end', () => {
try {
resolve(fn(body.length === 1 ? body[0] : Buffer.concat(body), req));
}
catch (e) {
next === null || next === void 0 ? void 0 : next(e);
resolve(undefined);
}
});
req.on('error', (err) => {
next === null || next === void 0 ? void 0 : next(err);
resolve(undefined);
});
});
/**
* Parse payload with a custom function
* @param fn
*/
const custom = (fn, type) => {
const parse = p(fn);
return async (req, _res, next) => {
if (hasBody(req.method) && checkType(req, type))
req.body = await parse(req, _res, next);
next === null || next === void 0 ? void 0 : next();
};
};
/**
* Parse JSON payload
* @param options
*/
const json = ({ payloadLimit, payloadLimitErrorFn, type, reviver } = {}) => {
const parse = p((x) => (x.length === 0 ? {} : JSON.parse(x.toString(), reviver)), payloadLimit, payloadLimitErrorFn);
return async (req, res, next) => {
if (hasBody(req.method) && checkType(req, type)) {
req.body = await parse(req, res, next);
}
next === null || next === void 0 ? void 0 : next();
};
};
/**
* Parse raw payload
* @param options
*/
const raw = ({ payloadLimit, payloadLimitErrorFn, type } = {}) => {
const parse = p((x) => x, payloadLimit, payloadLimitErrorFn);
return async (req, _res, next) => {
if (hasBody(req.method) && checkType(req, type)) {
req.body = await parse(req, _res, next);
}
next === null || next === void 0 ? void 0 : next();
};
};
/**
* Stringify request payload
* @param param0
* @returns
*/
const text = ({ payloadLimit, payloadLimitErrorFn, type } = {}) => {
const parse = p((x) => x.toString(), payloadLimit, payloadLimitErrorFn);
return async (req, _res, next) => {
if (hasBody(req.method) && checkType(req, type)) {
req.body = await parse(req, _res, next);
}
next === null || next === void 0 ? void 0 : next();
};
};
const td = new TextDecoder();
/**
* Parse urlencoded payload
* @param options
*/
const urlencoded = ({ payloadLimit, payloadLimitErrorFn, type } = {}) => {
const parse = p((x) => Object.fromEntries(new URLSearchParams(x.toString()).entries()), payloadLimit, payloadLimitErrorFn);
return async (req, _res, next) => {
if (hasBody(req.method) && checkType(req, type)) {
req.body = await parse(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; // 200MiB
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}`);
parts.forEach((part) => {
var _a, _b;
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] });
((_a = parsedBody[name]) !== null && _a !== void 0 ? _a : (parsedBody[name] = [])).push(file);
return;
}
;
((_b = parsedBody[name]) !== null && _b !== void 0 ? _b : (parsedBody[name] = [])).push(data);
});
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, type, ...opts } = {}) => {
const parse = p((x, req) => {
const boundary = getBoundary(req.headers['content-type']);
if (boundary)
return parseMultipart(td.decode(x), boundary, opts);
return {};
}, payloadLimit, payloadLimitErrorFn);
return async (req, res, next) => {
if (hasBody(req.method) && checkType(req, type)) {
req.body = await parse(req, res, next);
}
next === null || next === void 0 ? void 0 : next();
};
};
export { custom, json, raw, text, urlencoded, multipart };