Diagnose CORS errors after a deploy

Find CORS-blocked requests grouped by origin and endpoint. Surface the misconfigured Access-Control-Allow-Origin from a recent service move.

Updated · published

markdown for agents →

difficulty intermediate · time to value 1 minute · execution on demand

Start from this

Are we getting any CORS errors in production? If yes, group by origin + which endpoint is rejecting them.

Why this matters

CORS regressions are uniquely sneaky. The browser doesn’t throw a normal error — it shows a console message (“blocked by CORS policy”) and the fetch returns TypeError: Failed to fetch with no useful detail. Your server logs show nothing because the request was rejected before it got there.

You usually find out from a developer poking devtools, or from a support email saying “the dashboard won’t load.” By then half a day of paying customers have hit it.

The common cause: a service got moved to a new subdomain, or someone tightened the Access-Control-Allow-Origin list and forgot a caller. Sentry sees TypeError: Failed to fetch but can’t distinguish a CORS rejection from a network failure. Agentry can — if your client wraps fetch and tags the failure mode, the agent groups them and points at the misconfig.

What you get

  • A list of CORS-blocked requests grouped by (origin, endpoint)
  • The deploy SHA that most likely introduced the regression
  • The exact CORS header to add to fix it

Walk through it

You

Are we getting any CORS errors in production? Group by origin and which endpoint is rejecting them.

Agent

Let me query CORS-flagged errors and group them.

Endpoint: POST /v1/projects/:project_id/analytics/query
Exact shape: /v1/openapi.json?path=%2Fv1%2Fprojects%2F%7Bproject_id%7D%2Fanalytics%2Fquery&method=post
Custom HogQL goes in the OpenAPI-defined `query` field.
Concept fields:
  query: "SELECT properties.origin AS origin, properties.endpoint AS endpoint, count() AS cnt, uniq(distinct_id) AS users, min(timestamp) AS first_seen FROM events WHERE event = '$exception' AND properties.cors_blocked = true AND timestamp >= now() - INTERVAL 7 DAY GROUP BY origin, endpoint ORDER BY cnt DESC"
{
  "rows": [
    { "origin": "https://app.acme.io", "endpoint": "https://api.acme.io/v2/billing",
      "cnt": 412, "users": 88, "first_seen": 1747280000 },
    { "origin": "https://docs.acme.io", "endpoint": "https://api.acme.io/v2/search",
      "cnt": 19, "users": 7, "first_seen": 1747340000 }
  ]
}
Agent

Two CORS issues. The first one is your main app app.acme.io being rejected by api.acme.io/v2/billing — 412 blocked requests across 88 users. That endpoint was just moved or had its CORS policy changed. Let me check the deploy timeline.

Endpoint: GET /v1/projects/:project_id/deploys
Exact shape: /v1/openapi.json?path=%2Fv1%2Fprojects%2F%7Bproject_id%7D%2Fdeploys&method=get
Use query parameters, not a JSON body.
Query/filter concept:
  limit: 5
  since: 1747270000
{
  "deploys": [
    { "sha": "9c3a210", "service": "api", "received_at": 1747278000,
      "message": "refactor: split billing into separate microservice" },
    { "sha": "a3f29b1", "service": "app", "received_at": 1747340500,
      "message": "feat: dashboard v2" }
  ]
}
Agent

Confirmed. Deploy 9c3a210 ~30 min before the first CORS rejection split billing into its own service. The new service is missing app.acme.io from its Access-Control-Allow-Origin allowlist.

The second issue (docs.acme.io/v2/search) is more recent and lower volume, likely a forgotten allowlist entry for the new docs subdomain.

Fix is to add both to the API’s CORS config:

Access-Control-Allow-Origin: https://app.acme.io, https://docs.acme.io

Want me to draft the config PR?

The output

CORS-blocked requests (7d)

ORIGIN                  ENDPOINT                              COUNT  USERS  FIRST SEEN
app.acme.io             api.acme.io/v2/billing                  412     88  30m after 9c3a210
docs.acme.io            api.acme.io/v2/search                    19      7  not deploy-related

