Implementing no-transform for Compressed Assets
Problem Statement
You pre-compress JavaScript, CSS, and font files with Brotli at build time and serve them with Content-Encoding: br. Subresource Integrity (SRI) hashes in your HTML are computed against those exact bytes. After deployment you start seeing SRI violations in the browser console and Content-Encoding: gzip in responses — even though your origin never outputs gzip for those assets. An intermediary CDN or mobile carrier gateway is silently transcoding your Brotli payload to gzip before delivery, invalidating your SRI hashes and potentially corrupting binary assets. The no-transform directive exists precisely to stop this.
Prerequisite Concepts
Before working through the steps, make sure you understand:
- no-cache vs no-store: When to Use Each —
no-transformis unrelated to validation or storage; it is easy to conflate it withno-storeunless you already understand what those directives actually do. - Cache-Control Directives & Header Combinations — how multiple directives coexist in a single
Cache-Controlvalue and how caches parse them. - How to Combine Cache-Control Directives Safely — the mechanics of merging directives that govern different aspects of cache behavior (freshness, scope, payload) into one canonical header.
How no-transform Works
RFC 9111 §5.2.2.7 defines no-transform as an instruction to any intermediary (proxy, CDN edge node, gateway) that it must not modify the payload body, Content-Encoding, Content-Range, or Content-Type of a stored or forwarded response. It is a payload-preservation directive — it says nothing about whether the response may be cached, for how long, or by whom.
The scenario it targets: a proxy or CDN receives a response with Content-Encoding: br and decides to convert it to Content-Encoding: gzip for a client that sent Accept-Encoding: gzip without br. Under RFC 9111, an intermediary that is not bound by no-transform is permitted to perform this transcoding. Adding no-transform withdraws that permission.
HTTP/1.1 200 OK
Cache-Control: public, max-age=31536000, immutable, no-transform
Content-Encoding: br
Content-Type: application/javascript
Vary: Accept-Encoding
no-transform is orthogonal to no-cache and no-store. A response can be both fully cached (public, max-age=31536000) and transformation-protected (no-transform) at the same time.
Step-by-Step Resolution
Step 1 — Confirm an intermediary is actually transcoding
Do not add no-transform speculatively. First verify the problem exists:
# Fetch from origin directly (bypassing CDN) — expect br
curl -sI \
-H "Accept-Encoding: br, gzip, deflate" \
https://origin.example.com/assets/app.js \
| grep -iE "^(cache-control|content-encoding|content-length)"
# Fetch through the CDN — compare Content-Encoding and Content-Length
curl -sI \
-H "Accept-Encoding: br, gzip, deflate" \
https://example.com/assets/app.js \
| grep -iE "^(cache-control|content-encoding|content-length)"
A Content-Encoding that changes from br to gzip between the two responses, or a Content-Length that differs, confirms transcoding by an intermediary.
Also open browser DevTools > Console. An SRI failure message such as Failed to find a valid digest in the 'integrity' attribute for resource 'https://example.com/assets/app.js' is direct evidence of byte-level modification.
Step 2 — Add no-transform to your Cache-Control header
Add no-transform as an additional directive in your existing header, not as a replacement. Choose the configuration block that matches your stack:
Nginx (static asset location block):
location /assets/ {
add_header Cache-Control "public, max-age=31536000, immutable, no-transform" always;
}
Apache (.htaccess or <VirtualHost>):
Header set Cache-Control "public, max-age=31536000, immutable, no-transform"
Varnish VCL (emitting from the backend response):
sub vcl_backend_response {
if (bereq.url ~ "^/assets/") {
set beresp.http.Cache-Control = "public, max-age=31536000, immutable, no-transform";
}
}
Cloudflare Worker (adding the directive without rebuilding the full value):
export default {
async fetch(request, env) {
const response = await env.ASSETS.fetch(request);
const modified = new Response(response.body, response);
modified.headers.set(
"Cache-Control",
"public, max-age=31536000, immutable, no-transform"
);
return modified;
}
};
Keep all directives in a single Cache-Control value. Duplicate Cache-Control headers are merged by RFC 9111’s comma-join rule, but some CDN implementations apply first-match-wins, producing non-deterministic results.
Step 3 — Disable platform-level transformation features
no-transform instructs compliant HTTP intermediaries. CDN features that operate at the application or configuration layer — outside the HTTP caching model — do not read Cache-Control at all. You must disable them separately:
- Cloudflare Auto Minify: Speed > Optimization > Content Optimization > Auto Minify. Uncheck JavaScript, CSS, and HTML. Alternatively, create a Transform Rule that bypasses minification for
/assets/*. - Cloudflare Image Optimization / Polish: Speed > Optimization > Image Optimization. Set Polish to “Off” for paths serving pre-compressed binary assets.
- Fastly on-the-fly image resizing: In your VCL, unset the
X-Fastly-Imageopto-Apirequest header for the/assets/prefix, or set a Snippet condition that skips the Image Optimizer. - AWS CloudFront Lambda@Edge transformers: Remove or scope any response-modifying Lambda that touches
Content-Encodingor the body for your static asset paths.
Without this step, a CDN may silently transcode payloads even when no-transform is correctly set in the header.
Step 4 — Verify across all hops
After deployment, check each network hop in order:
Origin response:
curl -sI \
-H "Accept-Encoding: br, gzip" \
https://origin.example.com/assets/app.js \
| grep -iE "^(cache-control|content-encoding|vary)"
Expected:
cache-control: public, max-age=31536000, immutable, no-transform
content-encoding: br
vary: Accept-Encoding
CDN edge response (same command against the public hostname):
curl -sI \
-H "Accept-Encoding: br, gzip" \
https://example.com/assets/app.js \
| grep -iE "^(cache-control|content-encoding|vary)"
Both cache-control and content-encoding must match the origin response exactly. If a CDN edge strips no-transform, investigate its header normalisation settings.
SRI hash re-verification — recompute the hash directly from the delivered bytes:
curl -s https://example.com/assets/app.js \
| openssl dgst -sha384 -binary \
| openssl base64 -A
The output must match the integrity attribute in your HTML. A mismatch after adding no-transform and disabling platform features indicates a remaining transformation hop.
Expected Output / Verification
A correctly configured asset delivers:
HTTP/2 200
cache-control: public, max-age=31536000, immutable, no-transform
content-encoding: br
content-type: application/javascript
vary: Accept-Encoding
In browser DevTools (Network tab), select the asset, open the Response Headers panel, and confirm:
cache-controlcontainsno-transformcontent-encodingmatchesbr(or whichever format your origin pre-compresses)content-lengthmatches the on-disk Brotli file size
The Console should show no SRI errors. On a warm cache hit, the age response header will be non-zero, confirming the CDN is serving from cache rather than hitting origin — and content-encoding will still be br, proving the cached payload was not transcoded before storage.
Edge Cases
-
HTTP/1.1 transparent proxies stripping
no-transform: Some HTTP/1.1 proxies (particularly enterprise gateways) strip unrecognisedCache-Controlextensions before forwarding. RFC 9111 requires compliant intermediaries to preserve the directive, but a non-compliant proxy will silently discard it. Test header round-trips through every network segment usingcurl --proxy, not just end-to-end. -
Vary: Accept-Encodingwith multiple cached variants: When usingVary: Accept-Encodingwithout fragmenting your cache, a CDN that holds a Brotli variant must serve it as-is to any client that acceptsbr.no-transformprevents the CDN from converting the storedbrvariant togzipfor a client that only sentAccept-Encoding: gzip— instead the CDN must fetch the gzip variant from origin or return an error. Ensure your origin serves all encoding variants you declare inVary. -
Pre-compressed fonts and WOFF2: Browsers apply strict
Content-Typevalidation for fonts. If a proxy transcodes a WOFF2 file’s encoding and changesContent-Type, the font is rejected entirely.no-transformprotects font delivery the same way it protects JavaScript, but fontContent-Typemismatches produce silent fallback rather than console errors — harder to diagnose without header inspection. -
HTTP/2 vs HTTP/1.1 compression negotiation: HTTP/2 servers and CDNs sometimes apply different compression defaults per protocol version. Verify
no-transformbehaviour separately for HTTP/2 connections. The directive operates at the HTTP layer regardless of protocol version, but platform-level features that bypass it may behave differently on HTTP/2 paths.
FAQ
Does no-transform prevent caching?
No. no-transform is a payload-preservation directive only. A cache that receives it still stores and serves the response normally; it just cannot modify the body, Content-Encoding, or Content-Type in the process. Combine it freely with public, max-age, or immutable — they govern separate aspects of cache behaviour.
Does no-transform stop Cloudflare Auto Minify?
Not by itself. Platform-level features like Auto Minify operate at the application layer, outside the HTTP caching model defined by RFC 9111. You must disable them separately via the Cloudflare dashboard or Transform Rules. The origin header alone is insufficient for platform-managed transformations.
How does no-transform interact with Vary: Accept-Encoding?
The two directives are orthogonal. Vary: Accept-Encoding tells caches to store separate variants per encoding. no-transform tells caches they cannot transcode between those variants on the fly. Both should be present when serving pre-compressed assets: Vary drives correct cache keying; no-transform preserves the integrity of each variant.
Related
- Using
Vary: Accept-EncodingWithout Fragmenting Cache explains how to pairVarywith correct CDN cache key configuration so encoding variants are stored and served efficiently. - How to Combine Cache-Control Directives Safely covers the rules for assembling multiple directives — freshness, scope, and payload-preservation — into a single valid
Cache-Controlvalue. - How CDN Cache Keys Are Generated details how CDNs build cache keys from request headers, which determines whether a
no-transform-protected variant is stored and looked up correctly.