fix: tighten generic upstream passthrough headers (#422)

* fix: tighten generic upstream passthrough headers

* fix: preserve codex passthrough version headers
This commit is contained in:
Cita
2026-04-05 18:11:42 +08:00
committed by GitHub
parent afbbc4c79a
commit 5b005d8412
2 changed files with 128 additions and 0 deletions
@@ -696,6 +696,93 @@ describe('buildUpstreamEndpointRequest', () => {
]);
});
it('applies a sub2api-style allowlist to generic passthrough headers', () => {
const request = buildUpstreamEndpointRequest({
endpoint: 'chat',
modelName: 'upstream-gpt',
stream: false,
tokenValue: 'sk-test',
sitePlatform: 'sub2api',
siteUrl: 'https://example.com',
openaiBody: {
model: 'gpt-5.2',
messages: [{ role: 'user', content: 'hello' }],
},
downstreamFormat: 'openai',
downstreamHeaders: {
accept: 'application/json',
'accept-language': 'zh-CN',
'user-agent': 'client-ua/1.0',
originator: 'codex_cli_rs',
session_id: 'session-123',
conversation_id: 'conversation-123',
'x-codex-turn-state': 'turn-state',
'x-codex-turn-metadata': 'turn-metadata',
origin: 'https://client.example',
referer: 'https://client.example/chat',
'x-forwarded-for': '203.0.113.1',
'x-real-ip': '203.0.113.2',
version: '0.202.0',
'x-test-header': 'drop-me',
},
});
expect(request.headers.accept).toBe('application/json');
expect(request.headers['accept-language']).toBe('zh-CN');
expect(request.headers['user-agent']).toBe('client-ua/1.0');
expect(request.headers.originator).toBe('codex_cli_rs');
expect(request.headers.session_id).toBe('session-123');
expect(request.headers.conversation_id).toBe('conversation-123');
expect(request.headers['x-codex-turn-state']).toBe('turn-state');
expect(request.headers['x-codex-turn-metadata']).toBe('turn-metadata');
expect(request.headers.origin).toBeUndefined();
expect(request.headers.referer).toBeUndefined();
expect(request.headers['x-forwarded-for']).toBeUndefined();
expect(request.headers['x-real-ip']).toBeUndefined();
expect(request.headers.version).toBeUndefined();
expect(request.headers['x-test-header']).toBeUndefined();
});
it('preserves codex compatibility headers while stripping browser and ip passthrough headers', () => {
const request = buildUpstreamEndpointRequest({
endpoint: 'responses',
modelName: 'gpt-5.2-codex',
stream: false,
tokenValue: 'oauth-access-token',
oauthProvider: 'codex',
sitePlatform: 'codex',
siteUrl: 'https://chatgpt.com/backend-api/codex',
openaiBody: {
model: 'gpt-5.2-codex',
input: 'hello codex',
},
downstreamFormat: 'openai',
downstreamHeaders: {
'user-agent': 'OpenClaw/1.0',
version: '0.202.0',
session_id: 'session-from-client',
'x-responsesapi-include-timing-metrics': '1',
origin: 'https://openclaw.example',
referer: 'https://openclaw.example/app',
'x-forwarded-for': '203.0.113.1',
'x-real-ip': '203.0.113.2',
},
providerHeaders: {
Originator: 'codex_cli_rs',
},
} as any);
expect(request.headers.Version).toBe('0.202.0');
expect(request.headers.Session_id).toBe('session-from-client');
expect(request.headers['User-Agent']).toBe('OpenClaw/1.0');
expect(request.headers['x-responsesapi-include-timing-metrics']).toBe('1');
expect(request.headers.origin).toBeUndefined();
expect(request.headers.referer).toBeUndefined();
expect(request.headers['x-forwarded-for']).toBeUndefined();
expect(request.headers['x-real-ip']).toBeUndefined();
});
it('builds codex responses requests against backend-api path and preserves oauth provider headers', () => {
const request = buildUpstreamEndpointRequest({
endpoint: 'responses',
@@ -115,6 +115,20 @@ const BLOCKED_PASSTHROUGH_HEADERS = new Set([
'sec-websocket-version',
'sec-websocket-extensions',
]);
const GENERIC_PASSTHROUGH_ALLOWED_HEADERS = new Set([
'accept',
'accept-language',
'conversation-id',
'conversation_id',
'openai-beta',
'originator',
'session-id',
'session_id',
'user-agent',
'x-codex-beta-features',
'x-codex-turn-metadata',
'x-codex-turn-state',
]);
const METAPI_INTERNAL_HEADER_BLOCKLIST = new Set([
'x-metapi-tester-request',
'x-metapi-tester-forced-channel-id',
@@ -128,6 +142,7 @@ function shouldSkipPassthroughHeader(key: string): boolean {
if (HOP_BY_HOP_HEADERS.has(key) || BLOCKED_PASSTHROUGH_HEADERS.has(key)) return true;
if (METAPI_INTERNAL_HEADER_BLOCKLIST.has(key)) return true;
if (key.startsWith('x-metapi-')) return true;
if (!GENERIC_PASSTHROUGH_ALLOWED_HEADERS.has(key)) return true;
return false;
}
@@ -197,6 +212,28 @@ function extractResponsesPassthroughHeaders(
return forwarded;
}
function extractCodexPassthroughHeaders(
headers?: Record<string, unknown>,
): Record<string, string> {
if (!headers) return {};
const forwarded: Record<string, string> = {};
for (const [rawKey, rawValue] of Object.entries(headers)) {
const key = rawKey.toLowerCase();
const shouldForward = (
key === 'version'
|| key === 'x-responsesapi-include-timing-metrics'
);
if (!shouldForward) continue;
const value = headerValueToString(rawValue);
if (!value) continue;
forwarded[key] = value;
}
return forwarded;
}
function extractClaudeBetasFromBody(body: Record<string, unknown>): {
body: Record<string, unknown>;
betas: string[];
@@ -725,8 +762,12 @@ export function buildUpstreamEndpointRequest(input: {
};
const passthroughHeaders = extractSafePassthroughHeaders(input.downstreamHeaders);
const codexPassthroughHeaders = sitePlatform === 'codex'
? extractCodexPassthroughHeaders(input.downstreamHeaders)
: {};
const commonHeaders: Record<string, string> = {
...passthroughHeaders,
...codexPassthroughHeaders,
'Content-Type': 'application/json',
...(input.providerHeaders || {}),
};