Spaces:
Runtime error
Runtime error
| /** | |
| * Body Size Guard — E-1 Critical Fix | |
| * | |
| * Middleware helper that rejects oversized request bodies | |
| * before they are parsed, preventing OOM from malicious payloads. | |
| * | |
| * Usage: | |
| * import { checkBodySize, MAX_BODY_BYTES } from "@/shared/middleware/bodySizeGuard"; | |
| * | |
| * const rejection = checkBodySize(request); | |
| * if (rejection) return rejection; | |
| * | |
| * @module shared/middleware/bodySizeGuard | |
| */ | |
| import { | |
| normalizeRequestBodyLimitMb, | |
| parseRequestBodyLimitBytes, | |
| requestBodyLimitBytesToMb, | |
| requestBodyLimitMbToBytes, | |
| } from "../constants/bodySize"; | |
| /** Larger limit for backup/import routes: 100 MB */ | |
| export const MAX_BODY_BYTES_IMPORT = 100 * 1024 * 1024; | |
| /** Larger limit for audio transcription uploads: 100 MB */ | |
| export const MAX_BODY_BYTES_AUDIO = 100 * 1024 * 1024; | |
| /** Larger limit for file uploads: 500 MB */ | |
| export const MAX_BODY_BYTES_FILE = 500 * 1024 * 1024; | |
| /** Configured limit — reads from env or falls back to 10 MB */ | |
| export const MAX_BODY_BYTES = parseRequestBodyLimitBytes(process.env.MAX_BODY_SIZE_BYTES); | |
| type BodySizeRule = { prefix: string; limit: number }; | |
| const ROUTE_LIMITS: BodySizeRule[] = [ | |
| { prefix: "/api/db-backups/import", limit: MAX_BODY_BYTES_IMPORT }, | |
| { prefix: "/api/v1/audio/transcriptions", limit: MAX_BODY_BYTES_AUDIO }, | |
| { prefix: "/api/v1/files", limit: MAX_BODY_BYTES_FILE }, | |
| ]; | |
| export function getDefaultRequestBodyLimitMb(): number { | |
| return requestBodyLimitBytesToMb(MAX_BODY_BYTES); | |
| } | |
| export function getConfiguredBodySizeLimitBytes(settings?: Record<string, unknown>): number { | |
| const configuredMb = normalizeRequestBodyLimitMb(settings?.maxBodySizeMb); | |
| return configuredMb === null ? MAX_BODY_BYTES : requestBodyLimitMbToBytes(configuredMb); | |
| } | |
| /** | |
| * Resolve the body size limit for a request path. | |
| */ | |
| export function getBodySizeLimit(pathname: string, settings?: Record<string, unknown>): number { | |
| const configuredLimit = getConfiguredBodySizeLimitBytes(settings); | |
| const customRule = ROUTE_LIMITS.find((rule) => pathname.startsWith(rule.prefix)); | |
| return customRule ? Math.max(customRule.limit, configuredLimit) : configuredLimit; | |
| } | |
| /** | |
| * Check Content-Length header against the configured limit. | |
| * Returns a 413 Response if the body is too large, or null if OK. | |
| */ | |
| export function checkBodySize(request: Request, limit: number = MAX_BODY_BYTES): Response | null { | |
| const contentLength = request.headers.get("content-length"); | |
| if (contentLength) { | |
| const bytes = Number.parseInt(contentLength, 10); | |
| if (!Number.isNaN(bytes) && bytes > limit) { | |
| return new Response( | |
| JSON.stringify({ | |
| error: { | |
| message: `Request body too large. Maximum allowed: ${formatBytes(limit)}`, | |
| type: "payload_too_large", | |
| code: "PAYLOAD_TOO_LARGE", | |
| }, | |
| }), | |
| { | |
| status: 413, | |
| headers: { | |
| "Content-Type": "application/json", | |
| }, | |
| } | |
| ); | |
| } | |
| } | |
| return null; | |
| } | |
| /** Format bytes as human-readable string */ | |
| function formatBytes(bytes: number): string { | |
| if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(0)} MB`; | |
| if (bytes >= 1024) return `${(bytes / 1024).toFixed(0)} KB`; | |
| return `${bytes} bytes`; | |
| } | |