UNPKG

milliparsec

Version:

tiniest body parser in the universe

125 lines (124 loc) 5.07 kB
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 };