fix: tighten generic upstream passthrough headers (#422)
* fix: tighten generic upstream passthrough headers * fix: preserve codex passthrough version headers
This commit is contained in:
@@ -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', () => {
|
it('builds codex responses requests against backend-api path and preserves oauth provider headers', () => {
|
||||||
const request = buildUpstreamEndpointRequest({
|
const request = buildUpstreamEndpointRequest({
|
||||||
endpoint: 'responses',
|
endpoint: 'responses',
|
||||||
|
|||||||
@@ -115,6 +115,20 @@ const BLOCKED_PASSTHROUGH_HEADERS = new Set([
|
|||||||
'sec-websocket-version',
|
'sec-websocket-version',
|
||||||
'sec-websocket-extensions',
|
'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([
|
const METAPI_INTERNAL_HEADER_BLOCKLIST = new Set([
|
||||||
'x-metapi-tester-request',
|
'x-metapi-tester-request',
|
||||||
'x-metapi-tester-forced-channel-id',
|
'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 (HOP_BY_HOP_HEADERS.has(key) || BLOCKED_PASSTHROUGH_HEADERS.has(key)) return true;
|
||||||
if (METAPI_INTERNAL_HEADER_BLOCKLIST.has(key)) return true;
|
if (METAPI_INTERNAL_HEADER_BLOCKLIST.has(key)) return true;
|
||||||
if (key.startsWith('x-metapi-')) return true;
|
if (key.startsWith('x-metapi-')) return true;
|
||||||
|
if (!GENERIC_PASSTHROUGH_ALLOWED_HEADERS.has(key)) return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,6 +212,28 @@ function extractResponsesPassthroughHeaders(
|
|||||||
return forwarded;
|
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>): {
|
function extractClaudeBetasFromBody(body: Record<string, unknown>): {
|
||||||
body: Record<string, unknown>;
|
body: Record<string, unknown>;
|
||||||
betas: string[];
|
betas: string[];
|
||||||
@@ -725,8 +762,12 @@ export function buildUpstreamEndpointRequest(input: {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const passthroughHeaders = extractSafePassthroughHeaders(input.downstreamHeaders);
|
const passthroughHeaders = extractSafePassthroughHeaders(input.downstreamHeaders);
|
||||||
|
const codexPassthroughHeaders = sitePlatform === 'codex'
|
||||||
|
? extractCodexPassthroughHeaders(input.downstreamHeaders)
|
||||||
|
: {};
|
||||||
const commonHeaders: Record<string, string> = {
|
const commonHeaders: Record<string, string> = {
|
||||||
...passthroughHeaders,
|
...passthroughHeaders,
|
||||||
|
...codexPassthroughHeaders,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...(input.providerHeaders || {}),
|
...(input.providerHeaders || {}),
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user