Skip to content

Troubleshooting

Solutions for common CSP configuration issues.

This is the most common cause, and the only one with no local symptom. Content blockers — uBlock Origin, Adblock Plus, Brave Shields, Privacy Badger and others — block the browser’s POST to any reporting endpoint. There is no console error, no CSP violation, and nothing in DevTools that reads as a failure to a non-expert. Reports simply never arrive, and a correctly configured site looks identical to one that has had no violations.

To confirm it:

  1. Open DevTools and switch to the Network tab.
  2. Reload the page and trigger a violation.
  3. Look for a request to ingest.us.headerhawk.com. If it is missing, or shows as cancelled or blocked, an extension is the cause.
  4. Disable the extension for your origin and reload. Reports should appear within a minute.

A site keeps reports only from the hosts it lists. Each is an exact host (example.com) or a wildcard (*.example.com). A site that lists only example.com discards reports from www.example.com, app.example.com, and every other subdomain. A wildcard matches every subdomain at any depth, but not the bare domain: a site that lists only *.example.com discards reports from example.com itself.

This matters most when one CSP header is deployed across several hosts, which is the normal case. The endpoint returns the same success status whether a report was stored or discarded — deliberately, so that the response cannot be used to discover which hosts a tenant monitors — so nothing in the browser shows a problem.

To check, open the site’s setup: the site’s row under Settings → Sites, or, with the site chosen in the switcher at the top of the dashboard, Set up this site. If reports have been discarded because their host is not allowed, the panel lists how many and which hosts they came from. If a listed host is one of yours, choose Allow beside it — as the host itself, or, for a subdomain, as a wildcard over its parent, so docs.staging.example.com is offered *.staging.example.com rather than *.example.com — and reports from it are kept from then on. Members, admins and owners can do this. Hosts that do not share a domain with any of the site’s hosts are counted together as (other hosts) rather than named, since anyone can send reports claiming any host.

Verify the CSP header is being sent:

Terminal window
curl -I https://your-site.com | grep -i content-security-policy

You should see either Content-Security-Policy or Content-Security-Policy-Report-Only.

Ensure your report-uri directive is correct:

report-uri https://your-ingest-code.ingest.us.headerhawk.com/r

Common mistakes:

  • Missing https://
  • Wrong ingest code
  • Typo in the domain name
  • A 12-character code in the path (/r/k3x9mp2nq7ab) rather than the host. Only the 8-character codes of sites created before ingest codes moved into the host go in the path, on https://ingest.us.headerhawk.com/r/{code}

Send a test report. Use a page on one of the site’s own hosts as the document-uri — a report whose document-uri matches none of the site’s hosts is discarded, and the response looks identical either way:

Terminal window
curl -i -X POST \
https://your-ingest-code.ingest.us.headerhawk.com/r \
-H "Content-Type: application/csp-report" \
-d '{"csp-report":{"document-uri":"https://your-site.com/","violated-directive":"script-src","original-policy":"default-src self"}}'

Other statuses do tell you something:

Status Meaning What to do
404 Nothing is accepting reports for that ingest code: either no site has it, or the site it belongs to is deactivated Check the code against the site’s setup panel, then check the site is active
400 A code in the path that is not an 8-character one (a 12-character code goes in the host), or the report body failed validation Copy the endpoint from the setup panel, and check the body has document-uri, violated-directive and original-policy
429 Rate limited Back off

404 covers both the unknown-code and the deactivated-site case, and does not say which. That is deliberate, for the same reason a host-mismatch drop still returns 204: the ingest code sits in the report-uri of every page that uses it, so it is public by construction, and a status that distinguished “real code” from “no such code” would let anyone work through the keyspace and confirm which codes belong to real customers. Both of your own cases are visible from the dashboard, where the ambiguity costs you nothing.

Check the browser console for CSP messages. Violations appear as warnings even in report-only mode.

Script integrity reporting ('report-sha256') has its own set of silences, separate from violation reporting. This section is the diagnostic short list; Script Integrity Monitoring explains the mechanism in full — what the data covers, how sampling works, and how to read the Origins, Scripts and Pages views.

It needs the Reporting API, not report-uri

Section titled “It needs the Reporting API, not report-uri”

A hash report is a Reporting API report type. report-uri predates report types and carries CSP violation reports only, so it has no way to deliver a hash report. A policy combining 'report-sha256' with report-uri looks correct and reports nothing.

Use the Reporting-Endpoints header with report-to:

Reporting-Endpoints: csp-endpoint="https://your-ingest-code.ingest.us.headerhawk.com/r"
Content-Security-Policy-Report-Only: script-src 'self' 'report-sha256'; report-to csp-endpoint

Note the quotes. 'report-sha256' is a keyword source expression and must be quoted and placed inside script-src; bare report-sha256 is parsed as a hostname and does nothing.

The browser generates a hash when it fetches and executes a script. On a normal reload an external script is usually served from cache, no fetch happens, and no hash is generated — however correct your policy is. A hard refresh (shift+reload) forces the fetch, which removes caching as an explanation; it is not a guarantee that a report will follow, since support is still narrow (see below).

The same effect applies to real traffic: a site with well-cached assets reports far fewer hashes than it has scripts, and returning visitors contribute almost none. Combined with the sample rate, treat the Scripts page as a sample of your scripts rather than a complete inventory.

Chromium 133+ implements report-sha256; WebKit support is in progress; Firefox does not support it. Some Chromium builds accept the keyword and emit no hash reports at all. An empty Scripts page is not by itself evidence of a misconfiguration — see Browser support is narrow for what has actually been observed.

