fix(typescript): encode non-Latin-1 filenames in Content-Disposition header - #17656
fix(typescript): encode non-Latin-1 filenames in Content-Disposition header#17656fern-api[bot] wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
AI Review Summary
Adds toContentDisposition to safely encode non-Latin-1 filenames per RFC 6266/5987. Logic is correct; main gaps are the un-regenerated seed snapshots (39 fixtures) which will fail CI, and a minor line-length/formatting concern for prettier.
- 🟡 1 warning(s)
- 🔵 1 suggestion(s)
To request another review, comment /ai-review on this pull request.
| if (asciiFallback === normalized) { | ||
| return `attachment; filename="${asciiFallback}"`; | ||
| } | ||
| const encoded = encodeURIComponent(normalized).replace(/['()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`); |
There was a problem hiding this comment.
🔵 suggestion
This line is ~140 chars; the repo's prettier config (typically 120 for these generators) will likely reformat it. Consider breaking it up so CI lint doesn't complain:
| const encoded = encodeURIComponent(normalized).replace(/['()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`); | |
| const encoded = encodeURIComponent(normalized).replace( | |
| /['()*]/g, | |
| (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`, | |
| ); |
There was a problem hiding this comment.
Done in 366aa47 — broke the encodeURIComponent(...).replace(...) call across lines.
| export function toContentDisposition(filename: string): string { | ||
| const normalized = filename.normalize("NFC"); | ||
| const asciiFallback = normalized.replace(/[^\x20-\x7e]|["\\]/g, "_"); | ||
| if (asciiFallback === normalized) { | ||
| return `attachment; filename="${asciiFallback}"`; | ||
| } | ||
| const encoded = encodeURIComponent(normalized).replace(/['()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`); | ||
| return `attachment; filename="${asciiFallback}"; filename*=UTF-8''${encoded}`; | ||
| } |
There was a problem hiding this comment.
🟡 warning
Core-utilities source changes must be mirrored into the seed/ts-sdk/*/src/core/file/file.ts snapshots — the PR description acknowledges 39 fixtures weren't regenerated. Without pnpm seed test --generator ts-sdk, seed CI will fail on diff. Worth doing before merge rather than as a follow-up.
There was a problem hiding this comment.
Done in 366aa47 — regenerated via pnpm seed test --generator ts-sdk for the fixtures that ship core/file (file-upload ×6 variants, bytes-upload, enum, exhaustive incl. local-files-no-source cjs/esm outputs, file-upload-openapi, multiple-request-bodies, openapi-request-body-ref, ts-bytes-upload-path-param). All 39 seed/ts-sdk/**/core/file/file.ts copies now carry toContentDisposition; no remaining attachment; filename="${filename}".
| if (asciiFallback === normalized) { | ||
| return `attachment; filename="${asciiFallback}"`; | ||
| } | ||
| const encoded = encodeURIComponent(normalized).replace(/['()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`); |
There was a problem hiding this comment.
🟡 Malformed Unicode filenames still abort uploads
A filename containing an unpaired UTF-16 surrogate makes encodeURIComponent throw. The upload aborts before sending its request.
Prompt for agents
Handle ill-formed UTF-16 filenames in generators/typescript/utils/core-utilities/src/core/file/file.ts before passing them to encodeURIComponent. JavaScript permits strings containing unpaired surrogates, and encodeURIComponent throws URIError for them. Convert unpaired surrogates to a safe replacement while preserving valid surrogate pairs, then build both the fallback and filename* values from that sanitized string. Add unit coverage for isolated high and low surrogates.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Good catch — fixed in 366aa47. toContentDisposition now runs replaceLoneSurrogates() before .normalize("NFC"), replacing unpaired high/low surrogates with U+FFFD (valid pairs are preserved) so encodeURIComponent can't throw. Added unit cases for bad\ud800.txt, \udc00bad.txt, and a valid astral pair.
SDK Generation Benchmark ResultsComparing PR branch against median of 5 nightly run(s) on Full benchmark table (click to expand)
main (generator): generator-only time via --skip-scripts (includes Docker image build, container startup, IR parsing, and code generation — this is the same Docker-based flow customers use via |
…hots Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Description
Linear ticket: Refs Pylon #23214
Binary uploads in generated TS SDKs (
core/file/file.ts→toBinaryUploadRequest) interpolate the raw filename intoContent-Disposition.fetch/Headersrequires header values to be ISO-8859-1, so any filename with a non-Latin-1 code point throws before the request is sent:This also hits Latin-1-looking names like
été.pdfwhen they come from an NFD filesystem (macOS): the combining acute accent U+0301 is > 255. Reproduced in Node 22 (Headers.set→Cannot convert argument to a ByteString because the character at index N has a value of 769).Changes Made
toContentDisposition(filename)incore/file/file.ts, per RFC 6266 / RFC 5987:"/\) chars become_in the quoted fallback and the exact name is carried infilename*.toMultipartDataPartis unchanged:FormData.append(name, blob, filename)handles UTF-8 filenames itself and does not throw (checked in Node 22).generators/typescript/sdk/changes/unreleased/fix-content-disposition-non-latin1-filename.ymlTesting
tests/unit/file/file.test.tsgains atoContentDispositionsuite (ASCII passthrough, non-ASCII, NFD→NFC, quote escaping, lone/paired surrogates, and anew Headers().set(...)acceptance check).toBinaryUploadRequestthroughHeaders.setin Node 22 for CJK / NFD / quoted filenames; no throw.pnpm seed test --generator ts-sdkfor every fixture that shipscore/file(file-upload ×6, bytes-upload, enum, exhaustive incl.local-files-no-sourcecjs/esm, file-upload-openapi, multiple-request-bodies, openapi-request-body-ref, ts-bytes-upload-path-param). All 39seed/ts-sdk/**/core/file/file.tscopies now usetoContentDisposition. Ran the regeneratedtests/unit/file/file.test.tsinside a generated SDK (file-upload/no-custom-config): 36/36 pass,tsc --noEmitclean.encodeURIComponentcannot throw (bad\ud800.txt→filename="bad_.txt"; filename*=UTF-8''bad%EF%BF%BD.txt).