Skip to content

fix(storage): derive content type for signed URL uploads - #1158

Open
AndroidPoet wants to merge 2 commits into
supabase:mainfrom
AndroidPoet:fix/storage-signed-upload-content-type
Open

fix(storage): derive content type for signed URL uploads#1158
AndroidPoet wants to merge 2 commits into
supabase:mainfrom
AndroidPoet:fix/storage-signed-upload-content-type

Conversation

@AndroidPoet

Copy link
Copy Markdown
Contributor

Problem

uploadToSignedURL stamps Content-Type: text/plain;charset=UTF-8 on every upload when options is omitted, so a PNG or JPEG sent through a signed upload URL is stored with that mimetype and served back as text. Browsers then download the file instead of rendering it, and <img src> breaks.

Both public overloads default options to nil:

public func uploadToSignedURL(_ path: String, token: String, data: Data, options: FileOptions? = nil)
public func uploadToSignedURL(_ path: String, token: String, fileURL: URL, options: FileOptions? = nil)

and _uploadToSignedURL resolved that nil against defaultFileOptions, whose contentType is "text/plain;charset=UTF-8". Because that value is never nil, the extension-derived fallback on the encode path could not run:

mimeType: options.contentType ?? mimeType(forPathExtension: path.pathExtension)

Fix

Resolve the missing options against FileOptions() instead. cacheControl ("3600") and upsert (false) are identical between the two, so the only change is contentType, which becomes nil and lets the path extension decide, as the property already documents:

The Content-Type header value, e.g. "image/png". When nil, the type is inferred from the file extension.

Four things line up behind this:

  • upload() and update() already default to FileOptions(). I confirmed by capturing the request body that upload("file.txt", data:) emits bare Content-Type: text/plain, which is exactly what uploadToSignedURL emits after this change. No new behavior is introduced anywhere in the SDK, a divergence is removed.
  • The deprecated uploadToSignedURL overloads in Deprecated.swift pass FileOptions() and are therefore already correct. Moving off the deprecated API silently changed the stored mimetype.
  • storage-js applies DEFAULT_FILE_OPTIONS.contentType only on the raw body branch (headers['content-type'] = options.contentType). On the Blob/FormData branch, the one this SDK always takes, it appends only cacheControl and metadata and lets the file's own type become the part's Content-Type. See StorageFileApi.ts.
  • fix(storage): honor options.contentType for fileURL uploads #1124 added the ?? mimeType(forPathExtension:) fallback for fileURL uploads, but on this path defaultFileOptions made it unreachable.

One consequence worth naming: .txt uploads through a signed URL no longer carry ;charset=UTF-8. That matches what upload() has always sent for the same file, so it is alignment rather than a regression.

defaultFileOptions is left in place because _uploadOrUpdate still references it, though every caller there passes a non-optional FileOptions, so that fallback is already unreachable. Happy to remove the constant in a follow-up if you would prefer.

Tests

Two tests asserting the emitted Content-Type for a .png (data) and a .jpg (fileURL) upload with options omitted. Both fail on main with 4 failed expectations and pass with the fix. 147 StorageTests green.

They capture the request body through StorageHTTPSession rather than using snapshotRequest, because snapshotRequest assertions in this suite do not currently fail. I changed an existing expected value to Content-Type: totally/bogus and uploadToSignedURL() still passed, so the request-shape snapshots in StorageFileAPITests are not gating anything since the Swift Testing migration. That looked worth reporting separately rather than working around silently, and it is why these two tests assert directly on the body.

The existing uploadToSignedURL() snapshot is updated to the values this change produces (text/plain, Content-Length 297 to 283, verified by capturing the real body).

No public API change.

uploadToSignedURL fell back to defaultFileOptions when no options were
passed, and that constant carries contentType "text/plain;charset=UTF-8".
The fallback to the path extension could therefore never run, so a PNG or
JPEG uploaded through a signed URL was stored and served as text/plain.

upload() and update() already default to FileOptions(), whose contentType is
nil so the extension is used, and the deprecated uploadToSignedURL overloads
pass FileOptions() too. Use FileOptions() here as well. cacheControl and
upsert are identical between the two, so only contentType changes.

The two new tests capture the request body through StorageHTTPSession rather
than snapshotRequest, so they fail when the emitted content type is wrong.
@AndroidPoet
AndroidPoet requested review from a team and grdsdev as code owners July 30, 2026 10:09
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes
    • Improved uploads to pre-signed URLs by applying consistent default request settings.
    • Multipart uploads now automatically derive the correct content type from the destination path or source file when no options are provided.
    • Updated multipart request handling for more accurate content length and content type values.

Walkthrough

_uploadToSignedURL now uses plain FileOptions() when no upload options are supplied. Storage tests capture multipart request bodies, update expected headers, and verify content types derived from .png paths and .jpg file URLs.

Possibly related PRs

Suggested labels: Storage


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/Storage/StorageFileApi.swift`:
- Line 958: Update the uploadToSignedURLCleansPath snapshot to expect
Content-Length 283 and Content-Type text/plain for omitted FileOptions, while
preserving the existing signed-upload behavior and other snapshot assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5f754df1-32a3-4930-b330-fea49b2ef467

📥 Commits

Reviewing files that changed from the base of the PR and between b118484 and 425b535.

📒 Files selected for processing (2)
  • Sources/Storage/StorageFileApi.swift
  • Tests/StorageTests/StorageFileAPITests.swift

options: FileOptions?
) async throws -> SignedURLUploadResponse {
let options = options ?? defaultFileOptions
let options = options ?? FileOptions()

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.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Update the remaining signed-upload snapshot.

This fallback affects all signed uploads with omitted options. The existing uploadToSignedURLCleansPath snapshot at Lines 1393–1404 still expects Content-Length: 297 and Content-Type: text/plain;charset=UTF-8; with this change it should expect 283 and text/plain, so the test will fail in CI.

Proposed snapshot update
-        	--header "Content-Length: 297" \
+        	--header "Content-Length: 283" \
...
-        Content-Type: text/plain;charset=UTF-8\`#r`
+        Content-Type: text/plain\`#r`
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Storage/StorageFileApi.swift` at line 958, Update the
uploadToSignedURLCleansPath snapshot to expect Content-Length 283 and
Content-Type text/plain for omitted FileOptions, while preserving the existing
signed-upload behavior and other snapshot assertions.

@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 30533615360

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Coverage increased (+0.01%) to 84.037%

Details

  • Coverage increased (+0.01%) from the base build.
  • Patch coverage: 1 of 1 lines across 1 file are fully covered (100%).
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 10042
Covered Lines: 8439
Line Coverage: 84.04%
Coverage Strength: 39.69 hits per line

💛 - Coveralls

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.

2 participants