fix: strip streaming compression headers

This commit is contained in:
Kahramanov
2026-05-14 16:52:34 +03:00
parent c6f5b394f8
commit 7c89858797
2 changed files with 107 additions and 18 deletions

View File

@@ -485,6 +485,35 @@ function mergeResponseToolNameMap(
return merged;
}
const STREAMING_RESPONSE_HEADER_DENYLIST = new Set([
"content-type",
"content-encoding",
"content-length",
"transfer-encoding",
]);
export function buildStreamingResponseHeaders(
providerHeaders: Headers,
meta: Parameters<typeof buildOmniRouteResponseMetaHeaders>[0]
): Record<string, string> {
const forwardedHeaders: [string, string][] = [];
providerHeaders.forEach((value, key) => {
if (!STREAMING_RESPONSE_HEADER_DENYLIST.has(key.toLowerCase())) {
forwardedHeaders.push([key, value]);
}
});
return {
...Object.fromEntries(forwardedHeaders),
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
"X-Accel-Buffering": "no",
[OMNIROUTE_RESPONSE_HEADERS.cache]: "MISS",
...buildOmniRouteResponseMetaHeaders(meta),
};
}
function materializeDeduplicatedExecutionResult<T extends Record<string, unknown>>(result: T): T {
const snapshot =
result && typeof result === "object"
@@ -4134,7 +4163,10 @@ export async function handleChatCore({
try {
const firstChoice = translatedResponse?.choices?.[0];
const msg = firstChoice?.message;
cacheReasoningFromAssistantMessage(msg, provider, model, { requestId: skillRequestId, messageIndex: 0 });
cacheReasoningFromAssistantMessage(msg, provider, model, {
requestId: skillRequestId,
messageIndex: 0,
});
} catch {
// Cache capture is non-critical — never block the response
}
@@ -4364,28 +4396,17 @@ export async function handleChatCore({
await onRequestSuccess();
}
const responseHeaders: Record<string, string> = {
...Object.fromEntries(
(() => {
const arr: [string, string][] = [];
providerResponse.headers.forEach((v, k) => arr.push([k, v]));
return arr;
})().filter(([k]) => k.toLowerCase() !== "content-type")
),
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
"X-Accel-Buffering": "no",
[OMNIROUTE_RESPONSE_HEADERS.cache]: "MISS",
...buildOmniRouteResponseMetaHeaders({
const responseHeaders: Record<string, string> = buildStreamingResponseHeaders(
providerResponse.headers,
{
provider,
model,
cacheHit: false,
latencyMs: 0,
usage: null,
costUsd: 0,
}),
};
}
);
// Create transform stream with logger for streaming response
let transformStream;
@@ -4421,7 +4442,10 @@ export async function handleChatCore({
const body = streamResponseBody as Record<string, unknown>;
const choices = body.choices as { message?: Record<string, unknown> }[] | undefined;
const msg = choices?.[0]?.message;
cacheReasoningFromAssistantMessage(msg, provider, model, { requestId: skillRequestId, messageIndex: 0 });
cacheReasoningFromAssistantMessage(msg, provider, model, {
requestId: skillRequestId,
messageIndex: 0,
});
} catch {
// Cache capture is non-critical — never block the stream
}

View File

@@ -39,6 +39,7 @@ const {
shouldUseNativeCodexPassthrough,
isTokenExpiringSoon,
clearUpstreamProxyConfigCache,
buildStreamingResponseHeaders,
} = await import("../../open-sse/handlers/chatCore.ts");
const { resetPayloadRulesConfigForTests, setPayloadRulesConfig } =
await import("../../open-sse/services/payloadRules.ts");
@@ -2063,6 +2064,70 @@ test("chatCore emits final SSE metadata comments before [DONE] on streaming resp
);
});
test("buildStreamingResponseHeaders drops upstream compression and framing headers", () => {
const headers = new Headers(
buildStreamingResponseHeaders(
new Headers({
"Content-Type": "text/event-stream",
"Content-Encoding": "gzip",
"Content-Length": "999",
"Transfer-Encoding": "chunked",
"X-Upstream-Trace": "trace-1",
}),
{
provider: "openai",
model: "gpt-4o-mini",
cacheHit: false,
latencyMs: 0,
usage: null,
costUsd: 0,
}
)
);
assert.equal(headers.get("Content-Type"), "text/event-stream");
assert.equal(headers.get("Content-Encoding"), null);
assert.equal(headers.get("Content-Length"), null);
assert.equal(headers.get("Transfer-Encoding"), null);
assert.equal(headers.get("X-Upstream-Trace"), "trace-1");
assert.equal(headers.get("X-OmniRoute-Cache"), "MISS");
});
test("chatCore strips upstream compression and length headers from streaming responses", async () => {
const upstreamPayload = `data: ${JSON.stringify({
id: "chatcmpl-stream-headers",
object: "chat.completion.chunk",
choices: [{ index: 0, delta: { role: "assistant", content: "streamed" } }],
})}\n\ndata: [DONE]\n\n`;
const { result } = await invokeChatCore({
provider: "openai",
model: "gpt-4o-mini",
accept: "text/event-stream",
body: {
model: "gpt-4o-mini",
stream: true,
messages: [{ role: "user", content: "stream header sanitization" }],
},
responseFactory() {
return new Response(upstreamPayload, {
status: 200,
headers: {
"Content-Type": "text/event-stream",
"Content-Length": String(Buffer.byteLength(upstreamPayload)),
"X-Upstream-Trace": "trace-1",
},
});
},
});
assert.equal(result.success, true);
assert.equal(result.response.headers.get("Content-Type"), "text/event-stream");
assert.equal(result.response.headers.get("Content-Length"), null);
assert.equal(result.response.headers.get("X-Upstream-Trace"), "trace-1");
assert.equal(result.response.headers.get("X-OmniRoute-Cache"), "MISS");
await result.response.text();
});
test("chatCore maps upstream aborts to request-aborted errors", async () => {
const { result } = await invokeChatCore({
provider: "openai",