openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
243 lines (242 loc) • 7.39 kB
JavaScript
import { j as resolveTimerTimeoutMs, y as parseStrictNonNegativeInteger } from "./number-coercion-CJQ8TR--.js";
import "./parse-finite-number-Z7n6tXLk.js";
import { i as formatErrorMessage } from "./errors-BXgSefBE.js";
import { clearTimeout, setTimeout } from "node:timers";
//#region src/infra/http-body.ts
const DEFAULT_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
const DEFAULT_WEBHOOK_BODY_TIMEOUT_MS = 3e4;
const DEFAULT_ERROR_MESSAGE = {
PAYLOAD_TOO_LARGE: "PayloadTooLarge",
REQUEST_BODY_TIMEOUT: "RequestBodyTimeout",
CONNECTION_CLOSED: "RequestBodyConnectionClosed"
};
const DEFAULT_ERROR_STATUS_CODE = {
PAYLOAD_TOO_LARGE: 413,
REQUEST_BODY_TIMEOUT: 408,
CONNECTION_CLOSED: 400
};
const DEFAULT_RESPONSE_MESSAGE = {
PAYLOAD_TOO_LARGE: "Payload too large",
REQUEST_BODY_TIMEOUT: "Request body timeout",
CONNECTION_CLOSED: "Connection closed"
};
var RequestBodyLimitError = class extends Error {
constructor(init) {
super(init.message ?? DEFAULT_ERROR_MESSAGE[init.code]);
this.name = "RequestBodyLimitError";
this.code = init.code;
this.statusCode = DEFAULT_ERROR_STATUS_CODE[init.code];
}
};
function isRequestBodyLimitError(error, code) {
if (!(error instanceof RequestBodyLimitError)) return false;
if (!code) return true;
return error.code === code;
}
function requestBodyErrorToText(code) {
return DEFAULT_RESPONSE_MESSAGE[code];
}
function parseContentLengthHeader(req) {
const header = req.headers["content-length"];
const raw = Array.isArray(header) ? header[0] : header;
if (typeof raw !== "string") return null;
const parsed = parseStrictNonNegativeInteger(raw);
if (parsed === void 0) return null;
return parsed;
}
function resolveRequestBodyLimitValues(options) {
return {
maxBytes: Number.isFinite(options.maxBytes) ? Math.max(1, Math.floor(options.maxBytes)) : 1,
timeoutMs: options.timeoutMs === void 0 ? DEFAULT_WEBHOOK_BODY_TIMEOUT_MS : resolveTimerTimeoutMs(options.timeoutMs, DEFAULT_WEBHOOK_BODY_TIMEOUT_MS)
};
}
const testApi = { resolveRequestBodyLimitValues };
function advanceRequestBodyChunk(chunk, totalBytes, maxBytes) {
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
const nextTotalBytes = totalBytes + buffer.length;
return {
buffer,
totalBytes: nextTotalBytes,
exceeded: nextTotalBytes > maxBytes
};
}
async function readRequestBodyWithLimit(req, options) {
const { maxBytes, timeoutMs } = resolveRequestBodyLimitValues(options);
const encoding = options.encoding ?? "utf-8";
const declaredLength = parseContentLengthHeader(req);
if (declaredLength !== null && declaredLength > maxBytes) {
const error = new RequestBodyLimitError({ code: "PAYLOAD_TOO_LARGE" });
if (!req.destroyed) req.destroy();
throw error;
}
return await new Promise((resolve, reject) => {
let done = false;
let ended = false;
let totalBytes = 0;
const chunks = [];
const cleanup = () => {
req.removeListener("data", onData);
req.removeListener("end", onEnd);
req.removeListener("error", onError);
req.removeListener("close", onClose);
clearTimeout(timer);
};
const finish = (cb) => {
if (done) return;
done = true;
cleanup();
cb();
};
const fail = (error) => {
finish(() => reject(error));
};
const timer = setTimeout(() => {
const error = new RequestBodyLimitError({ code: "REQUEST_BODY_TIMEOUT" });
if (!req.destroyed) req.destroy();
fail(error);
}, timeoutMs);
const onData = (chunk) => {
if (done) return;
const progress = advanceRequestBodyChunk(chunk, totalBytes, maxBytes);
totalBytes = progress.totalBytes;
if (progress.exceeded) {
const error = new RequestBodyLimitError({ code: "PAYLOAD_TOO_LARGE" });
if (!req.destroyed) req.destroy();
fail(error);
return;
}
chunks.push(progress.buffer);
};
const onEnd = () => {
ended = true;
finish(() => resolve(Buffer.concat(chunks).toString(encoding)));
};
const onError = (error) => {
if (done) return;
fail(error);
};
const onClose = () => {
if (done || ended) return;
fail(new RequestBodyLimitError({ code: "CONNECTION_CLOSED" }));
};
req.on("data", onData);
req.on("end", onEnd);
req.on("error", onError);
req.on("close", onClose);
});
}
async function readJsonBodyWithLimit(req, options) {
try {
const trimmed = (await readRequestBodyWithLimit(req, options)).trim();
if (!trimmed) {
if (options.emptyObjectOnEmpty === false) return {
ok: false,
code: "INVALID_JSON",
error: "empty payload"
};
return {
ok: true,
value: {}
};
}
try {
return {
ok: true,
value: JSON.parse(trimmed)
};
} catch (error) {
return {
ok: false,
code: "INVALID_JSON",
error: formatErrorMessage(error)
};
}
} catch (error) {
if (isRequestBodyLimitError(error)) return {
ok: false,
code: error.code,
error: requestBodyErrorToText(error.code)
};
return {
ok: false,
code: "INVALID_JSON",
error: formatErrorMessage(error)
};
}
}
function installRequestBodyLimitGuard(req, res, options) {
const { maxBytes, timeoutMs } = resolveRequestBodyLimitValues(options);
const responseFormat = options.responseFormat ?? "json";
const customText = options.responseText ?? {};
let tripped = false;
let reason = null;
let done = false;
let ended = false;
let totalBytes = 0;
const cleanup = () => {
req.removeListener("data", onData);
req.removeListener("end", onEnd);
req.removeListener("close", onClose);
req.removeListener("error", onError);
clearTimeout(timer);
};
const finish = () => {
if (done) return;
done = true;
cleanup();
};
const respond = (error) => {
const text = customText[error.code] ?? requestBodyErrorToText(error.code);
if (!res.headersSent) {
res.statusCode = error.statusCode;
if (responseFormat === "text") {
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end(text);
} else {
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.end(JSON.stringify({ error: text }));
}
}
};
const trip = (error) => {
if (tripped) return;
tripped = true;
reason = error.code;
finish();
respond(error);
if (!req.destroyed) req.destroy();
};
const onData = (chunk) => {
if (done) return;
const progress = advanceRequestBodyChunk(chunk, totalBytes, maxBytes);
totalBytes = progress.totalBytes;
if (progress.exceeded) trip(new RequestBodyLimitError({ code: "PAYLOAD_TOO_LARGE" }));
};
const onEnd = () => {
ended = true;
finish();
};
const onClose = () => {
if (done || ended) return;
finish();
};
const onError = () => {
finish();
};
const timer = setTimeout(() => {
trip(new RequestBodyLimitError({ code: "REQUEST_BODY_TIMEOUT" }));
}, timeoutMs);
req.on("data", onData);
req.on("end", onEnd);
req.on("close", onClose);
req.on("error", onError);
const declaredLength = parseContentLengthHeader(req);
if (declaredLength !== null && declaredLength > maxBytes) trip(new RequestBodyLimitError({ code: "PAYLOAD_TOO_LARGE" }));
return {
dispose: finish,
isTripped: () => tripped,
code: () => reason
};
}
//#endregion
export { isRequestBodyLimitError as a, requestBodyErrorToText as c, installRequestBodyLimitGuard as i, testApi as l, DEFAULT_WEBHOOK_MAX_BODY_BYTES as n, readJsonBodyWithLimit as o, RequestBodyLimitError as r, readRequestBodyWithLimit as s, DEFAULT_WEBHOOK_BODY_TIMEOUT_MS as t };