Diagnosis:
  Deploy 9c3a210 ("split billing into microservice") shipped 30 min before
  the first CORS block on /v2/billing. Almost certainly the new service is
  missing `app.acme.io` from Access-Control-Allow-Origin.

Fix:
  In billing service config (apps/billing/src/middleware/cors.ts):

    + const ALLOWED_ORIGINS = [
    +   "https://app.acme.io",
    +   "https://docs.acme.io",
    + ];

  Or pattern-match: `https://*.acme.io`

Setting it up

CORS errors aren’t a thrown exception in JS — fetch just rejects with TypeError: Failed to fetch. You need to wrap fetch and detect the CORS signature. The trick: if the fetch fails AND the resource didn’t load at all (no response.type), it’s likely CORS.

This browser snippet does not set a User-Agent header: browser fetch attaches its own automatically. Only server, CI, and curl examples should set a custom User-Agent.

async function instrumentedFetch(input: RequestInfo, init?: RequestInit) {
  try {
    return await fetch(input, init);
  } catch (err) {
    // CORS rejection from the browser comes through as a generic TypeError.
    // The dead-giveaway is that the URL is cross-origin AND we have no response.
    const url = typeof input === "string" ? input : input.url;
    const isCrossOrigin = new URL(url).origin !== window.location.origin;
    if (isCrossOrigin && err instanceof TypeError) {
      await fetch(`https://api.agentry.sh/v1/logs/`, {
        method: "POST",
        headers: {
          "Authorization": `Bearer ${AGENTRY_PUBLIC_API_KEY}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          message: err.message,
          tags: {
            cors_blocked: true,
            origin: window.location.origin,
            endpoint: url,
          },
        }),
      });
    }
    throw err;
  }
}

The cors_blocked: true tag is what makes the agent’s query trivial. Without it you’d have to grep TypeError: Failed to fetch and guess.

Variations

  • “Show CORS errors only from authenticated users — anonymous CORS rejections might be probes.”
  • “Which of our subdomains are blocked the most? Maybe we forgot one in the allowlist entirely.”
  • “For the top CORS issue, give me the exact config diff for nginx / Cloudflare Workers / Express.”
  • “After an API deploy, review the next hour’s CORS rejections on demand and draft a #eng-api summary if a new pattern appears.”

Adapt this workflow in your own agent.

Ask your agent to map the starter prompt to your saved signal map, live events, and the current HTTP API before answering.

Agentry gives coding agents production context from errors, product analytics, and deploys through one HTTP API. Fetch and read https://agentry.sh/install.md completely, then install Agentry's full first-time telemetry baseline in this repository. I authorize device-code login, saving the owner key only to ~/.agentry/credentials.json, creating one Agentry project for this repository, and read-only inspection of shipped source plus hidden CI/provider configuration. Deeply analyze the product's business logic and complete value flow before choosing telemetry. Save the complete source-backed plan and show me its exact contents and hash. Until I explicitly approve that plan or request a replacement, do not edit app/CI source, place runtime/CI credentials, commit, exercise product traffic, start proof, push, or deploy. After approval, I authorize only the reviewed targets: place the required scoped browser/server/CI credentials through the established environment or secret mechanism, preserve existing telemetry, implement and test the baseline, commit it, push that reviewed commit when the shipped CI/provider path requires it, exercise safe proof paths with test/non-customer data, and perform one deployment through the reviewed shipped CI/provider path. Ask first if proof would charge money, contact a third party, change real customer data, or require new external access. After the plan is saved, immediately before every onboarding state-changing POST, GET current onboarding state, perform only its single returned next_action, then read state again; do not batch or infer later stages. Continue until status is verified, installation_complete is true, and next_action is null. Keep all secrets, source snapshots, proof markers, and scratch files outside the repository.

+ Full access
5.5 Extra High
  1. 1. Open your repo in Codex, Claude Code, Cursor etc.
  2. 2. Paste the install prompt.
  3. 3. Your agent reads the install doc and shows you an implementation plan for approval.