From cbe29e4e257dbfe109990064bd0b4d693c58933b Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 18 Mar 2026 14:11:30 -0400 Subject: [PATCH] fix: encode non-ASCII filenames in chat file upload header (#23241) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem Uploading a file on the `/agents` chat page fails with: ``` Failed to execute 'setRequestHeader' on 'XMLHttpRequest': String contains non ISO-8859-1 code point. ``` This happens when the image filename contains non-ASCII characters (e.g. CJK characters from macOS screenshots like `スクリーンショット.png`, accented characters, emoji, etc.). HTTP headers only support ISO-8859-1 code points, and the filename was being interpolated directly into the `Content-Disposition` header. ## Fix Use [RFC 5987](https://datatracker.ietf.org/doc/html/rfc5987) `filename*=UTF-8''` encoding so the percent-encoded name is always valid in the header. A static ASCII `filename="file"` fallback is included for older clients. The server already uses Go's `mime.ParseMediaType` which decodes `filename*` automatically, so no backend changes are needed. ### Before ```ts "Content-Disposition": `attachment; filename="${file.name}"` ``` ### After ```ts "Content-Disposition": `attachment; filename="file"; filename*=UTF-8''${encodeURIComponent(file.name)}` ``` ## Testing Added a server-side test (`TestGetChatFile/UnicodeFilename`) that uploads with a Japanese filename and verifies it round-trips correctly through the `Content-Disposition` header. --- coderd/chats_test.go | 25 +++++++++++++++++++++++++ site/src/api/api.ts | 6 +++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/coderd/chats_test.go b/coderd/chats_test.go index f056d5fbf4..6a38b592c3 100644 --- a/coderd/chats_test.go +++ b/coderd/chats_test.go @@ -6,6 +6,7 @@ import ( "database/sql" "encoding/json" "fmt" + "mime" "net/http" "net/http/httptest" "regexp" @@ -3986,6 +3987,30 @@ func TestGetChatFile(t *testing.T) { require.NotContains(t, cd, strings.Repeat("a", 256)) }) + t.Run("UnicodeFilename", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitLong) + client := newChatClient(t) + firstUser := coderdtest.CreateFirstUser(t, client) + + // Upload with a non-ASCII filename using RFC 5987 encoding, + // which is what the frontend sends for Unicode filenames. + data := append([]byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}, make([]byte, 64)...) + uploaded, err := client.UploadChatFile(ctx, firstUser.OrganizationID, "image/png", "スクリーンショット.png", bytes.NewReader(data)) + require.NoError(t, err) + + res, err := client.Request(ctx, http.MethodGet, + fmt.Sprintf("/api/experimental/chats/files/%s", uploaded.ID), nil) + require.NoError(t, err) + defer res.Body.Close() + require.Equal(t, http.StatusOK, res.StatusCode) + cd := res.Header.Get("Content-Disposition") + require.Contains(t, cd, "inline") + _, params, err := mime.ParseMediaType(cd) + require.NoError(t, err) + require.Equal(t, "スクリーンショット.png", params["filename"]) + }) + t.Run("NotFound", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitLong) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index feba6c2224..65a346d1ad 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -2312,7 +2312,11 @@ class ApiMethods { { headers: { "Content-Type": file.type || "application/octet-stream", - "Content-Disposition": `attachment; filename="${file.name}"`, + // Use RFC 5987 encoding for the filename to support + // non-ASCII characters. Placing the raw name directly in + // the header causes XMLHttpRequest to throw because HTTP + // headers only allow ISO-8859-1 code points. + "Content-Disposition": `attachment; filename="file"; filename*=UTF-8''${encodeURIComponent(file.name)}`, }, }, );