fix: encode non-ASCII filenames in chat file upload header (#23241)

## 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.
This commit is contained in:
Kyle Carberry
2026-03-18 14:11:30 -04:00
committed by GitHub
parent dbb7aee65b
commit cbe29e4e25
2 changed files with 30 additions and 1 deletions
+25
View File
@@ -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)