skip to content

How does the report-to directive in a Content-Security-Policy change where a violation report goes and what its body looks like?

level: middleimportance: should knowfreq 46%

answer

  1. one names a URL, one names a name
  2. the name resolves elsewhere on the response
  3. Reporting-Endpoints dictionary, same response
  4. envelope with type, age, url, body
  5. hyphenated keys versus camelCase members

basics

~20 s

report-uri names a URL inside the policy and posts a csp-report body of hyphenated keys. report-to names an endpoint declared by a Reporting-Endpoints response header field, and the report arrives as application/reports+json with a camelCase body inside a typed envelope.

solid answer

~40 s

`report-uri` writes the destination straight into the policy and is deprecated in CSP Level 3. Its delivery is a POST of a single JSON object with one `csp-report` member whose keys are hyphenated: `document-uri`, `blocked-uri`, `effective-directive`, `disposition` and the rest. `report-to` writes a **name** instead, which the browser resolves against a `Reporting-Endpoints` response header field on the same response, so the destination is configured once per response rather than per policy. Delivery then goes through the general reporting pipeline: a POST of `application/reports+json` carrying an array of envelopes, each with `type`, `age`, `url`, `user_agent` and `body`, where `type` is `csp-violation` and `body` is a `CSPViolationReportBody` using camelCase members such as `documentURL`, `blockedURL` and `effectiveDirective`. Same violation, two field spellings.

code

http · 3 lines
http
HTTP/1.1 200 OK
Reporting-Endpoints: csp-collector="https://pharmacy.example/csp-reports"
Content-Security-Policy: default-src 'self'; script-src 'self'; report-uri /legacy-csp; report-to csp-collector

go deeper

for a junior

The thing to hold on to is that a policy must say where records go, and there are two ways to say it: an older directive that names a URL and a newer one that names an endpoint declared elsewhere on the same response.

for a middle

Be able to name both directives, say which is deprecated, and describe the resolution step through the Reporting-Endpoints field. Knowing that the two paths produce hyphenated and camelCase bodies is the detail that marks real exposure.

for a senior

Show you have debugged the silent case: reports posting but nothing stored, because the collector parses one body shape and receives the other, or because the endpoint name resolves to nothing. Mention batching, delayed delivery and duplicate records across the two paths.

for a principal

The judgment is about the collector contract rather than the policy: one endpoint receiving several report types, retained for how long, holding whatever the page URL carried, is a data-retention decision that outlives whichever directive is fashionable.

## Two reporting directives, two different jobs A policy that records violations has to say where the record goes, and Content-Security-Policy has two directives that answer that, from two different eras. **`report-uri`** takes one or more URLs, written directly into the policy text: `report-uri /csp-collector`. It is the original mechanism, it is marked **deprecated in CSP Level 3**, and it is still honoured very widely, which is why it is still seen in production policies alongside its replacement. **`report-to`** takes a **token**, not a URL: `report-to csp-collector`. The token is a name that the browser resolves against a `Reporting-Endpoints` response header field delivered on the same response, which is a structured-field dictionary mapping names to URLs. The indirection means the endpoint is declared once for the response and can be referenced by anything on that response that needs to report, rather than being repeated inside each policy. ```http Reporting-Endpoints: csp-collector="https://pharmacy.example/csp-reports" Content-Security-Policy-Report-Only: default-src 'self'; report-to csp-collector ``` If the name in `report-to` matches no entry in that dictionary, there is nowhere for the report to go and it is simply not delivered. ## The two body shapes, field by field This is where candidates come unstuck, because the same violation is described by two sets of member names that differ in spelling and in one case in name: | Legacy `report-uri` body (hyphenated) | Reporting API `CSPViolationReportBody` (camelCase) | |---|---| | `document-uri` | `documentURL` | | `blocked-uri` | `blockedURL` | | `effective-directive` | `effectiveDirective` | | `violated-directive` | `violatedDirective`, a documented historical alias | | `original-policy` | `originalPolicy` | | `source-file` | `sourceFile` | | `script-sample` | `sample` | | `status-code` | `statusCode` | | `line-number` / `column-number` | `lineNumber` / `columnNumber` | | `referrer`, `disposition` | `referrer`, `disposition` | Two things are worth noticing in that table. `violated-directive` and `effective-directive` carry the **same value**; the first is history, not extra information. And `referrer` and `disposition` happen to be spelled identically in both shapes, which is exactly why a collector written against one shape and fed the other appears to half-work. ## What arrives on the wire - The legacy path POSTs **one** object, `{"csp-report": { ... }}`, as `application/csp-report`. - The Reporting API path POSTs an **array** of envelopes as `application/reports+json`, because the pipeline batches reports and may deliver several at once, of several different types. - Each envelope has `type` (here `csp-violation`), `age` (milliseconds the report waited in the queue before delivery), `url` (the document that generated it), `user_agent`, and `body` (the `CSPViolationReportBody`). - Delivery is **out of band and best effort**: it is not tied to the failed fetch, it can be delayed, and a report can simply never arrive. A collector therefore has to be written to accept a batch, to read `type` before it reads `body`, and to tolerate `age` values that are not small. ## Why the indirection exists 1. **One destination for many report types.** Content-Security-Policy is not the only thing that reports; the named-endpoint dictionary is shared, so a response declares its collector once. 2. **Batching and back-off.** Because the browser owns delivery rather than the policy naming a URL to post to immediately, reports can be queued, batched and retried rather than producing one request per violation. 3. **A typed envelope.** `type` lets a single collector endpoint receive several kinds of report and dispatch on the field rather than on the path it was posted to. ## The practical consequence for a policy you write today Because support for the two directives differs across client generations, a policy is frequently written with both, and a collector that accepts only one body shape will silently see half its traffic as malformed. The failure looks like a collector that receives requests and stores nothing: the POSTs arrive, the parser looks for `csp-report` and finds an array of envelopes, or looks for `blockedURL` and finds `blocked-uri`. Reading the media type first, `application/csp-report` against `application/reports+json`, is the cheapest way to route to the right parser.

  • The policy carries report-to csp-collector and reports never arrive. What do you check first?
    Whether the same response carries a `Reporting-Endpoints` field defining `csp-collector`. The directive takes a name, not a URL, and an unresolved name means the browser has no destination and drops the report. Check the spelling of the name on both sides, and that the field is on the same response as the policy rather than on a different one.
  • Why does the Reporting API deliver an array rather than a single report per request?
    Because delivery is decoupled from the violation. The browser queues reports and flushes them together, possibly of several different types, which is why each element carries its own `type` and `age`. A collector must parse a batch and dispatch per element; one that assumes a single object will reject everything after the first.
  • If report-uri is deprecated, why is it still in real policies?
    Deprecated means discouraged, not removed. Client support for the two directives has never been uniform, so a policy that wants records from every client generation carries both and accepts that some clients report through one path, some through the other, and some through both, which means the collector must also deduplicate.

saying these in an interview costs you the question

  • Thinks report-to takes a URL just as report-uri does
  • Expects Reporting-Endpoints to be sent by the client on the request
  • Assumes both delivery paths post an identical body shape
  • Believes deprecated means browsers no longer honour report-uri
  • Expects exactly one report per delivered request