feat: make forwarded upstream response-header budget configurable (#9243) (#9492)

Validated in local merge-train (diegosouzapw batch)
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-08-06 10:39:15 -03:00
committed by GitHub
parent 8fdb67f1d3
commit 5ea43c7a9d
6 changed files with 51 additions and 1 deletions

View File

@@ -353,6 +353,7 @@ ALLOW_API_KEY_REVEAL=false
# instead of growing an unbounded string until the V8 heap is exhausted.
# Used by: open-sse/handlers/chatCore/nonStreamingResponseBody.ts
# Default: 67108864 (64 MB)
# OMNIROUTE_FORWARDING_HEADER_BUDGET_BYTES=768
# OMNIROUTE_MAX_NONSTREAMING_RESPONSE_BYTES=67108864
# CORS configuration — controls which cross-origin browser clients can call the API.

View File

@@ -0,0 +1 @@
- feat: make forwarded upstream response-header budget configurable via env var (#9243)

View File

@@ -196,6 +196,7 @@ OmniRoute uses **SQLite** (via `better-sqlite3`) for all persistence. These vari
| `OMNIROUTE_CHAT_HEAVY_ESTIMATED_TOKENS` | `32000` | `src/shared/middleware/chatBodyAdmission.ts` | Conservative string-size token estimate that classifies a request as heavyweight; this is an admission-cost proxy, not provider billing tokenization. |
| `OMNIROUTE_CHAT_HARD_MAX_MESSAGES` | `800` | `src/shared/middleware/chatBodyAdmission.ts` | Hard chat history cap. Requests above it receive structured compact-required `413` before compression, translation, or provider dispatch. |
| `OMNIROUTE_MAX_NONSTREAMING_RESPONSE_BYTES` | `67108864` (64 MB) | `open-sse/handlers/chatCore/nonStreamingResponseBody.ts` | Hard cap for a non-streaming upstream response buffered fully into memory. Past this the upstream reader is cancelled and the request fails fast instead of growing an unbounded string until the heap is exhausted. |
| `OMNIROUTE_FORWARDING_HEADER_BUDGET_BYTES` | `768` | `open-sse/handlers/chatCore/responseHeaders.ts` | Max wire bytes forwarded from upstream response headers. When the budget is exceeded, lower-priority headers (e.g., custom `x-codex-*`, `x-oai-request-id`) are dropped to stay within common reverse-proxy header limits. Set higher to forward more upstream metadata at the cost of larger response header size. |
| `CORS_ORIGIN` | _(unset)_ | `src/server/cors/origins.ts` | Legacy single-origin CORS allowlist. Prefer `CORS_ALLOWED_ORIGINS` for new deployments. CORS is only for cross-origin browser API clients; authenticated dashboard writes use same-origin requests plus session-bound CSRF protection instead. |
| `CORS_ALLOWED_ORIGINS` | _(unset)_ | `src/server/cors/origins.ts` | Comma-separated CORS allowlist. No wildcard is sent unless `CORS_ALLOW_ALL=true` is explicitly configured. |
| `CORS_ALLOW_ALL` | `false` | `src/server/cors/origins.ts` | Development-only escape hatch to echo any browser `Origin`. Do not enable on shared or production deployments. |

View File

@@ -30,12 +30,27 @@ const STREAMING_RESPONSE_HEADER_DENYLIST = new Set([
"x-accel-buffering",
]);
const DEFAULT_FORWARDED_HEADER_BUDGET_BYTES = 768;
/**
* Resolve the forwarded upstream response-header budget from an optional string value
* (typically `process.env.OMNIROUTE_FORWARDING_HEADER_BUDGET_BYTES`). Returns the
* default of 768 when the input is unset, empty, or non-positive.
* Extracted as a pure function so unit tests can pass values directly without
* module-cache manipulation.
*/
export function resolveForwardedHeaderBudget(env?: string): number {
const parsed = Number.parseInt(String(env ?? process.env.OMNIROUTE_FORWARDING_HEADER_BUDGET_BYTES), 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_FORWARDED_HEADER_BUDGET_BYTES;
}
/**
* Keep upstream-derived headers comfortably below common reverse-proxy response-header limits.
* This budget includes each header name, separator, value, and trailing CRLF. OmniRoute's own
* response metadata and framework/security headers are added separately.
* Override with `OMNIROUTE_FORWARDING_HEADER_BUDGET_BYTES`.
*/
export const MAX_FORWARDED_UPSTREAM_RESPONSE_HEADER_BYTES = 768;
export const MAX_FORWARDED_UPSTREAM_RESPONSE_HEADER_BYTES = resolveForwardedHeaderBudget();
const MAX_LOGGED_DROPPED_RESPONSE_HEADERS = 20;
const responseHeaderEncoder = new TextEncoder();

View File

@@ -208,6 +208,7 @@
"tests/unit/executor-antigravity.test.ts",
"tests/unit/executor-web-cookie-sweep.test.ts",
"tests/unit/format-provider-error-cause.test.ts",
"tests/unit/forwarded-header-budget.test.ts",
"tests/unit/gemini-web-missing-browser-3516.test.ts",
"tests/unit/grok-cli-oauth.test.ts",
"tests/unit/guardrails-api-3496.test.ts",

View File

@@ -0,0 +1,31 @@
import { describe, it } from "node:test";
import { equal } from "node:assert/strict";
describe("Forwarded upstream response-header budget (#9243)", () => {
it("resolveForwardedHeaderBudget returns default 768 when env is unset", async () => {
const { resolveForwardedHeaderBudget } = await import(
"@/../open-sse/handlers/chatCore/responseHeaders"
);
equal(resolveForwardedHeaderBudget(undefined), 768);
equal(resolveForwardedHeaderBudget(), 768);
});
it("resolveForwardedHeaderBudget overrides with a valid value", async () => {
const { resolveForwardedHeaderBudget } = await import(
"@/../open-sse/handlers/chatCore/responseHeaders"
);
equal(resolveForwardedHeaderBudget("2048"), 2048);
equal(resolveForwardedHeaderBudget("1"), 1);
equal(resolveForwardedHeaderBudget("4096"), 4096);
});
it("resolveForwardedHeaderBudget falls back to default on invalid input", async () => {
const { resolveForwardedHeaderBudget } = await import(
"@/../open-sse/handlers/chatCore/responseHeaders"
);
equal(resolveForwardedHeaderBudget(""), 768, "empty string");
equal(resolveForwardedHeaderBudget("abc"), 768, "non-numeric");
equal(resolveForwardedHeaderBudget("0"), 768, "zero");
equal(resolveForwardedHeaderBudget("-1"), 768, "negative");
});
});