Skip to content

fix(cf-workers): mark forwarded object responses no-transform - #150

Merged
alukach merged 4 commits into
mainfrom
fix/cf-no-transform
Sep 25, 2026
Merged

alukach merged 4 commits into
mainfrom
fix/cf-no-transform

Conversation

@alukach

@alukach alukach commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

What I'm changing

The Preview smoke tests TestRangeRequests::test_head_includes_accept_ranges and test_range_after_full_get_still_returns_206 have failed on every branch since early August (the last green run was 2026-07-28). tests/smoke/ hasn't changed since June.

Cloudflare now gzips full-object Worker responses on the fly for compressible types such as text/markdown, whenever the client sends Accept-Encoding: gzip. Python requests, browsers and many HTTP clients send that header by default. Compressing the body strips Content-Length and Accept-Ranges. Range responses (206) are never compressed, so only full GET/HEAD responses are affected.

This is user-visible, not only a test problem. Any client that plans downloads from a HEAD, such as aws s3 cp or anything that splits a download into ranges, gets no size or range support for text objects. A proxy should return object bytes and headers as the backend sent them. I reproduced it against the #145 preview deployment:

Request content-length accept-ranges content-encoding
HEAD, no Accept-Encoding 5555 bytes —
HEAD, Accept-Encoding: gzip, deflate missing missing gzip

How I did it

  • crates/cf-workers/src/response.rs: response_from_forward calls a new set_no_transform before building the web_sys::Response. It appends no-transform to Cache-Control and keeps any directives the backend returned (such as max-age). If the directive is already present, it does nothing; if there's no Cache-Control, it sets no-transform. Cloudflare doesn't compress responses marked no-transform.
  • Scope: only the Forward path (streamed object bodies) changes. response_from_proxy_result, which serves small LIST/XML/error bodies, is left alone because compressing those is harmless.
  • docs/deployment/cloudflare-workers.md: new "Response Handling" section. It explains the header, notes that compression also weakens the ETag, and shows integrators how to opt out by deleting cache-control from the response into_web_sys() returns. The opt-out is a documented header edit rather than a config option; I didn't add a config option because no one has asked for one.

Test plan

  • cargo check -p multistore-cf-workers --target wasm32-unknown-unknown
  • cargo clippy -p multistore-cf-workers --target wasm32-unknown-unknown
  • cargo check, cargo fmt
  • The Preview smoke tests on this PR's deployment pass TestRangeRequests. These two tests are the failing regression tests: multistore-cf-workers doesn't build for the host target, so there's no native unit test for set_no_transform.

🤖 Generated with Claude Code

alukach and others added 2 commits September 24, 2026 22:24
Cloudflare gzips compressible content types (e.g. text/markdown) on the fly
for clients sending Accept-Encoding: gzip, stripping Content-Length and
Accept-Ranges from full-object GET/HEAD responses. The Preview smoke tests
`test_head_includes_accept_ranges` and
`test_range_after_full_get_still_returns_206` have failed on every branch
since early August because of it.

Add `no-transform` to Cache-Control on forwarded responses so the body is
passed through byte-for-byte, keeping any backend Cache-Control directives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@claude

claude Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Claude finished @alukach's task in 1m 33s —— View job


✅ No blocking issues — safe to merge.

Reviewed crates/cf-workers/src/response.rs, the docs addition, and the integration test change against origin/main...HEAD.

  • set_no_transform (crates/cf-workers/src/response.rs:45-65) correctly handles the cases that matter: existing no-transform directive (no duplicate append, matched via per-directive eq_ignore_ascii_case rather than a substring check, so "public-no-transform"-style false positives are avoided), empty/missing Cache-Control (sets bare no-transform), and non-empty existing value (appends , no-transform, preserving backend directives like max-age=3600). This matches the PR description and the updated assertion in tests/integration/test_integration.py:180-183.
  • Scope is correctly limited to response_from_forward (the streamed object-body path); response_from_proxy_result (buffered LIST/XML/error bodies) is untouched, matching the stated rationale that compressing small buffered bodies is harmless.
  • Confirmed tests/integration/test_integration.py runs against the real CF Workers dev server (per its module docstring: "Requires CF Workers (wrangler dev)..."), so the updated CacheControl assertion is exercising the actual code path, not a mismatched test target.
  • Docs snippet (docs/deployment/cloudflare-workers.md) using gateway.handle_request(...).await.into_web_sys() matches the real GatewayResponseExt::into_web_sys trait and the usage in examples/cf-workers/src/lib.rs:118-122 — accurate.
  • Cargo.lock diff is just the workspace version bump (0.7.1 → 0.7.2), unrelated noise, not a concern.

Minor, non-blocking observations:

  • set_no_transform uses HeaderMap::get/insert, which only sees/replaces a single Cache-Control header value; if a backend ever sent multiple separate Cache-Control header lines (unusual, but legal HTTP), only the first would be preserved. Not worth guarding against given how backends here behave.
  • The if let Ok(v) = value.parse() silently no-ops if constructing the new header value ever failed. In practice this can't happen (appending ASCII , no-transform to an already-valid HeaderValue can't produce an invalid one), so this is fine as-is.

Simplify (ponytail)

Nothing to flag — set_no_transform is a handful of lines with no external dependency available for Cache-Control directive parsing in this crate; hand-rolling the split/trim is already the minimal approach.


💰 Estimated review cost: $0.31 · 1m32s · 15 turns

@github-actions github-actions Bot added the fix label Sep 25, 2026
@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

📖 Docs preview deployed to https://multistore-docs-pr-150.development-seed.workers.dev

  • Date: 2026-09-25T05:42:54Z
  • Commit: 434f3f1

@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

🚀 Latest commit deployed to https://multistore-proxy-pr-150.development-seed.workers.dev

  • Date: 2026-09-25T05:42:54Z
  • Commit: 434f3f1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Forwarded responses now append no-transform; assert the stored max-age
directive survives alongside it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@alukach
alukach marked this pull request as ready for review September 25, 2026 05:49
@alukach
alukach merged commit 662314b into main Sep 25, 2026
19 of 20 checks passed
@alukach
alukach deleted the fix/cf-no-transform branch September 25, 2026 05:49

This branch was successfully deployed

1 active deployment
preview — 5000329e Deployed Sep 25, 2026 by alukach via Deploy & Test / Deploy #427
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant