diff --git a/src/internal/headers.ts b/src/internal/headers.ts index c724a9d225..50e1e205a6 100644 --- a/src/internal/headers.ts +++ b/src/internal/headers.ts @@ -74,7 +74,14 @@ export const buildHeaders = (newHeaders: HeadersLike[]): NullableHeaders => { for (const headers of newHeaders) { const seenHeaders = new Set(); 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); diff --git a/src/internal/utils/log.ts b/src/internal/utils/log.ts index 94de74b5b7..a2bdbb226e 100644 --- a/src/internal/utils/log.ts +++ b/src/internal/utils/log.ts @@ -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, + ]; + }, ), ); } diff --git a/tests/buildHeaders.test.ts b/tests/buildHeaders.test.ts index 3f8e4d28ec..f4a8356bdb 100644 --- a/tests/buildHeaders.test.ts +++ b/tests/buildHeaders.test.ts @@ -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; + } + }); });