Request/Response Transformation
Request/Response transformation at the edge intercepts, rewrites, and routes HTTP traffic in the PoP nearest the client, mutating requests before cache lookup or origin fetch and reshaping responses on the way back.
Key Implementation Points:
- The transformation layer sits between DNS resolution and the origin, so request mutations run before cache evaluation and response mutations run after the origin replies — ordering that governs caching, security, and routing correctness across Edge Routing & Serverless Function Architecture.
- Core use cases are header injection and normalization, URL and path rewriting, payload sanitization, body streaming, protocol fallback, and dynamic routing on client attributes.
- Two execution models exist: declarative CDN rule engines (fast, low-code, no compute budget) and programmatic edge compute (V8 isolates or WebAssembly) for logic-driven transforms — each with different latency, memory, and debugging profiles.
- Any transform that varies output by header, cookie, or geo must be reflected in the cache key or a
Varydirective, or the cache will serve one user’s transformed payload to everyone.
The sections below trace the interception lifecycle, then walk provider-specific implementations for Cloudflare, AWS, Fastly, and Vercel, a comparison matrix, a numbered deployment procedure, the caching and propagation implications, and a troubleshooting and rollback playbook.
Edge Interception Lifecycle & Architecture
Transformations occur at precise stages within the DNS-to-origin request/response cycle. Traffic flow follows a strict sequence: DNS resolution routes the client to the nearest edge PoP, where the transformation layer evaluates rules before cache lookup. Understanding where in this pipeline a mutation runs is the difference between a correct deployment and one that poisons caches or breaks routing.
Request transformations execute prior to cache evaluation and origin fetch. This means a path rewrite in the request phase changes the cache key the edge computes — rewriting /api/v1 to /de/api/v1 makes the edge look up (and store) a different cache entry. Response transformations execute immediately after the origin returns a payload but, critically, before the edge writes the object to cache. A response-body mutation applied before cache store is persisted; one applied after a cache read on a hit may run on already-mutated content, double-transforming it. This ordering is the single most common source of edge transformation bugs.
Latency overhead remains sub-millisecond for header and path modifications because they manipulate metadata, not payload bytes. Processing scales linearly when parsing or streaming response bodies — every byte must pass through your decoder/encoder. Memory constraints at the PoP level require strict chunking for payloads exceeding roughly 1MB; buffering a full multi-megabyte body in an isolate risks an out-of-memory eviction that drops the connection.
| Stage | Execution Timing | Primary Impact |
|---|---|---|
| DNS Resolution | Pre-Edge | PoP selection and routing |
| Request Transform | Pre-Cache | Header injection, path rewriting, cache-key shaping |
| Cache Evaluation | Mid-Flow | Hit/miss determination |
| Origin Fetch | Conditional (miss only) | Backend data retrieval |
| Response Transform | Post-Origin, Pre-Store | Body streaming, header rewrite, protocol fallback |
| Cache Store | Post-Transform | Object persisted with key + Vary |
The transformation layer is also the natural place to enforce cross-cutting concerns that would otherwise be duplicated per-origin: normalizing inbound headers, injecting trace IDs, and stripping hop-by-hop directives. For deeper coverage of how edge runtimes lowercase, merge, and strip headers, see Modifying request headers at the CDN edge layer.
Declarative CDN Rule Engine Configuration
Declarative engines provide low-code transformation via native dashboards or REST APIs, with no compute budget consumed. You define match conditions using URL paths, headers, cookies, or geo-IP attributes, then apply rewrite, redirect, or header-modification actions to matched traffic. Because the rule engine runs in the proxy’s hot path rather than a sandbox, it adds effectively zero CPU time and is the right tool whenever the logic is expressible as a static condition-action pair.
Rule execution order dictates precedence. Higher-priority rules override lower ones, and a terminate/final flag stops evaluation. Always validate configurations in staging or dry-run mode before pushing to production — misordered rules cause routing loops or header collisions where a later rule re-adds a header a prior rule removed. Keep rules idempotent so re-evaluation is safe.
Cache behavior requires explicit definition. Transformations that vary output by a request attribute must include a Vary directive or fold that attribute into the cache key, or the engine serves the first user’s transformed content to everyone. This interaction with caching is shared with Edge Compression & Asset Optimization, where compressing by Accept-Encoding without a Vary: Accept-Encoding directive corrupts caches in exactly the same way.
# Cloudflare API — create or update a Transform Rules ruleset
curl -X PUT "https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/rulesets/phases/http_request_late_transform/entrypoint" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
-d @transform-rules.json
Rollback protocol: Maintain a versioned ruleset backup. Every Cloudflare ruleset update returns a version field; revert by re-PUTting the prior payload or by DELETEing the offending rule by id. Disable the rule immediately if cache fragmentation or 5xx rates spike. Because declarative rules carry no deploy artifact, rollback is a single API call and propagates globally in seconds.
Programmatic Edge Compute Transformation
Serverless edge functions enable dynamic, logic-driven transformations that a rule engine cannot express — JWT inspection, body parsing, conditional origin selection. You leverage the standard Fetch API to intercept Request and Response objects, modify their properties, and return transformed payloads, optionally without involving the origin at all.
Framework-specific routing patterns dictate execution boundaries. Cloudflare Workers Routing enables granular path-based middleware execution with strict CPU limits. Vercel Edge Middleware provides a standardized NextResponse.rewrite() and NextResponse.next() API for seamless framework integration, while AWS exposes both Lambda@Edge (Node/Python, viewer/origin event hooks) and the lighter CloudFront Functions (JavaScript, viewer-only, sub-millisecond).
Cloudflare Workers
Workers run on V8 isolates with a generous CPU budget on paid plans (50ms on Bundled, higher on Unbound) and 10ms on the free tier. The fetch handler returns a transformed Response:
export default {
async fetch(request, env, ctx) {
// Request transform: normalize and inject before origin fetch
const url = new URL(request.url);
const req = new Request(url, request);
req.headers.set("X-Edge-Transformed", "true");
const originResponse = await fetch(req);
// Response transform: rewrite a header, leave body untouched
const res = new Response(originResponse.body, originResponse);
res.headers.set("X-Cache-Tier", "edge");
res.headers.delete("Server");
return res;
},
};
AWS Lambda@Edge
Lambda@Edge attaches to CloudFront at four event points. An origin-request handler can rewrite the URI before the origin fetch:
'use strict';
exports.handler = async (event) => {
const request = event.Records[0].cf.request;
const country = (request.headers['cloudfront-viewer-country'] || [{ value: 'US' }])[0].value;
if (country === 'DE') {
request.uri = '/de' + request.uri;
}
request.headers['x-edge-transformed'] = [{ key: 'X-Edge-Transformed', value: 'true' }];
return request;
};
Fastly Compute@Edge
Fastly compiles to WebAssembly with higher memory ceilings than V8 isolates, making it well-suited to body-heavy transforms:
/// <reference types="@fastly/js-compute" />
addEventListener("fetch", (event) => event.respondWith(handle(event)));
async function handle(event) {
const req = event.request;
req.headers.set("X-Edge-Transformed", "true");
const res = await fetch(req, { backend: "origin_0" });
res.headers.set("X-Cache-Tier", "edge");
return res;
}
# Local development across runtimes
npx wrangler dev # Cloudflare Workers
npx vercel dev # Vercel Edge Middleware
fastly compute serve # Fastly Compute@Edge
sam local invoke # Lambda@Edge (via SAM)
Failover strategy: Implement a circuit-breaker pattern. Catch runtime exceptions and return NextResponse.next(), the untouched origin response, or a fallback static response rather than a 500. Log the failure and bypass transformation for the affected route until patched — a transform that throws should fail open to origin, never closed.
Streaming Body & Protocol Transformation
Large payloads require streaming architectures to avoid memory exhaustion. Never buffer entire response bodies in edge compute environments. Use TransformStream to process data in chunks as it flows from origin to client, which keeps peak memory bounded by chunk size rather than payload size.
Validate Content-Type before parsing. Malformed JSON, XML, or binary payloads trigger immediate runtime exceptions; implement strict type checking and fall back to pass-through streaming when validation fails. When you transform a compressed body you must either decode it first or skip the transform — attempting a regex over gzipped bytes silently corrupts output. Coordinate with Edge Compression & Asset Optimization so compression runs after any body mutation, and ensure the runtime recomputes Content-Length or switches to chunked Transfer-Encoding.
const transformStream = new TransformStream({
transform(chunk, controller) {
const text = new TextDecoder().decode(chunk);
const modified = text.replace(/"status":"active"/g, '"status":"verified"');
controller.enqueue(new TextEncoder().encode(modified));
},
});
// Pipe origin body through the transform without buffering
const transformedResponse = new Response(
originalResponse.body.pipeThrough(transformStream),
{ headers: originalResponse.headers },
);
A subtle hazard: a regex applied per chunk can miss matches that straddle a chunk boundary. For correctness on boundary-sensitive replacements, accumulate a small overlap window or use a streaming parser rather than naive replace.
Protocol translation requires explicit header manipulation. Converting HTTP/3 responses to HTTP/1.1 fallbacks demands stripping QUIC-specific and alt-svc headers and adjusting connection-pooling parameters. Verify downstream proxy compatibility before enabling protocol downgrades, and never copy hop-by-hop headers (Connection, Keep-Alive, Transfer-Encoding) from origin to client verbatim.
Platform Comparison
| Provider | Mechanism | Wire behavior | Failover / Notes |
|---|---|---|---|
| Cloudflare Transform Rules | Declarative ruleset (Wirefilter) | Header/URI rewrite in proxy hot path, zero CPU | Revert by re-PUTting prior version; global in seconds |
| Cloudflare Workers | V8 isolate, Fetch API | Full request/response control, streaming | ctx.waitUntil; fail open to origin on throw |
| AWS Lambda@Edge | Node/Python, 4 CloudFront events | Viewer/origin request and response hooks | Cold starts; 5s viewer / 30s origin timeout |
| AWS CloudFront Functions | JS, viewer-only | Header/URI rewrite, sub-ms, no body access | Lightest option; no network or body I/O |
| Fastly Compute@Edge | WebAssembly | High memory ceiling, strong streaming | fastly compute serve for local; backend bindings |
| Vercel Edge Middleware | V8 isolate, Next.js API | rewrite() / next() / redirect() |
Framework-scoped matchers; fail with next() |
Numbered Deployment Procedure
- Classify the transform. If it is a static condition-action (add header, rewrite path, redirect by country), use a declarative rule engine. If it needs logic, body access, or external lookups, use edge compute.
- Pin the execution phase. Decide request-phase (pre-cache, shapes the cache key) versus response-phase (post-origin, pre-store). Document which cache key results.
- Define cache semantics first. Add the varying attribute to the cache key or emit
Vary. Do this before shipping the transform, not after a cache-poisoning incident. - Build and test locally with
wrangler dev,vercel dev, orfastly compute serve. Assert expected output headers and body withcurl -v. - Deploy to a staging route or a dry-run ruleset. Compare origin response against edge output to isolate drift.
- Canary a small traffic slice using a percentage rule or a header-gated route. Watch 5xx rate, p99 latency, and cache hit ratio.
- Promote to 100% once metrics are flat. Keep the prior ruleset version or Worker deployment ID recorded for one-command rollback.
TTL, Caching & Propagation Implications
Request-phase rewrites change the cache key. Two clients hitting /api/v1 who get rewritten to /de/api/v1 and /fr/api/v1 populate two distinct cache objects with independent TTLs — desirable for localization, catastrophic if the split is unintentional and fragments your hit ratio. Audit every request rewrite for its cache-key effect.
Response-phase body mutations are cached as transformed. If you later change the transform logic, already-cached objects retain the old mutation until their TTL expires; you must purge to force regeneration. Header-only response transforms behave differently per platform: Cloudflare Transform Rules apply on every response including cache hits, whereas a body mutation inside a Worker only runs on the path that produced the Response object.
Declarative rule changes propagate across the edge in seconds because they carry no build artifact. Edge-compute deployments propagate as the new version rolls out to PoPs, typically under a minute, but in-flight requests may briefly straddle versions — make transforms backward-compatible so mixed-version traffic stays correct. When a transform feeds into a routing decision, the same propagation discipline used in Cloudflare Workers Routing applies: deploy additively, verify, then remove the old path.
Production Debugging & Observability
Trace transformation execution by injecting diagnostic headers. Append X-Edge-Transform-Status and X-Transform-Route to verify which rule or branch executed, and compare origin responses against edge outputs to isolate drift. Strip these debug headers before serving to real users or gate them behind a secret request header.
Stream edge logs to capture execution errors, bypass flags, and cache hit/miss ratios. Filter by transform_id or worker_version to pinpoint regressions, and correlate latency spikes with body-parsing operations — a p99 jump that tracks payload size almost always means a buffering transform that should be streaming.
# Inspect transform output and confirm length recalculation
curl -v -H 'X-Debug: edge-transform' https://example.com/api/v1/status \
| grep -E 'X-Edge|X-Transform|HTTP/|Content-Length|Transfer-Encoding'
# Tail Cloudflare Worker logs filtered to the deployed version
npx wrangler tail --format pretty --status error
When a path rewrite misbehaves, cross-check it against the patterns in Rewriting URLs and paths at the edge, since most “transform not applying” reports trace back to a mismatched matcher or a rewrite that the cache key did not account for.
Troubleshooting & Rollback
| Symptom | Likely cause | Fix |
|---|---|---|
| Transform not applying | Rule priority / matcher mismatch | Verify expression against raw request; check rule order and final flags |
| Wrong content served to many users | Varying transform not in cache key | Add to cache key or emit Vary; purge poisoned objects |
| Client hang / dropped connection | Manual Content-Length after body change |
Let runtime recompute length or use chunked encoding |
| Worker timeout / OOM | Buffering a large body | Switch to TransformStream; offload heavy work to origin |
| Garbled response body | Transforming a compressed payload | Decode first, or run compression after the mutation |
| Boundary-split match misses | Per-chunk regex over streamed body | Use overlap window or streaming parser |
Rollback sequence: for declarative rules, re-PUT the previous ruleset version or DELETE the offending rule — propagation is seconds. For edge compute, redeploy the prior version (wrangler rollback, a pinned Lambda@Edge version + CloudFront association, or a Vercel promotion) and confirm the bad version drained from wrangler tail. In both cases, purge any cache objects written under the faulty transform before declaring the incident closed.
Edge Cases & Gotchas
Content-LengthvsTransfer-Encoding: never set both, and never overrideContent-Lengthafter mutating a body — let the runtime recalculate or stream with chunked encoding.- Hop-by-hop headers:
Connection,Keep-Alive,Proxy-*,TE,Trailer,Upgrade, andTransfer-Encodingmust not be copied origin-to-client; edge runtimes strip them, but manual copies reintroduce them. - Header case and merging: edge runtimes lowercase header names and may merge duplicates; logic that depends on exact casing or repeated headers breaks.
- Caching transformed responses without key variation: the fastest route to a data-leak incident; always tie the varying attribute to the cache key.
- Synchronous heavy transforms (>10MB): exceed CPU/memory limits; stream and apply backpressure, or push the work to origin.
- Failing closed: a transform that throws should fall back to the untouched origin response, not a 500.
Frequently Asked Questions
Does request transformation affect CDN cache hits?
Yes. Transformations that alter headers, paths, or cookies used in the cache key fragment the cache, since the edge stores and looks up a distinct object per variant. Keep cache keys consistent, emit Vary for attributes that legitimately change output, or apply the transformation after cache lookup when the variation should not be cached.
Can I transform response bodies without buffering the entire payload?
Yes. Modern edge runtimes support TransformStream for chunked, streaming modifications, so data is processed as it flows from origin to client and peak memory stays bounded by chunk size. Watch for matches that straddle chunk boundaries and use an overlap window or a streaming parser when correctness demands it.
How do I debug why a transformation isn’t applying in production?
Verify rule priority order and confirm match conditions against the raw request, then inspect edge logs for bypass flags and inject debug headers like X-Transform-Route to trace the executed branch. Compare the origin response with the edge output to localize the drift, and confirm the matcher and cache key agree.
Should declarative rules or edge compute handle my transform? Use a declarative rule engine for static condition-action transforms — header injection, path rewrites, country redirects — because they add zero CPU and roll back in seconds. Reach for edge compute (Workers, Lambda@Edge, Compute@Edge) only when you need logic, body parsing, or external lookups that a rule expression cannot represent.