cloudflare / cloudflare/cloudflare-docs

_headers: warn that comma-joining a singleton header like Cache-Control produces conflicting directives

Open
#32,979 1 comment 0 reactions 5 assignees Claimed by @WalshyDev View on GitHub
product:workers
Dominant language
MDX
Stars
5.2k
Forks
16.7k
Avg merge
2d 6h
Merged PRs (30d)
337

Description

### Which page

https://developers.cloudflare.com/workers/static-assets/headers/

### What the page gets right

The combining behaviour is documented, clearly:

> An incoming request which matches multiple rules' URL patterns will inherit all rules' headers.

> If a header is applied twice in the `_headers` file, the values are joined with a comma separator.

What's missing is a warning about one consequence of that behaviour.

### The gap

The worked example on that page joins `X-Robots-Tag`, where a comma-separated list is exactly what the header means. Readers reasonably generalise from it.

For a singleton header the same join produces something no browser can act on sensibly. Adding a longer cache lifetime to one file under an existing glob:

```
/lwe/quadgrams.txt
Cache-Control: public, max-age=604800, stale-while-revalidate=604800

/lwe/*
Cache-Control: public, max-age=0, s-maxage=86400
```

puts this on the wire (reproduced on wrangler 4.125.0, `wrangler dev --local`):

```
Cache-Control: public, max-age=604800, stale-while-revalidate=604800, public, max-age=0, s-maxage=86400
```

Two `max-age` values in one field. RFC 9111 leaves that ambiguous, so implementations resolve it however they like, and the result is worse than the single revalidation the narrower rule was meant to save.

### Why it's easy to miss

Nothing errors, the asset still serves, and a browser still caches it somehow. `curl -sI` shows it only if you read the whole line instead of grepping for the directive you just added.

The other thing readers assume, because every other layered-config system in Workers behaves this way, is that the more specific rule wins. Wrangler's routes, CSP maps, and `run_worker_first` all let the specific entry take precedence. `_headers` is the exception.

### Suggested addition

A short warning after the "joined with a comma separator" line, saying that joining works for list-valued headers like `X-Robots-Tag` and produces conflicting directives for singleton headers like `Cache-Control`.

It's worth stating the remedy in the same breath, because there isn't an obvious one: a file under a glob can only get a different `Cache-Control` by moving out of the glob's path or by setting the header in the Worker. Adding a narrower rule is not one of the options.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.