Migrating CSP from report-uri to report-to
This guide is part of the Content-Security-Policy (CSP) reference and covers moving CSP violation reporting from the deprecated report-uri directive to the modern report-to directive backed by the Reporting API. report-uri fires one JSON POST per violation directly to a URL. report-to names an endpoint group that the browser registers from a Reporting-Endpoints header, then batches, queues, and retries deliveries to it. The migration is additive and reversible: run both directives in parallel, confirm the new pipeline receives reports, then drop the legacy one. Do this without breaking reporting on browsers that only understand report-uri.
Configuration Syntax & Exact Values
The modern setup needs two pieces: a Reporting-Endpoints header that maps an endpoint name to a collector URL, and a report-to directive in the CSP that references that name. Keep report-uri during the transition.
Reporting-Endpoints: csp-endpoint="https://collector.example.com/csp"
Content-Security-Policy: default-src 'self'; script-src 'self'; report-uri https://collector.example.com/csp; report-to csp-endpoint
Annotated breakdown:
Reporting-Endpoints: csp-endpoint="…"— registers the named groupcsp-endpointand its absolute HTTPS collector URL. This is the Reporting API v1 header. It uses simplename="url"structured-field syntax.report-to csp-endpoint— the CSP directive that points violations at the registered group by name (not a URL). The name must match the key inReporting-Endpointsexactly.report-uri https://…— the legacy directive taking a literal URL. Browsers that support the Reporting API and see both directives preferreport-toand ignorereport-uri; older browsers usereport-uri. Keeping both yields zero reporting gaps.
A note on the older Report-To header: the original Reporting API used a Report-To header carrying a JSON object with group, max_age, and endpoints[]. It is superseded by the simpler Reporting-Endpoints header. The CSP report-to directive references the group name from either header, so choose Reporting-Endpoints for new work and only fall back to Report-To if you must support a browser matrix that requires it.
How the Two Delivery Models Differ
The reason the migration is worth doing is that the two directives deliver reports differently, and that difference shows up under load. report-uri is fire-and-forget: the browser issues one uncredentialed POST to the literal URL the instant a violation occurs, with no batching, no queue, and no retry. A page that trips fifty inline-script violations on first paint sends fifty separate requests, and any that fail — a collector blip, a network drop, a throttled connection — are simply lost.
report-to routes through the Reporting API instead. The browser first registers the named endpoint group from the Reporting-Endpoints header, then funnels violations into a per-origin queue that it flushes in batches, retries with backoff, and can even deliver after the offending page has been closed. That buffering trades a little latency (why reports can lag up to a minute) for far higher delivery reliability and fewer connections.
Server-Side Configuration
Nginx
add_header Reporting-Endpoints 'csp-endpoint="https://collector.example.com/csp"' always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.example.com; report-uri https://collector.example.com/csp; report-to csp-endpoint" always;
The always flag emits both headers on error responses too, so violations triggered on 4xx/5xx pages still report.
Apache
Header always set Reporting-Endpoints "csp-endpoint=\"https://collector.example.com/csp\""
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.example.com; report-uri https://collector.example.com/csp; report-to csp-endpoint"
Header always set guarantees attachment across all status codes; requires mod_headers.
Cloudflare
export default {
async fetch(request) {
const response = await fetch(request);
const headers = new Headers(response.headers);
headers.set('Reporting-Endpoints', 'csp-endpoint="https://collector.example.com/csp"');
headers.set(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' https://cdn.example.com; report-uri https://collector.example.com/csp; report-to csp-endpoint"
);
return new Response(response.body, { status: response.status, headers });
},
};
Setting both headers at the edge guarantees delivery even if the origin omits them.
Node/Express (Helmet)
const helmet = require('helmet');
app.use((req, res, next) => {
res.setHeader('Reporting-Endpoints', 'csp-endpoint="https://collector.example.com/csp"');
next();
});
app.use(helmet.contentSecurityPolicy({
useDefaults: false,
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "https://cdn.example.com"],
reportUri: ["https://collector.example.com/csp"],
'report-to': ["csp-endpoint"],
},
}));
Helmet has no first-class reportTo key, so pass report-to as a raw directive name and set Reporting-Endpoints in middleware.
Diagnostic & Verification Steps
Confirm both headers ship, then trigger a real violation and watch it reach the collector.
# 1. Both headers present
curl -sI https://example.com | grep -iE 'reporting-endpoints|content-security-policy'
Expected output:
reporting-endpoints: csp-endpoint="https://collector.example.com/csp"
content-security-policy: default-src 'self'; script-src 'self' https://cdn.example.com; report-uri https://collector.example.com/csp; report-to csp-endpoint
# 2. Collector accepts a Reporting API payload (no CSP context needed)
curl -s -o /dev/null -w '%{http_code}\n' -X POST \
-H 'Content-Type: application/reports+json' \
-d '[{"type":"csp-violation","age":0,"url":"https://example.com","body":{"documentURL":"https://example.com","effectiveDirective":"script-src","disposition":"enforce"}}]' \
https://collector.example.com/csp
Expected output: 204 (or 200). A 400/404/415 means the route or Content-Type is wrong; a missing response with a CORS error means the collector lacks Access-Control-Allow-Origin for the preflight.
In the browser: load a page that violates the policy (for example an inline script with no nonce), open DevTools → Network, filter by the collector host, and confirm a POST with Content-Type: application/reports+json whose body contains type: "csp-violation", body.documentURL, body.effectiveDirective, and body.disposition. Reporting API delivery is batched, so allow up to a minute. See the parent CSP reference for the full Report-Only rollout that precedes enforcement.
Edge Cases, Security Implications & Safe Rollback
The dual configuration is safe precisely because each browser resolves to exactly one directive at report time. A browser that implements the Reporting API honors report-to and silently discards the report-uri token it also sees; a browser without the API never parses report-to and falls back to report-uri. With both present, every client lands on a working path and no report is emitted twice from the same event.
- Legacy browsers ignore
report-to. Older Safari and Firefox builds, and IE11, only honorreport-uri. Keep both directives until analytics show legacy-only clients are below your tolerance (commonly under 5%). Droppingreport-uriearly causes silent reporting loss, not user-visible breakage. - Header size on verbose policies. A large policy plus
Reporting-Endpointscan push response headers past proxy/server limits (8 KB is a common default). Shorten collector URLs and trim redundant sources; do not split a single policy across multipleContent-Security-Policyheaders to save space, because the browser enforces the intersection of all of them. - Duplicate reports during the dual phase. A browser that supports the Reporting API uses only
report-to, so it does not double-report. But mixed fleets and proxy retries can still duplicate; deduplicate server-side by hashingdocumentURL+effectiveDirectivewithin a short time window. - Collector exposure. Violation reports leak URL paths and sometimes query data. Serve the collector over TLS 1.2+, accept only
application/csp-report/application/reports+json, and rate-limit to blunt report flooding. - Field-name differences. Reporting API v1 reports (
report-to) usedocumentURL,effectiveDirective,disposition; legacyreport-urireports use acsp-reportenvelope withdocument-uri,violated-directive,original-policy. Your collector must parse both shapes during the dual phase.
Rollback (reversible, non-destructive): remove the Reporting-Endpoints header and the report-to token from the CSP, leaving report-uri intact, then reload the service.
# Nginx: delete the Reporting-Endpoints line and drop "; report-to csp-endpoint" from the CSP, then:
# nginx -t && systemctl reload nginx
# Apache:
Header unset Reporting-Endpoints
# remove "; report-to csp-endpoint" from the Content-Security-Policy value, then apachectl -t && systemctl reload apache2
Re-run the curl -sI check and confirm only report-uri remains. Reporting continues via the legacy path with no downtime.
Frequently Asked Questions
Will I get duplicate reports while both directives are live?
Generally no. Any browser that supports the Reporting API uses report-to and ignores report-uri; browsers without it use report-uri. Duplicates usually come from proxy retries or mixed report formats, not the dual directive itself — deduplicate server-side by documentURL + effectiveDirective.
Do I need Reporting-Endpoints or the older Report-To header?
Use Reporting-Endpoints (name="url" syntax) for new deployments — it is the Reporting API v1 header and is simpler. Only use the older Report-To JSON header if your required browser matrix predates Reporting-Endpoints support.
Why are reports not arriving even though the header is correct?
The Reporting API batches and delays delivery, so allow up to a minute. Confirm the collector returns 204/200 to a manual POST, that it serves over HTTPS, and that the endpoint name in report-to exactly matches the key in Reporting-Endpoints.
Can I point report-uri and report-to at the same collector?
Yes. A single endpoint can accept both the legacy csp-report envelope and the Reporting API reports+json array — branch on Content-Type and the body shape. This keeps the dual phase to one collector.
Conclusion
Roll this out incrementally: add Reporting-Endpoints and report-to alongside the existing report-uri in staging, verify the collector receives Reporting API payloads, then promote the dual configuration to production. Hold the dual phase until legacy-only browsers fall below your traffic threshold, then remove report-uri and keep only report-to with Reporting-Endpoints.
Related
- Content-Security-Policy (CSP) reference — the parent header reference and Report-Only rollout.
- Generating CSP nonces per request — per-response nonces for inline scripts.
- CSP strict-dynamic explained — propagating trust to dynamically loaded scripts.