import type { IncomingMessage, ServerResponse } from "node:http" ;
import {
logRejectedLargePayload,
parseContentLengthHeader,
} from "../logging/diagnostic-payload.js" ;
import type { GatewayAuthResult } from "./auth.js" ;
import { readJsonBody } from "./hooks.js" ;
/**
* Apply baseline security headers that are safe for all response types ( API JSON ,
* HTML pages , static assets , SSE streams ) . Headers that restrict framing or set a
* Content - Security - Policy are intentionally omitted here because some handlers
* ( canvas host , A2UI ) serve content that may be loaded inside frames .
*/
export function setDefaultSecurityHeaders(
res: ServerResponse,
opts?: { strictTransportSecurity?: string },
) {
res.setHeader("X-Content-Type-Options" , "nosniff" );
res.setHeader("Referrer-Policy" , "no-referrer" );
res.setHeader("Permissions-Policy" , "camera=(), microphone=(self), geolocation=()" );
const strictTransportSecurity = opts?.strictTransportSecurity;
if (typeof strictTransportSecurity === "string" && strictTransportSecurity.length > 0 ) {
res.setHeader("Strict-Transport-Security" , strictTransportSecurity);
}
}
export function sendJson(res: ServerResponse, status: number, body: unknown) {
res.statusCode = status;
res.setHeader("Content-Type" , "application/json; charset=utf-8" );
res.end(JSON.stringify(body));
}
export function sendText(res: ServerResponse, status: number, body: string) {
res.statusCode = status;
res.setHeader("Content-Type" , "text/plain; charset=utf-8" );
res.end(body);
}
export function sendMethodNotAllowed(res: ServerResponse, allow = "POST" ) {
res.setHeader("Allow" , allow);
sendText(res, 405 , "Method Not Allowed" );
}
export function sendUnauthorized(res: ServerResponse) {
sendJson(res, 401 , {
error: { message: "Unauthorized" , type: "unauthorized" },
});
}
export function sendRateLimited(res: ServerResponse, retryAfterMs?: number) {
if (retryAfterMs && retryAfterMs > 0 ) {
res.setHeader("Retry-After" , String(Math.ceil(retryAfterMs / 1000 )));
}
sendJson(res, 429 , {
error: {
message: "Too many failed authentication attempts. Please try again later." ,
type: "rate_limited" ,
},
});
}
export function sendGatewayAuthFailure(res: ServerResponse, authResult: GatewayAuthResult) {
if (authResult.rateLimited) {
sendRateLimited(res, authResult.retryAfterMs);
return ;
}
sendUnauthorized(res);
}
export function sendInvalidRequest(res: ServerResponse, message: string) {
sendJson(res, 400 , {
error: { message, type: "invalid_request_error" },
});
}
export async function readJsonBodyOrError(
req: IncomingMessage,
res: ServerResponse,
maxBytes: number,
): Promise<unknown> {
const body = await readJsonBody(req, maxBytes);
if (!body.ok) {
if (body.error === "payload too large" ) {
const contentLength = parseContentLengthHeader(req.headers?.["content-length" ]);
logRejectedLargePayload({
surface: "gateway.http.json" ,
limitBytes: maxBytes,
reason: "json_body_limit" ,
...(contentLength !== undefined ? { bytes: contentLength } : {}),
});
sendJson(res, 413 , {
error: { message: "Payload too large" , type: "invalid_request_error" },
});
return undefined;
}
if (body.error === "request body timeout" ) {
sendJson(res, 408 , {
error: { message: "Request body timeout" , type: "invalid_request_error" },
});
return undefined;
}
sendInvalidRequest(res, body.error);
return undefined;
}
return body.value;
}
export function writeDone(res: ServerResponse) {
res.write("data: [DONE]\n\n" );
}
export function setSseHeaders(res: ServerResponse) {
res.statusCode = 200 ;
res.setHeader("Content-Type" , "text/event-stream; charset=utf-8" );
res.setHeader("Cache-Control" , "no-cache" );
res.setHeader("Connection" , "keep-alive" );
res.flushHeaders?.();
}
export function watchClientDisconnect(
req: IncomingMessage,
res: ServerResponse,
abortController: AbortController,
onDisconnect?: () => void ,
) {
const sockets = Array.from(
new Set(
[req.socket, res.socket].filter(
(socket): socket is NonNullable<typeof socket> => socket !== null ,
),
),
);
if (sockets.length === 0 ) {
return () => {};
}
const handleClose = () => {
onDisconnect?.();
if (!abortController.signal.aborted) {
abortController.abort();
}
};
for (const socket of sockets) {
socket.on("close" , handleClose);
}
return () => {
for (const socket of sockets) {
socket.off("close" , handleClose);
}
};
}
Messung V0.5 in Prozent C=99 H=97 G=97
¤ Dauer der Verarbeitung: 0.17 Sekunden
(vorverarbeitet am 2026-06-10)
¤
*© Formatika GbR, Deutschland