From cfeafa4f961b971f72f4964164ae45267b0e4c37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonat=C3=A3=20Oliveira?= Date: Mon, 24 Aug 2026 20:52:19 -0300 Subject: [PATCH 1/3] Fix file uploads with non-ASCII filenames MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit uploadFile() sent the file path in a raw HTTP header, but headers only accept ISO-8859-1. Filenames with accents (e.g. macOS's NFD-decomposed "café.txt") fall outside that range, so fetch() throws before the request is even sent. deleteFile() and getDownloadUrl() already avoided this by sending the path as an encoded query param; upload was the odd one out, across the core SDK, admin SDK, and the dashboard's storage explorer. content-disposition has the same issue and gets the same fix. Clients now send path/content-disposition as an encoded query param and only mirror them into headers when the value is ISO-8859-1-safe, so requests to older self-hosted servers keep working. The corresponding server upload handlers merge headers and query params (query params win), matching how file-delete and signed-download-url-get already read their params, so newer clients keep working against older servers too. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01PQ8ju4b8aRBBpVTXsfxdSp --- client/packages/admin/src/index.ts | 29 ++++++++--- .../components/explorer/inner-explorer.tsx | 22 +++++++-- client/packages/core/src/StorageAPI.ts | 24 ++++++++-- client/www/app/docs/http-api/page.md | 4 +- server/src/instant/admin/routes.clj | 14 ++++-- server/src/instant/dash/routes.clj | 10 +++- server/src/instant/storage/routes.clj | 8 +++- server/test/instant/admin/routes_test.clj | 14 ++++++ server/test/instant/storage/routes_test.clj | 48 +++++++++++++++++++ 9 files changed, 151 insertions(+), 22 deletions(-) create mode 100644 server/test/instant/storage/routes_test.clj diff --git a/client/packages/admin/src/index.ts b/client/packages/admin/src/index.ts index 63e8c5c7b2..adc09fbfbb 100644 --- a/client/packages/admin/src/index.ts +++ b/client/packages/admin/src/index.ts @@ -845,6 +845,15 @@ const isNodeReadable = (v: any): v is Readable => const isWebReadable = (v: any): v is ReadableStream => v && typeof v.getReader === 'function'; +// HTTP header values must be ISO-8859-1. Filenames often aren't (e.g. +// macOS decomposes accented characters into combining marks that fall +// outside that range), so we prefer sending `path` as an encoded query +// param and only mirror it into a header when it's safe to do so, for +// compatibility with older self-hosted servers that only look at headers. +function isHeaderSafe(value: string): boolean { + return /^[\x20-\x7e\xa0-\xff]*$/.test(value); +} + /** * Functions to manage file storage. */ @@ -872,10 +881,16 @@ class Storage { ): Promise => { const headers = { ...authorizedHeaders(this.config, this.impersonationOpts), - path, }; + // Kept for backwards compatibility with servers that only read `path` + // from a header; the query param below is the source of truth. + if (isHeaderSafe(path)) { + headers['path'] = path; + } if (metadata.contentDisposition) { - headers['content-disposition'] = metadata.contentDisposition; + if (isHeaderSafe(metadata.contentDisposition)) { + headers['content-disposition'] = metadata.contentDisposition; + } } // headers.content-type will become "undefined" (string) @@ -906,10 +921,12 @@ class Storage { ...(duplex && { duplex }), }; - return jsonFetch( - `${this.config.apiURI}/admin/storage/upload?app_id=${this.config.appId}`, - options, - ); + let url = `${this.config.apiURI}/admin/storage/upload?app_id=${encodeURIComponent(this.config.appId)}&path=${encodeURIComponent(path)}`; + if (metadata.contentDisposition) { + url += `&content-disposition=${encodeURIComponent(metadata.contentDisposition)}`; + } + + return jsonFetch(url, options); }; /** diff --git a/client/packages/components/src/components/explorer/inner-explorer.tsx b/client/packages/components/src/components/explorer/inner-explorer.tsx index 7eba2ed498..c67def9c1b 100644 --- a/client/packages/components/src/components/explorer/inner-explorer.tsx +++ b/client/packages/components/src/components/explorer/inner-explorer.tsx @@ -1490,6 +1490,15 @@ export async function jsonFetch( : Promise.reject({ status: res.status, body: json }); } +// HTTP header values must be ISO-8859-1. Filenames often aren't (e.g. +// macOS decomposes accented characters into combining marks that fall +// outside that range), so we prefer sending `path` as an encoded query +// param and only mirror it into a header when it's safe to do so, for +// compatibility with older self-hosted servers that only look at headers. +function isHeaderSafe(value: string): boolean { + return /^[\x20-\x7e\xa0-\xff]*$/.test(value); +} + async function upload( token: string, appId: string, @@ -1497,15 +1506,22 @@ async function upload( customFilename: string, apiUri: string, ): Promise { - const headers = { + const path = customFilename || file.name; + const headers: Record = { 'app-id': appId, app_id: appId, - path: customFilename || file.name, authorization: `Bearer ${token}`, 'content-type': file.type, }; + // Kept for backwards compatibility with servers that only read `path` + // from a header; the query param below is the source of truth. + if (isHeaderSafe(path)) { + headers['path'] = path; + } + + const url = `${apiUri}/dash/apps/${appId}/storage/upload?path=${encodeURIComponent(path)}`; - const data = await jsonFetch(`${apiUri}/dash/apps/${appId}/storage/upload`, { + const data = await jsonFetch(url, { method: 'PUT', headers, body: file, diff --git a/client/packages/core/src/StorageAPI.ts b/client/packages/core/src/StorageAPI.ts index f5197db748..d18c1a5dc0 100644 --- a/client/packages/core/src/StorageAPI.ts +++ b/client/packages/core/src/StorageAPI.ts @@ -1,5 +1,14 @@ import { jsonFetch } from './utils/fetch.js'; +// HTTP header values must be ISO-8859-1. Filenames often aren't (e.g. +// macOS decomposes accented characters into combining marks that fall +// outside that range), so we prefer sending `path` as an encoded query +// param and only mirror it into a header when it's safe to do so, for +// compatibility with older self-hosted servers that only look at headers. +function isHeaderSafe(value: string): boolean { + return /^[\x20-\x7e\xa0-\xff]*$/.test(value); +} + export type UploadFileResponse = { data: { id: string; @@ -32,15 +41,24 @@ export async function uploadFile({ const headers = { 'app-id': appId, app_id: appId, - path, authorization: `Bearer ${refreshToken}`, 'content-type': contentType || file.type, }; - if (contentDisposition) { + // Kept for backwards compatibility with servers that only read `path` + // from a header; the query param below is the source of truth. + if (isHeaderSafe(path)) { + headers['path'] = path; + } + if (contentDisposition && isHeaderSafe(contentDisposition)) { headers['content-disposition'] = contentDisposition; } - const data = await jsonFetch(`${apiURI}/storage/upload`, { + let url = `${apiURI}/storage/upload?app_id=${encodeURIComponent(appId)}&path=${encodeURIComponent(path)}`; + if (contentDisposition) { + url += `&content-disposition=${encodeURIComponent(contentDisposition)}`; + } + + const data = await jsonFetch(url, { method: 'PUT', headers, body: file, diff --git a/client/www/app/docs/http-api/page.md b/client/www/app/docs/http-api/page.md index 653d68bbab..a613ba903a 100644 --- a/client/www/app/docs/http-api/page.md +++ b/client/www/app/docs/http-api/page.md @@ -323,10 +323,8 @@ You can also manage your app's [storage](/docs/storage) with the HTTP API. Upload a file with `PUT /admin/storage/upload`: ```shell -curl -X PUT "https://api.instantdb.com/admin/storage/upload" \ +curl -X PUT "https://api.instantdb.com/admin/storage/upload?app_id=$APP_ID&path=snippets/demo.txt" \ -H "Authorization: Bearer $ADMIN_TOKEN" \ - -H "App-Id: $APP_ID" \ - -H "Path: snippets/demo.txt" \ -H "Content-Type: text/plain" \ --data-binary "@demo.txt" ``` diff --git a/server/src/instant/admin/routes.clj b/server/src/instant/admin/routes.clj index 9ebf1da15f..2038066e3c 100644 --- a/server/src/instant/admin/routes.clj +++ b/server/src/instant/admin/routes.clj @@ -627,11 +627,17 @@ (defn upload-put [req] (let [{:keys [app-id] :as perms} (get-perms! req :storage/write) - params (:headers req) - path (ex/get-param! params ["path"] string-util/coerce-non-blank-str) + ;; `path` (and `content-disposition`) may arrive either as a query + ;; param (preferred, URL-decoded by Ring) or as a raw header (kept + ;; for backwards compatibility with older clients). Query params + ;; take priority since they can carry values headers can't (e.g. + ;; non-ISO-8859-1 filenames). + params (merge (w/keywordize-keys (:headers req)) + (:params req)) + path (ex/get-param! params [:path] string-util/coerce-non-blank-str) file (ex/get-param! req [:body] identity) - content-type (storage-coordinator/coerce-content-type (get params "content-type")) - content-disposition (ex/get-optional-param! params ["content-disposition"] string-util/coerce-non-blank-str) + content-type (storage-coordinator/coerce-content-type (:content-type params)) + content-disposition (ex/get-optional-param! params [:content-disposition] string-util/coerce-non-blank-str) data (storage-coordinator/upload-file! {:app-id app-id :path path :content-type content-type diff --git a/server/src/instant/dash/routes.clj b/server/src/instant/dash/routes.clj index a8cf75c7e0..ad549e0495 100644 --- a/server/src/instant/dash/routes.clj +++ b/server/src/instant/dash/routes.clj @@ -1764,8 +1764,14 @@ (let [{{app-id :id} :app} (req->app-accepting-superadmin-or-ref-token! :collaborator :apps/read req) - params (:headers req) - path (ex/get-param! params ["path"] string-util/coerce-non-blank-str) + ;; `path` may arrive either as a query param (preferred, + ;; URL-decoded by Ring) or as a raw header (kept for backwards + ;; compatibility with older clients). Query params take priority + ;; since they can carry values headers can't (e.g. non-ISO-8859-1 + ;; filenames). + params (merge (w/keywordize-keys (:headers req)) + (:params req)) + path (ex/get-param! params [:path] string-util/coerce-non-blank-str) file (ex/get-param! req [:body] identity) content-type (storage-coordinator/coerce-content-type (:content-type req)) data (storage-coordinator/upload-file! diff --git a/server/src/instant/storage/routes.clj b/server/src/instant/storage/routes.clj index d6bb684129..4847500582 100644 --- a/server/src/instant/storage/routes.clj +++ b/server/src/instant/storage/routes.clj @@ -27,7 +27,13 @@ :content-disposition (ex/get-optional-param! params [:content-disposition] string-util/coerce-non-blank-str)})) (defn upload-put [req] - (let [params (w/keywordize-keys (:headers req)) + (let [;; `path` (and `content-disposition`) may arrive either as a query + ;; param (preferred, URL-decoded by Ring) or as a raw header (kept + ;; for backwards compatibility with older clients). Query params + ;; take priority since they can carry values headers can't (e.g. + ;; non-ISO-8859-1 filenames). + params (merge (w/keywordize-keys (:headers req)) + (:params req)) ctx (req->app-file! req params) file (ex/get-param! req [:body] identity) data (storage-coordinator/upload-file! ctx file)] diff --git a/server/test/instant/admin/routes_test.clj b/server/test/instant/admin/routes_test.clj index ec9676d4a0..3c33e036c9 100644 --- a/server/test/instant/admin/routes_test.clj +++ b/server/test/instant/admin/routes_test.clj @@ -1088,6 +1088,20 @@ (is (= 200 (:status ret))) (is (some? (-> ret :body :data :id))))) + (testing "admin can upload a file with a non-ASCII filename via query params" + ;; Filenames with accents (e.g. macOS's NFD-decomposed "café.txt") + ;; can't be sent as raw HTTP header values, so clients send them + ;; as an encoded query param instead. + (let [ret (upload-put + {:body (make-file-content) + :params {:path "café à noite.txt"} + :headers {"app-id" app-id + "authorization" (str "Bearer " admin-token) + "content-type" "text/plain"} + :content-length 5})] + (is (= 200 (:status ret))) + (is (some? (-> ret :body :data :id))))) + (testing "user with email can upload" (let [ret (upload-put {:body (make-file-content) diff --git a/server/test/instant/storage/routes_test.clj b/server/test/instant/storage/routes_test.clj new file mode 100644 index 0000000000..00b957a639 --- /dev/null +++ b/server/test/instant/storage/routes_test.clj @@ -0,0 +1,48 @@ +(ns instant.storage.routes-test + (:require [clojure.test :as test :refer [deftest is testing]] + [instant.storage.routes :as storage-routes] + [instant.storage.coordinator :as storage-coordinator])) + +(deftest upload-put-reads-path-from-query-params + ;; Filenames with accents (e.g. macOS's NFD-decomposed "café.txt") can't + ;; be sent as raw HTTP header values, so clients send them as an encoded + ;; query param (already URL-decoded by Ring's wrap-params) instead. The + ;; route should prefer that over the legacy `path` header. + (let [captured-ctx (atom nil) + app-id (random-uuid)] + (with-redefs [storage-coordinator/upload-file! + (fn [ctx _file] + (reset! captured-ctx ctx) + {:id "fake-file-id"})] + (testing "path comes from query params when present" + (let [ret (storage-routes/upload-put + {:body "file-contents" + :params {:app_id (str app-id) + :path "café à noite.txt"} + :headers {"app-id" (str app-id) + "content-type" "text/plain"} + :content-length 5})] + (is (= 200 (:status ret))) + (is (= "café à noite.txt" (:path @captured-ctx))))) + + (testing "path still works from the legacy header for ASCII filenames" + (let [ret (storage-routes/upload-put + {:body "file-contents" + :params {} + :headers {"app-id" (str app-id) + "path" "legacy-file.txt" + "content-type" "text/plain"} + :content-length 5})] + (is (= 200 (:status ret))) + (is (= "legacy-file.txt" (:path @captured-ctx))))) + + (testing "query param takes priority over the header when both are present" + (let [ret (storage-routes/upload-put + {:body "file-contents" + :params {:path "café.txt"} + :headers {"app-id" (str app-id) + "path" "stale-ascii-name.txt" + "content-type" "text/plain"} + :content-length 5})] + (is (= 200 (:status ret))) + (is (= "café.txt" (:path @captured-ctx)))))))) From 83f2c971a98d90296ebe0c97317e94d2a26236de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonat=C3=A3=20Oliveira?= Date: Mon, 24 Aug 2026 21:07:04 -0300 Subject: [PATCH 2/3] Add docstrings for the new/touched upload helpers Documents isHeaderSafe and the touched uploadFile/upload-put functions across the client and server, matching the docstring style already used by their siblings (e.g. the other functions in storage/coordinator.clj). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01PQ8ju4b8aRBBpVTXsfxdSp --- client/packages/admin/src/index.ts | 12 +++++++----- .../src/components/explorer/inner-explorer.tsx | 18 +++++++++++++----- client/packages/core/src/StorageAPI.ts | 18 +++++++++++++----- server/src/instant/admin/routes.clj | 13 +++++++------ server/src/instant/dash/routes.clj | 13 +++++++------ server/src/instant/storage/routes.clj | 15 ++++++++------- 6 files changed, 55 insertions(+), 34 deletions(-) diff --git a/client/packages/admin/src/index.ts b/client/packages/admin/src/index.ts index adc09fbfbb..d6c265c2de 100644 --- a/client/packages/admin/src/index.ts +++ b/client/packages/admin/src/index.ts @@ -845,11 +845,13 @@ const isNodeReadable = (v: any): v is Readable => const isWebReadable = (v: any): v is ReadableStream => v && typeof v.getReader === 'function'; -// HTTP header values must be ISO-8859-1. Filenames often aren't (e.g. -// macOS decomposes accented characters into combining marks that fall -// outside that range), so we prefer sending `path` as an encoded query -// param and only mirror it into a header when it's safe to do so, for -// compatibility with older self-hosted servers that only look at headers. +/** + * Whether `value` can be safely sent as a raw HTTP header value. Header + * values must be ISO-8859-1; filenames often aren't (e.g. macOS decomposes + * accented characters into combining marks that fall outside that range), + * so callers should prefer sending such values as an encoded query param + * instead. + */ function isHeaderSafe(value: string): boolean { return /^[\x20-\x7e\xa0-\xff]*$/.test(value); } diff --git a/client/packages/components/src/components/explorer/inner-explorer.tsx b/client/packages/components/src/components/explorer/inner-explorer.tsx index c67def9c1b..a37a04d817 100644 --- a/client/packages/components/src/components/explorer/inner-explorer.tsx +++ b/client/packages/components/src/components/explorer/inner-explorer.tsx @@ -1490,15 +1490,23 @@ export async function jsonFetch( : Promise.reject({ status: res.status, body: json }); } -// HTTP header values must be ISO-8859-1. Filenames often aren't (e.g. -// macOS decomposes accented characters into combining marks that fall -// outside that range), so we prefer sending `path` as an encoded query -// param and only mirror it into a header when it's safe to do so, for -// compatibility with older self-hosted servers that only look at headers. +/** + * Whether `value` can be safely sent as a raw HTTP header value. Header + * values must be ISO-8859-1; filenames often aren't (e.g. macOS decomposes + * accented characters into combining marks that fall outside that range), + * so callers should prefer sending such values as an encoded query param + * instead. + */ function isHeaderSafe(value: string): boolean { return /^[\x20-\x7e\xa0-\xff]*$/.test(value); } +/** + * Uploads `file` to the dashboard's storage explorer for `appId`, at either + * `customFilename` or the file's own name. The filename is sent as an + * encoded query param (and mirrored into a header only when + * ISO-8859-1-safe) so that non-ASCII filenames don't break the request. + */ async function upload( token: string, appId: string, diff --git a/client/packages/core/src/StorageAPI.ts b/client/packages/core/src/StorageAPI.ts index d18c1a5dc0..6bf55d5177 100644 --- a/client/packages/core/src/StorageAPI.ts +++ b/client/packages/core/src/StorageAPI.ts @@ -1,10 +1,12 @@ import { jsonFetch } from './utils/fetch.js'; -// HTTP header values must be ISO-8859-1. Filenames often aren't (e.g. -// macOS decomposes accented characters into combining marks that fall -// outside that range), so we prefer sending `path` as an encoded query -// param and only mirror it into a header when it's safe to do so, for -// compatibility with older self-hosted servers that only look at headers. +/** + * Whether `value` can be safely sent as a raw HTTP header value. Header + * values must be ISO-8859-1; filenames often aren't (e.g. macOS decomposes + * accented characters into combining marks that fall outside that range), + * so callers should prefer sending such values as an encoded query param + * instead. + */ function isHeaderSafe(value: string): boolean { return /^[\x20-\x7e\xa0-\xff]*$/.test(value); } @@ -21,6 +23,12 @@ export type DeleteFileResponse = { }; }; +/** + * Uploads `file` to Instant Storage at `path`. `path` and + * `contentDisposition` are sent as encoded query params (and mirrored into + * headers only when ISO-8859-1-safe) so that non-ASCII filenames don't + * break the request. + */ export async function uploadFile({ apiURI, appId, diff --git a/server/src/instant/admin/routes.clj b/server/src/instant/admin/routes.clj index 2038066e3c..cd34b2ec5a 100644 --- a/server/src/instant/admin/routes.clj +++ b/server/src/instant/admin/routes.clj @@ -625,13 +625,14 @@ ;; --- ;; Storage -(defn upload-put [req] +(defn upload-put + "Uploads a file from the request body. `path` (and `content-disposition`) + may arrive either as a query param (preferred, URL-decoded by Ring) or + as a raw header (kept for backwards compatibility with older clients). + Query params take priority since they can carry values headers can't + (e.g. non-ISO-8859-1 filenames)." + [req] (let [{:keys [app-id] :as perms} (get-perms! req :storage/write) - ;; `path` (and `content-disposition`) may arrive either as a query - ;; param (preferred, URL-decoded by Ring) or as a raw header (kept - ;; for backwards compatibility with older clients). Query params - ;; take priority since they can carry values headers can't (e.g. - ;; non-ISO-8859-1 filenames). params (merge (w/keywordize-keys (:headers req)) (:params req)) path (ex/get-param! params [:path] string-util/coerce-non-blank-str) diff --git a/server/src/instant/dash/routes.clj b/server/src/instant/dash/routes.clj index ad549e0495..aa2ca898f6 100644 --- a/server/src/instant/dash/routes.clj +++ b/server/src/instant/dash/routes.clj @@ -1760,15 +1760,16 @@ ;; --- ;; Storage -(defn upload-put [req] +(defn upload-put + "Uploads a file from the request body. `path` may arrive either as a + query param (preferred, URL-decoded by Ring) or as a raw header (kept + for backwards compatibility with older clients). Query params take + priority since they can carry values headers can't (e.g. + non-ISO-8859-1 filenames)." + [req] (let [{{app-id :id} :app} (req->app-accepting-superadmin-or-ref-token! :collaborator :apps/read req) - ;; `path` may arrive either as a query param (preferred, - ;; URL-decoded by Ring) or as a raw header (kept for backwards - ;; compatibility with older clients). Query params take priority - ;; since they can carry values headers can't (e.g. non-ISO-8859-1 - ;; filenames). params (merge (w/keywordize-keys (:headers req)) (:params req)) path (ex/get-param! params [:path] string-util/coerce-non-blank-str) diff --git a/server/src/instant/storage/routes.clj b/server/src/instant/storage/routes.clj index 4847500582..02eade0c55 100644 --- a/server/src/instant/storage/routes.clj +++ b/server/src/instant/storage/routes.clj @@ -26,13 +26,14 @@ :content-length content-length :content-disposition (ex/get-optional-param! params [:content-disposition] string-util/coerce-non-blank-str)})) -(defn upload-put [req] - (let [;; `path` (and `content-disposition`) may arrive either as a query - ;; param (preferred, URL-decoded by Ring) or as a raw header (kept - ;; for backwards compatibility with older clients). Query params - ;; take priority since they can carry values headers can't (e.g. - ;; non-ISO-8859-1 filenames). - params (merge (w/keywordize-keys (:headers req)) +(defn upload-put + "Uploads a file from the request body. `path` (and `content-disposition`) + may arrive either as a query param (preferred, URL-decoded by Ring) or + as a raw header (kept for backwards compatibility with older clients). + Query params take priority since they can carry values headers can't + (e.g. non-ISO-8859-1 filenames)." + [req] + (let [params (merge (w/keywordize-keys (:headers req)) (:params req)) ctx (req->app-file! req params) file (ex/get-param! req [:body] identity) From 9a21e1ae68dc8257ed7c218299e2ca445386efa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonat=C3=A3=20Oliveira?= Date: Mon, 24 Aug 2026 21:09:15 -0300 Subject: [PATCH 3/3] Remove explanatory comments The rationale for the header/query-param split is already in the PR description; the code doesn't need to restate it inline. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01PQ8ju4b8aRBBpVTXsfxdSp --- client/packages/admin/src/index.ts | 9 --------- .../src/components/explorer/inner-explorer.tsx | 15 --------------- client/packages/core/src/StorageAPI.ts | 15 --------------- server/src/instant/admin/routes.clj | 8 +------- server/src/instant/dash/routes.clj | 8 +------- server/src/instant/storage/routes.clj | 8 +------- server/test/instant/admin/routes_test.clj | 3 --- server/test/instant/storage/routes_test.clj | 4 ---- 8 files changed, 3 insertions(+), 67 deletions(-) diff --git a/client/packages/admin/src/index.ts b/client/packages/admin/src/index.ts index d6c265c2de..d4ab71133e 100644 --- a/client/packages/admin/src/index.ts +++ b/client/packages/admin/src/index.ts @@ -845,13 +845,6 @@ const isNodeReadable = (v: any): v is Readable => const isWebReadable = (v: any): v is ReadableStream => v && typeof v.getReader === 'function'; -/** - * Whether `value` can be safely sent as a raw HTTP header value. Header - * values must be ISO-8859-1; filenames often aren't (e.g. macOS decomposes - * accented characters into combining marks that fall outside that range), - * so callers should prefer sending such values as an encoded query param - * instead. - */ function isHeaderSafe(value: string): boolean { return /^[\x20-\x7e\xa0-\xff]*$/.test(value); } @@ -884,8 +877,6 @@ class Storage { const headers = { ...authorizedHeaders(this.config, this.impersonationOpts), }; - // Kept for backwards compatibility with servers that only read `path` - // from a header; the query param below is the source of truth. if (isHeaderSafe(path)) { headers['path'] = path; } diff --git a/client/packages/components/src/components/explorer/inner-explorer.tsx b/client/packages/components/src/components/explorer/inner-explorer.tsx index a37a04d817..6d5dabf33f 100644 --- a/client/packages/components/src/components/explorer/inner-explorer.tsx +++ b/client/packages/components/src/components/explorer/inner-explorer.tsx @@ -1490,23 +1490,10 @@ export async function jsonFetch( : Promise.reject({ status: res.status, body: json }); } -/** - * Whether `value` can be safely sent as a raw HTTP header value. Header - * values must be ISO-8859-1; filenames often aren't (e.g. macOS decomposes - * accented characters into combining marks that fall outside that range), - * so callers should prefer sending such values as an encoded query param - * instead. - */ function isHeaderSafe(value: string): boolean { return /^[\x20-\x7e\xa0-\xff]*$/.test(value); } -/** - * Uploads `file` to the dashboard's storage explorer for `appId`, at either - * `customFilename` or the file's own name. The filename is sent as an - * encoded query param (and mirrored into a header only when - * ISO-8859-1-safe) so that non-ASCII filenames don't break the request. - */ async function upload( token: string, appId: string, @@ -1521,8 +1508,6 @@ async function upload( authorization: `Bearer ${token}`, 'content-type': file.type, }; - // Kept for backwards compatibility with servers that only read `path` - // from a header; the query param below is the source of truth. if (isHeaderSafe(path)) { headers['path'] = path; } diff --git a/client/packages/core/src/StorageAPI.ts b/client/packages/core/src/StorageAPI.ts index 6bf55d5177..da9628ba0c 100644 --- a/client/packages/core/src/StorageAPI.ts +++ b/client/packages/core/src/StorageAPI.ts @@ -1,12 +1,5 @@ import { jsonFetch } from './utils/fetch.js'; -/** - * Whether `value` can be safely sent as a raw HTTP header value. Header - * values must be ISO-8859-1; filenames often aren't (e.g. macOS decomposes - * accented characters into combining marks that fall outside that range), - * so callers should prefer sending such values as an encoded query param - * instead. - */ function isHeaderSafe(value: string): boolean { return /^[\x20-\x7e\xa0-\xff]*$/.test(value); } @@ -23,12 +16,6 @@ export type DeleteFileResponse = { }; }; -/** - * Uploads `file` to Instant Storage at `path`. `path` and - * `contentDisposition` are sent as encoded query params (and mirrored into - * headers only when ISO-8859-1-safe) so that non-ASCII filenames don't - * break the request. - */ export async function uploadFile({ apiURI, appId, @@ -52,8 +39,6 @@ export async function uploadFile({ authorization: `Bearer ${refreshToken}`, 'content-type': contentType || file.type, }; - // Kept for backwards compatibility with servers that only read `path` - // from a header; the query param below is the source of truth. if (isHeaderSafe(path)) { headers['path'] = path; } diff --git a/server/src/instant/admin/routes.clj b/server/src/instant/admin/routes.clj index cd34b2ec5a..b8eb7004ee 100644 --- a/server/src/instant/admin/routes.clj +++ b/server/src/instant/admin/routes.clj @@ -625,13 +625,7 @@ ;; --- ;; Storage -(defn upload-put - "Uploads a file from the request body. `path` (and `content-disposition`) - may arrive either as a query param (preferred, URL-decoded by Ring) or - as a raw header (kept for backwards compatibility with older clients). - Query params take priority since they can carry values headers can't - (e.g. non-ISO-8859-1 filenames)." - [req] +(defn upload-put [req] (let [{:keys [app-id] :as perms} (get-perms! req :storage/write) params (merge (w/keywordize-keys (:headers req)) (:params req)) diff --git a/server/src/instant/dash/routes.clj b/server/src/instant/dash/routes.clj index aa2ca898f6..1a3d357f9d 100644 --- a/server/src/instant/dash/routes.clj +++ b/server/src/instant/dash/routes.clj @@ -1760,13 +1760,7 @@ ;; --- ;; Storage -(defn upload-put - "Uploads a file from the request body. `path` may arrive either as a - query param (preferred, URL-decoded by Ring) or as a raw header (kept - for backwards compatibility with older clients). Query params take - priority since they can carry values headers can't (e.g. - non-ISO-8859-1 filenames)." - [req] +(defn upload-put [req] (let [{{app-id :id} :app} (req->app-accepting-superadmin-or-ref-token! :collaborator :apps/read req) diff --git a/server/src/instant/storage/routes.clj b/server/src/instant/storage/routes.clj index 02eade0c55..84d13e2ae4 100644 --- a/server/src/instant/storage/routes.clj +++ b/server/src/instant/storage/routes.clj @@ -26,13 +26,7 @@ :content-length content-length :content-disposition (ex/get-optional-param! params [:content-disposition] string-util/coerce-non-blank-str)})) -(defn upload-put - "Uploads a file from the request body. `path` (and `content-disposition`) - may arrive either as a query param (preferred, URL-decoded by Ring) or - as a raw header (kept for backwards compatibility with older clients). - Query params take priority since they can carry values headers can't - (e.g. non-ISO-8859-1 filenames)." - [req] +(defn upload-put [req] (let [params (merge (w/keywordize-keys (:headers req)) (:params req)) ctx (req->app-file! req params) diff --git a/server/test/instant/admin/routes_test.clj b/server/test/instant/admin/routes_test.clj index 3c33e036c9..ad0fa927fd 100644 --- a/server/test/instant/admin/routes_test.clj +++ b/server/test/instant/admin/routes_test.clj @@ -1089,9 +1089,6 @@ (is (some? (-> ret :body :data :id))))) (testing "admin can upload a file with a non-ASCII filename via query params" - ;; Filenames with accents (e.g. macOS's NFD-decomposed "café.txt") - ;; can't be sent as raw HTTP header values, so clients send them - ;; as an encoded query param instead. (let [ret (upload-put {:body (make-file-content) :params {:path "café à noite.txt"} diff --git a/server/test/instant/storage/routes_test.clj b/server/test/instant/storage/routes_test.clj index 00b957a639..07aa7a8cda 100644 --- a/server/test/instant/storage/routes_test.clj +++ b/server/test/instant/storage/routes_test.clj @@ -4,10 +4,6 @@ [instant.storage.coordinator :as storage-coordinator])) (deftest upload-put-reads-path-from-query-params - ;; Filenames with accents (e.g. macOS's NFD-decomposed "café.txt") can't - ;; be sent as raw HTTP header values, so clients send them as an encoded - ;; query param (already URL-decoded by Ring's wrap-params) instead. The - ;; route should prefer that over the legacy `path` header. (let [captured-ctx (atom nil) app-id (random-uuid)] (with-redefs [storage-coordinator/upload-file!