Every report type beyond CSP violations has its own header and its own ways of staying silent. Browser Report Types lists the header for each; the setup panel under Settings → Sites generates them for the types the site has switched on.

That is sampling. Each type has a sample rate in the site’s settings, and at a rate below 100% HeaderHawk keeps that share of the reports it receives and discards the rest before they count towards your quota. Deprecations and interventions start at 5% and script hashes at 10%, because a page can send the same one on every load.

Counts are scaled back up by the rate, so a total marked ≈ or estimated stands for the reports sent, not just the ones kept. A list of individual reports shows only the kept ones, which is why DevTools can show twenty deprecation reports leaving the page and the Explorer one. Raise the rate for a type in the site’s settings to keep more of it; the quota counts what is kept.

  • Chromium-based browsers only. Firefox and Safari do not implement Network Error Logging, so their visitors never send one.
  • It uses Report-To, not Reporting-Endpoints. NEL predates Reporting-Endpoints and still names its group through the older Report-To header. The setup generates both lines; keep them together, and keep the group name in NEL’s report_to the same as in Report-To.
  • The browser caches the policy. A browser learns the NEL policy from a response over HTTPS and keeps it for max_age seconds (30 days in the generated header). A header you just added takes effect only for browsers that have visited since, and one you removed stays in effect for browsers that have not. The setup adds a line withdrawing the policy (max_age: 0) while NEL is switched off and reports are still arriving.
  • Only failures are asked for. The generated header sets success_fraction to 0, so a site whose requests all succeed sends nothing. A failure that stops the browser reaching you at all is reported once it can reach the ingestion endpoint again, not as it happens.

No deprecation, intervention or crash reports

Section titled “No deprecation, intervention or crash reports”

Browsers send these three to the endpoint named default. A Reporting-Endpoints header naming the endpoint anything else, headerhawk included, gets CSP and the other policy reports and none of these. The setup names the endpoint default while any of the three is switched on; if you wrote the header yourself, check the name. The one exception is a crash on a page that also declares an endpoint named crash-reporting: Chrome sends it there instead.

They are also rare by nature. A deprecation is sent only when a page calls an API the browser plans to remove, and a crash only when a tab crashes or stops responding. A quiet Browser signals section on a well-maintained site is the expected result, not a fault.

Always use Content-Security-Policy-Report-Only first:

Content-Security-Policy-Report-Only: default-src 'self'; report-uri https://your-ingest-code.ingest.us.headerhawk.com/r

This reports violations without blocking anything.

Issue Solution
Inline scripts blocked Add 'unsafe-inline' or use nonces
CDN scripts blocked Add the CDN domain to script-src
Google Fonts blocked Add fonts.googleapis.com and fonts.gstatic.com
Images not loading Add domains to img-src or use https:
API calls failing Add API domain to connect-src
  1. Open browser DevTools
  2. Go to the Console tab
  3. Look for “Refused to…” messages
  4. Note the blocked URL and violated directive

Some violations are expected or from browser extensions. Filter in HeaderHawk dashboard by:

  • Document host (your domains only)
  • Blocked host (exclude known extension domains)
  • Directive (focus on critical directives)
Source Cause Action
chrome-extension:// Browser extensions Filter out in dashboard
moz-extension:// Firefox extensions Filter out in dashboard
about:blank Injected iframes Usually safe to ignore
localhost Dev tools or extensions Filter out in dashboard
  1. Run in report-only mode for at least a week
  2. Review all violation types in the dashboard
  3. Add legitimate resources to your policy
  4. Test critical user flows
  5. Have a rollback plan

Change the header from:

Content-Security-Policy-Report-Only: ...

To:

Content-Security-Policy: ...

Keep the report-uri to continue receiving reports about enforced blocks.

If things break:

  1. Immediately revert to Content-Security-Policy-Report-Only
  2. Check new violations in the dashboard
  3. Update policy to allow legitimate resources
  4. Try enforcing again

Problem: Refused to execute inline script

Solutions:

  1. Move to external file (recommended)
  2. Use nonce:
    Content-Security-Policy: script-src 'nonce-abc123'
    <script nonce="abc123">
    ...
    </script>
  3. Use hash:
    Content-Security-Policy: script-src 'sha256-...'
  4. Allow unsafe-inline (not recommended):
    Content-Security-Policy: script-src 'unsafe-inline'

Problem: Refused to apply inline style

Solutions:

  1. Move styles to external CSS file
  2. Use 'unsafe-inline' for styles (lower risk than scripts):
    Content-Security-Policy: style-src 'self' 'unsafe-inline'

Problem: Refused to evaluate a string as JavaScript

Cause: Your code or a library uses eval(), new Function(), or similar.

Solutions:

  1. Replace with safer alternatives
  2. If unavoidable, add 'unsafe-eval' (security risk):
    Content-Security-Policy: script-src 'unsafe-eval'

Problem: WebSocket connection refused

Solution: Add to connect-src:

Content-Security-Policy: connect-src 'self' wss://your-websocket-server.com

Problem: Cannot load content in iframe

Solution: Add source to frame-src:

Content-Security-Policy: frame-src 'self' https://youtube.com https://player.vimeo.com

If you’re stuck:

  1. Check the browser console for specific error messages
  2. Review violations in the HeaderHawk dashboard
  3. Search for the specific directive and error message
  4. Test with a minimal policy and add directives incrementally