No-Vary-Search for Cache-Safe URLs
Model URL equivalence from representation semantics, preserve locale and product variants, and test ignored parameters against adversarial cache-key cases.
No-Vary-Search lets a server declare which URL query differences do not change a navigation response, enabling reuse without guessing from parameter names. This tutorial builds an adversarial equivalence corpus so analytics noise can collapse while locale, authentication, experiments, pagination, and sorting stay distinct.
No-Vary-Search declares representation equivalence
Two URLs can differ in their query strings while the server returns the same representation. Tracking parameters often have that property; language, authentication, pagination, filters, sort order, or experiment assignments often do not. No-Vary-Search gives the response a way to describe selected query differences that navigation caching may ignore.
This is a server assertion about response meaning, not a browser heuristic. The team that owns the route must know which parameters influence rendering, data, permissions, headers, personalization, and client bootstrap state. If that knowledge is incomplete, preserve the parameter in the cache key.
The HTTPWG draft is the source for current field syntax and semantics, and it may evolve. Keep implementation behind a versioned parser or platform behavior rather than copying examples into an unreviewed canonicalizer.
The bundled fixture implements only a declared bounded policy over generated URLs. It is not a complete standards parser or evidence about a particular browser cache; it demonstrates the equivalence decisions your route must prove.
Classify every parameter by representation effect
Inventory query parameters on the route and trace each to server logic, edge logic, data fetching, templates, client hydration, analytics, and redirects. Mark whether it changes bytes, language, permissions, content selection, order, pagination, experiment treatment, or visible defaults. Unknown parameters remain part of the key until their effect is understood.
Analytics parameters such as utm_source may be candidates to ignore when they are consumed only for attribution and do not alter the response. Even then, check whether middleware writes different markup, cookies, or redirects. A parameter with an analytics-looking name can still affect product behavior.
Locale, tenant, preview, auth, currency, experiment, page, sort, filter, and search terms should begin as representation-changing. The URLs as product architecture guide helps keep those semantics durable and shareable rather than treating query strings as incidental implementation detail.
No-Vary-Search needs an owner per route. A global rule that ignores the same names everywhere will eventually collapse a route where one of those names gains meaning.
- Ignored analytics parameters map to one navigation-cache representation.
- Representation-changing parameters retain distinct keys.
- Equivalence is a server declaration, not a client guess about similar pages.
| Signal | Interpretation |
|---|---|
| URL equivalence lattice | Tracking variants collapse into one representation node while locale, page, sort, authentication, and experiment branches remain separate. |
Runnable artifact — The bounded parser model canonicalizes a frozen URL corpus and verifies that only declared tracking differences collapse.
import assert from "node:assert/strict";
const policy={ignore:new Set(["utm_source","utm_campaign"]),keyOrder:false};
const canonical=value=>{const url=new URL(value);const kept=[...url.searchParams].filter(([key])=>!policy.ignore.has(key));if(!policy.keyOrder)kept.sort(([a,av],[b,bv])=>a.localeCompare(b)||av.localeCompare(bv));url.search="";for(const [key,val] of kept)url.searchParams.append(key,val);return url.toString()};
const cases=[
["https://shop.test/p/7?utm_source=a","https://shop.test/p/7?utm_source=b",true],
["https://shop.test/p/7?locale=en","https://shop.test/p/7?locale=fr",false],
["https://shop.test/p/7?page=1","https://shop.test/p/7?page=2",false],
["https://shop.test/p/7?sort=price&utm_campaign=x","https://shop.test/p/7?utm_campaign=y&sort=price",true],
];
const results=cases.map(([left,right,expected])=>({left,right,expected,actual:canonical(left)===canonical(right)}));assert.deepEqual(results.map(x=>x.actual),results.map(x=>x.expected));console.log(JSON.stringify({policy:{ignore:[...policy.ignore],keyOrder:policy.keyOrder},results},null,2));console.log("PASS: URL corpus preserves representation-changing parameters");
Choose params, except, and key-order policies
A policy can describe parameters that do not vary the response or invert the declaration by varying on an exception set, depending on the standard's current grammar. Choose the form that is smallest and hardest to misread for the route. Avoid broad allowlists that silently include future parameters with unknown semantics.
Key order requires separate thought. Servers commonly treat a=1&b=2 and b=2&a=1 as equivalent, but repeated parameters or order-sensitive application logic can violate that assumption. Test duplicates, empty values, percent encoding, case, and repeated keys before declaring order irrelevant.
Do not create an ad hoc URL normalizer and assume it matches user-agent navigation-cache behavior. Use the actual platform integration where supported, and keep the local canonicalizer only as an expectation model for tests and reviews.
The fixture ignores two tracking names and sorts retained pairs. Its narrow result is derived from frozen cases, not a recommendation to apply the same policy to every route or HTTP cache.
Build an adversarial URL corpus
Start with pairs expected to be equivalent and pairs expected to remain distinct. Cover analytics values, locale, currency, tenant, authentication state, preview tokens, experiment groups, page numbers, sort order, filters, repeated parameters, blank values, unknown keys, encoding variants, and reordered pairs. Include a response digest or semantic snapshot for each environment when possible.
Expected equivalence should be reviewed by the route owner, security, and localization owners where relevant. A cache test cannot infer whether two pages are permitted for the same principal. Keep authorization outside public URL variance and never use reuse as a shortcut around an access check.
The local four-pair corpus proves that tracking differences collapse while locale and pagination do not. Expand it before rollout; the small teaching set deliberately omits repeated-key and encoding edge cases.
No-Vary-Search becomes safer when every production incident adds a minimized regression pair. Store the header policy, input URLs, response variant labels, and expected match so later field changes cannot reopen the same false hit.
| Class | Default |
|---|---|
| Analytics | candidate to ignore after proof |
| Locale/auth/experiment | preserve |
| Pagination/sort/filter | preserve unless representation proof says otherwise |
| Signal | Interpretation |
|---|---|
| Parameter-to-cache-key map | Query parameters flow through ignore, preserve, and order policies before a navigation cache key is formed. |
Treat false hits as the dangerous failure
A false hit reuses a cached navigation response for a URL whose representation should differ. That can show the wrong language, product order, experiment, user state, or private content. A false miss performs duplicate work for equivalent URLs. Both matter, but the first is a correctness and potentially security failure.
Bias rollout toward false misses. Ignore only parameters with strong equivalence evidence, monitor response variant mismatches, and keep a fast way to remove the header. Do not compensate for an unsafe cache key by asking client JavaScript to patch the page after activation.
The diagnostic split makes the asymmetry explicit. When a test fails, the repair differs: preserve the parameter for a false hit, or consider ignoring it for a false miss only after response-level validation.
The HTTP caching foundations article explains adjacent validators, freshness, and cache ownership. No-Vary-Search belongs to navigation reuse semantics and should not be generalized casually to every intermediary cache key.
Connect URL variance to speculative loading
Speculative navigation may fetch one URL before the user chooses a variant whose query differs only in ignorable parameters. A correct declaration can let the prepared response satisfy that navigation rather than starting over. The WHATWG integration defines how the feature participates in speculative loading behavior.
Do not let reuse broaden candidate eligibility. A private or effectful URL remains unsuitable for speculation even if some parameters are non-varying. First decide whether preparation is allowed, then decide whether a prepared response is equivalent to the activated URL.
Track prefetch cache reuse, false-hit diagnostics, and misses separately from ordinary HTTP cache metrics. A response reused across navigation URLs should retain the requested URL semantics the application expects, including analytics attribution handled outside representation content.
Log that distinction per activation.
No-Vary-Search can complement HTTP 103 Early Hints for different phases, but combining them complicates attribution. Test one change at a time against the same route corpus.
Verify server and client assumptions together
Compare server-rendered HTML, critical response headers, embedded data, hydration inputs, and post-load visible state for URL pairs declared equivalent. A byte difference is not always a semantic difference, but a supposedly equivalent response that initializes a different product state is a clear failure.
Exercise redirects and canonical URLs. Ignored analytics parameters may be stripped, retained in the address bar, or captured through another mechanism, but the policy should not create redirect loops or duplicate indexing signals. Keep the canonical page identity separate from the navigation cache decision.
Use the URLPattern routes article to build route-specific policy matching without accidentally applying a product-page declaration to search or account pages. Unknown routes and parameters should default to variance.
Review that default on every release.
The fixture prints its retained keys and expected equivalence. Add response snapshots from a controlled local server before treating it as a deployment gate, and label those snapshots with the exact application revision.
- A false hit can expose or render the wrong representation and is a correctness failure.
- A false miss repeats work but preserves correctness.
- Bias toward distinct keys until equivalence has response-level evidence.
| Signal | Interpretation |
|---|---|
| False-hit and false-miss diagnostic split | A center line separates dangerous collapsed variants from wasteful duplicate keys, with repair paths pointing in opposite directions. |
Roll out one conservative route at a time
Choose a public route with high tracking-parameter noise and no personalization. Document the ignored set, corpus, route owner, rollback, and browser support assumptions. Deploy to a small scope, then watch errors, unexpected response variants, navigation reuse, and origin request changes.
Version the policy with route code. When a new parameter appears, require an explicit classification rather than inheriting ignored status. Re-run the adversarial corpus on middleware, CDN, server, and client changes because any layer can make a formerly irrelevant parameter meaningful.
No-Vary-Search succeeds when it removes duplicate navigation work without changing what any user sees or may access. The absence of obvious visual bugs is not enough; automated equivalence and permission cases should remain in the release gate.
Run the included fixture, add locale, tenant, experiment, repeated-key, and unknown-parameter pairs from your own URL design, and keep the header disabled until every expected distinction survives.
Use query parameter cache keys as the reviewable output, URL search variance as the route-owner vocabulary, prefetch cache reuse as the optimization outcome, and HTTP caching as adjacent rather than identical infrastructure. The synthetic receipt should show the canonical form for both sides of every pair, the exact ignored set, ordering policy, and expected verdict. Add negative cases before adding ignored names; this makes the test corpus grow toward safety instead of toward a higher reuse percentage.
Before rollout, compare response digests for every supposedly equivalent pair under the same anonymous request context, then repeat with changed language, session, and experiment state. Any unexplained difference returns the parameter to the cache key.