Files
pikasTech-HWLAB/internal/cloud/server-http-utils.ts
T
2026-06-21 04:14:58 +08:00

103 lines
3.2 KiB
TypeScript

import { decorateErrorPayloadForHttp, logErrorEnvelope } from "./error-envelope.ts";
const DEFAULT_BODY_LIMIT_BYTES = 1024 * 1024;
export function readBody(request, limitBytes = DEFAULT_BODY_LIMIT_BYTES) {
const limit = Number.isInteger(limitBytes) && limitBytes > 0 ? limitBytes : DEFAULT_BODY_LIMIT_BYTES;
return new Promise((resolve, reject) => {
let body = "";
request.setEncoding("utf8");
request.on("data", (chunk) => {
body += chunk;
if (Buffer.byteLength(body, "utf8") > limit) {
request.destroy(new Error(`request body exceeds ${limit} bytes`));
}
});
request.on("end", () => resolve(body));
request.on("error", reject);
});
}
export function sendJson(response, statusCode, body) {
if (response.headersSent || response.writableEnded) return false;
const payload = decorateErrorPayloadForHttp(body, { response, statusCode });
logErrorEnvelope(payload, { statusCode });
const headers: Record<string, string> = {
"content-type": "application/json; charset=utf-8",
"cache-control": "no-store"
};
const retryAfterSeconds = retryAfterHeaderSeconds(payload);
if (retryAfterSeconds !== null) headers["retry-after"] = String(retryAfterSeconds);
response.writeHead(statusCode, headers);
response.end(JSON.stringify(payload) + "\n");
return true;
}
function retryAfterHeaderSeconds(payload) {
const retryAfterMs = Number(payload?.error?.retryAfterMs ?? payload?.retryAfterMs);
if (!Number.isFinite(retryAfterMs) || retryAfterMs <= 0) return null;
return Math.max(1, Math.ceil(retryAfterMs / 1000));
}
export function sendRedirect(response, location, body = {}) {
response.writeHead(302, {
"content-type": "application/json; charset=utf-8",
"cache-control": "no-store",
location
});
response.end(JSON.stringify({ redirectTo: location, ...body }) + "\n");
}
export function getHeader(request, name) {
const value = request.headers[name.toLowerCase()];
if (Array.isArray(value)) return value[0];
return value;
}
export function firstHeaderValue(request, name) {
return getHeader(request, name);
}
export function truthyFlag(value) {
return /^(?:1|true|yes|on|enabled)$/iu.test(String(value ?? "").trim());
}
export function safeTraceId(value) {
const text = String(value ?? "").trim();
return /^trc_[A-Za-z0-9_.:-]+$/u.test(text) ? text : null;
}
export function safeSessionId(value) {
const text = String(value ?? "").trim();
return /^ses_[A-Za-z0-9_.:-]+$/u.test(text) ? text : null;
}
export function safeConversationId(value) {
const text = String(value ?? "").trim();
return /^cnv_[A-Za-z0-9_.:-]+$/u.test(text) ? text : null;
}
export function safeOpaqueId(value) {
const text = String(value ?? "").trim();
return /^[A-Za-z0-9_.:-]{3,180}$/u.test(text) ? text : null;
}
export function parsePositiveInteger(value, fallback) {
const parsed = Number.parseInt(value ?? "", 10);
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
}
export function positiveInteger(value, fallback) {
const parsed = Number.parseInt(value ?? "", 10);
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
}
export function text(value) {
if (value === null || value === undefined) return "";
return String(value);
}