Skip to content

Restore Content-Type emission for S3 SigV4 requests under EMSCRIPTEN#352

Open
konstantinoscs wants to merge 1 commit into
duckdb:mainfrom
konstantinoscs:fix-wasm-s3-put
Open

Restore Content-Type emission for S3 SigV4 requests under EMSCRIPTEN#352
konstantinoscs wants to merge 1 commit into
duckdb:mainfrom
konstantinoscs:fix-wasm-s3-put

Conversation

@konstantinoscs

@konstantinoscs konstantinoscs commented Jun 26, 2026

Copy link
Copy Markdown

Fixes #351.

Summary

Restore Content-Type header emission for S3 SigV4 requests under EMSCRIPTEN.

CreateS3Header signs content-type when a content type is provided, including the application/octet-stream used by S3 PUT. After #234, that value is intentionally not emitted into the header map so native curl can keep its existing default/override behavior. In DuckDB-Wasm, however, the header map is what HTTPWasmClient forwards to XMLHttpRequest, so the browser request can be missing a header that was included in SignedHeaders, producing AWS S3 403 SignatureDoesNotMatch. Only writes are affected — GET/HEAD pass an empty content type, so it is neither signed nor sent, which is why reads work and writes fail.

This change keeps native behavior unchanged and restores the previous WASM-specific emission from #105.

Testing

  • Native: unchanged by construction. The #else branch is byte-identical to the current code, so the existing MinIO S3 suite is unaffected (regression guard).
  • DuckDB-Wasm + real S3 — validated end-to-end. Built httpfs for wasm_eh against DuckDB v1.5.4 (emsdk 3.1.71) and loaded it into a DuckDB-Wasm app via INSTALL httpfs FROM '<local-repo>' with SET allow_unsigned_extensions=true. The fix lives in s3fs.cpp, so it applies identically to the mvp/threads variants.
    • Before (stock httpfs): a browser→S3-direct Iceberg INSERT failed with 403 SignatureDoesNotMatch on the data-file PUT.
    • After (patched httpfs): the same INSERT succeeded against real S3 — the PUT returned 200. In DevTools the request now carries Content-Type: application/octet-stream, matching the content-type already listed in Authorization's SignedHeaders=... (i.e. signed == sent).
    • Isolated equivalent for reviewers (no catalog required), which exercises the same PUT path: COPY (SELECT 1) TO 's3://<bucket>/<prefix>/probe.parquet' (FORMAT parquet).

From a web app I'm building that uses duckdb WASM to attach to a Polaris backed Catalog:

  1. (Pre-fix) Reads work correctly:
image
  1. (Pre-fix) Writes error (ignore the wrong error message heh)
image with the 403 response saying:
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>SignatureDoesNotMatch</Code><Message>The request signature we calculated does not match the signature you provided. Check your key and signing method.</Message></Error>
  1. (After-fix) Writes succeed
image
  1. (After-fix) Read verifies new row is there
image

Notes

The duckdb-httpfs native MinIO CI is not expected to exercise this branch because the behavior is guarded by EMSCRIPTEN. There is also currently no CI harness that runs SQL through the WASM HTTP transport, so this path was validated manually in a browser against real S3 (above) rather than by an automated test.

@konstantinoscs konstantinoscs marked this pull request as ready for review June 26, 2026 18:42
On Emscripten, S3 PUT/POST sign `content-type: application/octet-stream` into
the SigV4 canonical request and SignedHeaders, but DuckDB-Wasm's XHR transport
does not send a Content-Type for a Uint8Array body. S3 recomputes a different
signature and rejects with `403 SignatureDoesNotMatch`. Reads (GET/HEAD, no
body) are unaffected.

Native curl/httplib re-add the default Content-Type, so signed == sent there.
Emit the signed Content-Type into the header map under EMSCRIPTEN so the wire
value matches what was signed -- restoring the behavior of duckdb#105, which was
regressed by the `!= "application/octet-stream"` guard introduced in duckdb#234.
Native behavior is unchanged.
@konstantinoscs

Copy link
Copy Markdown
Author

@carlopi I see that historically you are active on this area. Can you take a look?

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.

WASM S3 PUT signs a Content-Type it never sends → 403 SignatureDoesNotMatch (regression of #105 via #234)

1 participant