UNPKG

milliparsec

Version:

tiniest body parser in the universe

154 lines (153 loc) 5.93 kB
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 };