Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/internal/headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,14 @@ export const buildHeaders = (newHeaders: HeadersLike[]): NullableHeaders => {
for (const headers of newHeaders) {
const seenHeaders = new Set<string>();
for (const [name, value] of iterateHeaders(headers)) {
const lowerName = name.toLowerCase();
// HTTP header names are ASCII per RFC 9110, but `String.prototype.toLowerCase()`
// applies the host locale to certain Unicode mappings. On Turkish locales the
// uppercase `I` in `OpenAI-Organization` becomes the dotless `ı`, producing
// `openaı-organization`, which fails Node's HTTP token validation
// (`TypeError: Header name must be a valid HTTP token`). Pin the locale to
// `en-US` so the casing rule stays ASCII-safe regardless of the runtime locale.
// https://github.com/openai/openai-node/issues/1928
const lowerName = name.toLocaleLowerCase('en-US');
if (!seenHeaders.has(lowerName)) {
targetHeaders.delete(name);
seenHeaders.add(lowerName);
Expand Down
31 changes: 19 additions & 12 deletions src/internal/utils/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,18 +103,25 @@ export const formatRequestDetails = (details: {
if (details.headers) {
details.headers = Object.fromEntries(
(details.headers instanceof Headers ? [...details.headers] : Object.entries(details.headers)).map(
([name, value]) => [
name,
(
name.toLowerCase() === 'authorization' ||
name.toLowerCase() === 'api-key' ||
name.toLowerCase() === 'x-api-key' ||
name.toLowerCase() === 'cookie' ||
name.toLowerCase() === 'set-cookie'
) ?
'***'
: value,
],
([name, value]) => {
// Lowercase with an explicit `en-US` locale: on Turkish locales a plain
// `.toLowerCase()` maps `I` → `ı`, so `Authorization` becomes
// `authorızatıon` and the credential redaction below silently misses it.
// https://github.com/openai/openai-node/issues/1928
const lowerName = name.toLocaleLowerCase('en-US');
return [
name,
(
lowerName === 'authorization' ||
lowerName === 'api-key' ||
lowerName === 'x-api-key' ||
lowerName === 'cookie' ||
lowerName === 'set-cookie'
) ?
'***'
: value,
];
},
),
);
}
Expand Down
20 changes: 20 additions & 0 deletions tests/buildHeaders.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,24 @@ describe('buildHeaders', () => {
expect(inspectNullableHeaders(buildHeaders(input))).toEqual(expected);
});
}

// Regression for https://github.com/openai/openai-node/issues/1928
test('lowercases header names with ASCII rules even under a Turkish process locale', () => {
const originalLowerCase = String.prototype.toLowerCase;
// Simulate the Turkish locale's casing rule that maps uppercase `I` to the
// dotless `ı`. This is what V8 actually does on Turkish Windows / Linux
// installs, but we monkey-patch it here so the test is locale-independent.
String.prototype.toLowerCase = function turkishToLowerCase(this: string): string {
return this.replace(/I/g, 'ı').replace(/[A-HJ-Z]/g, (c) => c.charCodeAt(0) === 73 ? c : String.fromCharCode(c.charCodeAt(0) | 0x20));
};
try {
const result = buildHeaders([{ 'OpenAI-Organization': 'org_test' }]);
// The canonical key must be ASCII-only — no dotless `ı`.
const keys = [...result.values.keys()];
expect(keys).toContain('openai-organization');
expect(keys.every((k) => /^[\x00-\x7F]*$/.test(k))).toBe(true);
} finally {
String.prototype.toLowerCase = originalLowerCase;
}
});
});