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.
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
- 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.