Problem
TenantMiddleware._vary_sender (framework/hosting/simple_module_hosting/_tenant.py) merges the resolver's vary names into the response with
headers = MutableHeaders(scope=message)
merged = merge_vary(headers.get("vary"), vary)
if merged is not None:
headers["vary"] = merged
MutableHeaders.get returns only the first Vary header line, and the assignment then deletes every other Vary line. A response that carries more than one Vary field loses all but the first. If a later line is Vary: *, the wildcard disappears and is replaced by a finite list, so a shared cache treats a deliberately uncacheable response as cacheable. #367 promised that existing entries are kept and Vary: * is left untouched.
Reproduction
Found by independent QA on #384 and still on main at 7db61a0. A downstream app returns two header lines, Vary: Accept and Vary: *, and the resolver answers TenantResolution("acme", "header", ("Host",)). The response leaves the middleware with one line:
before: [(b"vary", b"Accept"), (b"vary", b"*")]
after: [(b"vary", b"Accept, Host")] # wildcard gone
A smaller issue in the same helper: merge_vary("Accept, accept", ("Host",)) returns Accept, accept, Host. Case-insensitive duplicates already present in the input are kept, and only appended names are de-duplicated.
Expected
- Every existing
Vary line is read (headers.getlist("vary"), split on commas), merged, and written back as one line.
- If any existing line contains
*, the response is left exactly as it was.
- Existing tokens are de-duplicated case-insensitively, keeping the first spelling.
Tests to add
- Two
Vary lines (Accept + Accept-Language) plus resolver ("Host",) → one line Accept, Accept-Language, Host.
Vary: Accept + Vary: * → unchanged, * preserved.
merge_vary("Accept, accept", ("Host",)) → Accept, Host.
Problem
TenantMiddleware._vary_sender(framework/hosting/simple_module_hosting/_tenant.py) merges the resolver'svarynames into the response withMutableHeaders.getreturns only the firstVaryheader line, and the assignment then deletes every otherVaryline. A response that carries more than oneVaryfield loses all but the first. If a later line isVary: *, the wildcard disappears and is replaced by a finite list, so a shared cache treats a deliberately uncacheable response as cacheable. #367 promised that existing entries are kept andVary: *is left untouched.Reproduction
Found by independent QA on #384 and still on
mainat 7db61a0. A downstream app returns two header lines,Vary: AcceptandVary: *, and the resolver answersTenantResolution("acme", "header", ("Host",)). The response leaves the middleware with one line:A smaller issue in the same helper:
merge_vary("Accept, accept", ("Host",))returnsAccept, accept, Host. Case-insensitive duplicates already present in the input are kept, and only appended names are de-duplicated.Expected
Varyline is read (headers.getlist("vary"), split on commas), merged, and written back as one line.*, the response is left exactly as it was.Tests to add
Varylines (Accept+Accept-Language) plus resolver("Host",)→ one lineAccept, Accept-Language, Host.Vary: Accept+Vary: *→ unchanged,*preserved.merge_vary("Accept, accept", ("Host",))→Accept, Host.