/** Small request/response helpers shared by the router. */
export class HttpError extends Error {
constructor(status, message) {
super(message);
this.status = status;
}
}
export const bad = (msg) => { throw new HttpError(400, msg); };
export const unauthorized = (msg = 'You need to sign in to do that.') => { throw new HttpError(401, msg); };
export const forbidden = (msg = 'You are not allowed to do that.') => { throw new HttpError(403, msg); };
export const notFound = (msg = 'Not found.') => { throw new HttpError(404, msg); };
export function sendJson(res, status, payload, headers = {}) {
const body = Buffer.from(JSON.stringify(payload));
res.writeHead(status, {
'Content-Type': 'application/json; charset=utf-8',
'Content-Length': body.length,
'Cache-Control': 'no-store',
...headers,
});
res.end(body);
}
export async function readBody(req, limit = 1024 * 1024) {
const chunks = [];
let size = 0;
for await (const chunk of req) {
size += chunk.length;
if (size > limit) throw new HttpError(413, 'Request body is too large.');
chunks.push(chunk);
}
return Buffer.concat(chunks);
}
export async function readJson(req) {
const raw = await readBody(req);
if (!raw.length) return {};
try {
const parsed = JSON.parse(raw.toString('utf8'));
if (parsed === null || typeof parsed !== 'object') bad('Expected a JSON object.');
return parsed;
} catch (err) {
if (err instanceof HttpError) throw err;
throw new HttpError(400, 'Request body was not valid JSON.');
}
}
/** Trim + require a string field, with a max length. */
export function field(obj, name, { max = 5000, min = 1, label = name } = {}) {
const value = typeof obj[name] === 'string' ? obj[name].trim() : '';
if (value.length < min) bad(`${label} is required.`);
if (value.length > max) bad(`${label} must be ${max} characters or fewer.`);
return value;
}
export function intParam(value, fallback, { min = 0, max = Number.MAX_SAFE_INTEGER } = {}) {
const n = Number.parseInt(value, 10);
if (!Number.isFinite(n)) return fallback;
return Math.min(max, Math.max(min, n));
}