Skip to content

fix(typescript): encode non-Latin-1 filenames in Content-Disposition header - #17656

Open
fern-api[bot] wants to merge 2 commits into
mainfrom
devin/1788539479-ts-content-disposition-encoding
Open

fix(typescript): encode non-Latin-1 filenames in Content-Disposition header#17656
fern-api[bot] wants to merge 2 commits into
mainfrom
devin/1788539479-ts-content-disposition-encoding

Conversation

@fern-api

@fern-api fern-api Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Description

Linear ticket: Refs Pylon #23214

Binary uploads in generated TS SDKs (core/file/file.tstoBinaryUploadRequest) interpolate the raw filename into Content-Disposition. fetch/Headers requires header values to be ISO-8859-1, so any filename with a non-Latin-1 code point throws before the request is sent:

TypeError: Failed to execute 'set' on 'Headers': String contains non ISO-8859-1 code point.

This also hits Latin-1-looking names like été.pdf when they come from an NFD filesystem (macOS): the combining acute accent U+0301 is > 255. Reproduced in Node 22 (Headers.setCannot convert argument to a ByteString because the character at index N has a value of 769).

Changes Made

  • New toContentDisposition(filename) in core/file/file.ts, per RFC 6266 / RFC 5987:
    "report.pdf"        -> attachment; filename="report.pdf"
    "rapport-été.pdf"   -> attachment; filename="rapport-_t_.pdf"; filename*=UTF-8''rapport-%C3%A9t%C3%A9.pdf
    "日本.pdf"           -> attachment; filename="__.pdf"; filename*=UTF-8''%E6%97%A5%E6%9C%AC.pdf
    
    NFC-normalizes first; plain-ASCII names are emitted exactly as before. Non-ASCII (and "/\) chars become _ in the quoted fallback and the exact name is carried in filename*.
  • toMultipartDataPart is unchanged: FormData.append(name, blob, filename) handles UTF-8 filenames itself and does not throw (checked in Node 22).
  • Changelog: generators/typescript/sdk/changes/unreleased/fix-content-disposition-non-latin1-filename.yml
  • Updated README.md generator (if applicable) — N/A

Testing

  • Unit tests added/updated — tests/unit/file/file.test.ts gains a toContentDisposition suite (ASCII passthrough, non-ASCII, NFD→NFC, quote escaping, lone/paired surrogates, and a new Headers().set(...) acceptance check).
  • Manual testing completed — ran the new helper + toBinaryUploadRequest through Headers.set in Node 22 for CJK / NFD / quoted filenames; no throw.
  • Seed snapshots regenerated with pnpm seed test --generator ts-sdk for every fixture that ships core/file (file-upload ×6, bytes-upload, enum, exhaustive incl. local-files-no-source cjs/esm, 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 use toContentDisposition. Ran the regenerated tests/unit/file/file.test.ts inside a generated SDK (file-upload/no-custom-config): 36/36 pass, tsc --noEmit clean.
  • Also handles ill-formed UTF-16: lone surrogates are replaced with U+FFFD before NFC normalization so encodeURIComponent cannot throw (bad\ud800.txtfilename="bad_.txt"; filename*=UTF-8''bad%EF%BF%BD.txt).

Devin Review

@nitpickybot nitpickybot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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()}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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:

Suggested change
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()}`,
);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 366aa47 — broke the encodeURIComponent(...).replace(...) call across lines.

Comment on lines +41 to +49
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}`;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}".

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

Devin Review

if (asciiFallback === normalized) {
return `attachment; filename="${asciiFallback}"`;
}
const encoded = encodeURIComponent(normalized).replace(/['()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

SDK Generation Benchmark Results

Comparing PR branch against median of 5 nightly run(s) on main (latest: 2026-09-04T04:06:24Z).

Full benchmark table (click to expand)
Generator Spec main (generator) main (E2E) PR (generator) Delta
ts-sdk square 178s (n=5) 184s (n=5) 135s -43s (-24.2%)

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 fern generate). main (E2E): full customer-observable time including build/test scripts (nightly baseline, informational). Delta is computed against generator-only baseline.
⚠️ = generation exited with a non-zero exit code (timing may not reflect a successful run).
Baseline from nightly runs on main (latest: 2026-09-04T04:06:24Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-09-04 17:55 UTC

…hots

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants