slop audit
Blog post 57 / Browser checks

CORS errors: debug failed API requests before launch

Fix CORS errors on deployed websites: inspect origins, preflight requests, credentials, and response headers, then verify browser access before launch.

By Slop Audit · 8 min read

CORS errors occur when a browser cannot complete the cross-origin sharing checks needed for JavaScript to access a response. A deployed frontend can load correctly while its pricing, search, or project list stays empty. Start with the exact failed request and repair the API response policy or deployment configuration that caused it.

This guide covers browser API requests on websites you own. It shows how to inspect the failure, test preflight responses, handle credentials, and keep a browser check in CI. The Fetch Standard's CORS protocol defines the response-sharing mechanism; CORS does not replace authentication, authorization, or CSRF protection.

CORS errors troubleshooting checklist

  1. Record the deployed page origin and exact API URL.
  2. Read the browser's full Console message and matching Network entry.
  3. Check whether an OPTIONS preflight failed before the real request.
  4. Inspect CORS headers on both the preflight and actual response.
  5. Check credential mode, redirects, error responses, and CDN behavior.
  6. Retest from the real frontend origin with browser protections enabled.
  7. Save the request contract and resulting UI state as release evidence.

Use the JavaScript console errors guide when the page also throws exceptions. A generic TypeError: Failed to fetch needs the browser's network details before you can assign a cause.

Identify the origin and the actual failure

An origin consists of a scheme, host, and port. https://app.example.com and https://api.example.com are different origins, even though they share a parent domain. Local ports such as http://localhost:5173 and http://localhost:3000 also differ. Paths do not create a new origin. See MDN's origin definition.

A development proxy can hide this distinction by forwarding /api locally. After deployment, the built frontend may call a separate API directly. Compare the Network request URL with the intended production configuration, then rebuild if the API URL was embedded in the bundle.

In Chrome DevTools, clear the Network log, enable Preserve log when navigation is involved, reload, and perform the failing action. Inspect the request's initiator, method, Origin, status, response headers, and redirects. Chrome's Network reference documents these views. Save the exact Console reason as well; MDN's CORS error reference maps specific messages to failed checks.

Distinguish nearby problems before changing policy:

  • An HTTPS page calling an HTTP API may have a mixed content error.
  • A connect-src violation needs CSP testing.
  • An API returning 401 or 500 needs authentication or server investigation, even if missing CORS headers also hide that response from JavaScript.
  • A DNS or certificate failure needs transport repair before response headers can help.

Fix Access-Control-Allow-Origin on the API response

CORS permission belongs on the response from the resource being accessed. Adding Access-Control-Allow-Origin to your frontend's fetch request does not grant permission.

For an API restricted to one frontend, an illustrative response is:

HTTP/1.1 200 OK
Content-Type: application/json
Access-Control-Allow-Origin: https://app.example.com
Vary: Origin

The allowed origin has no path or trailing slash. If you support several origins, compare the incoming Origin against an explicit allowlist and return the single matching origin. Do not return a comma-separated list or reflect arbitrary origins. A wildcard is appropriate only when the resource is intentionally readable by any origin without credentials. MDN's Access-Control-Allow-Origin reference explains these values and the Vary: Origin cache signal.

Choose one response layer to own this policy. An application and reverse proxy can accidentally add duplicate allow-origin headers, which browsers reject. The multiple allow-origin error reference describes that failure. Inspect the public response through the CDN after deployment, including allowed-origin error responses.

If you do not control the API, use the provider's supported browser integration or a server-side integration you own. Keep server credentials on the server. A proxy should have fixed upstream routes, authentication where needed, and bounded inputs; avoid building an arbitrary public URL forwarder.

Diagnose a failed CORS preflight request

Some cross-origin requests require an OPTIONS check before the actual request. A JSON POST or a request with an Authorization header commonly triggers this. MDN's CORS guide explains which request methods and headers require preflight.

Inspect the exact request from DevTools. For a JSON endpoint you own, reproduce its preflight headers without submitting application data:

curl --silent --show-error --dump-header - --output /dev/null \
  --request OPTIONS 'https://api.example.com/projects' \
  --header 'Origin: https://app.example.com' \
  --header 'Access-Control-Request-Method: POST' \
  --header 'Access-Control-Request-Headers: content-type'

An illustrative response for that contract is:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: Content-Type
Vary: Origin

The permitted method and headers must match the intended request. Allow: POST is not a replacement for Access-Control-Allow-Methods; see the allowed-methods reference. If the real request uses Authorization, explicitly include it in the allowed header list. The allowed-headers reference explains its special wildcard restriction.

Check whether routing, authentication middleware, or a CDN challenge intercepts OPTIONS. The preflight has no application cookies or bearer token; it asks permission for the later request. Handle that policy check without weakening authentication on the actual endpoint. MDN's preflight request reference describes the exchange and its separate browser cache.

A successful preflight is only half the check. The subsequent POST response still needs the appropriate allow-origin header. Reproduce the actual write only in a controlled environment with test data. Curl exposes server behavior but does not enforce browser CORS, so finish in the browser.

Check credentials and readable response headers

If a cross-origin fetch deliberately includes cookies, its client configuration might contain:

const response = await fetch("https://api.example.com/account", {
  credentials: "include",
});

For an allowed frontend, both the relevant preflight response and actual response need the credential permission:

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Vary: Origin

A credentialed request cannot use Access-Control-Allow-Origin: *. Adding credentials: "include" alone does not fix the server policy. MDN's allow-credentials reference covers this contract. Cookies also remain subject to their attributes and browser restrictions; use the HTTP cookies guide when CORS succeeds but a session is absent.

If the body is readable but a custom response header is missing in JavaScript, check header exposure. For example, an API can expose a request identifier with Access-Control-Expose-Headers: X-Request-Id. This is separate from allowing request headers. See the expose-headers reference.

Do not use mode: "no-cors" to repair a JSON API integration. It produces an opaque response whose body and headers JavaScript cannot read, as documented in the Request mode reference. Disabling browser security or installing a bypass extension also removes the conditions visitors will use.

Add browser CORS testing to CI

Use a real browser context from the frontend origin. A passing server-side API request does not establish that frontend JavaScript can read the response.

This Playwright example assumes a public, read-only catalog endpoint returning a JSON array. Replace both URLs and the data contract with your own. Keep the API live during this check rather than fulfilling it with a mock:

import { expect, test } from "@playwright/test";

test("frontend can read the public catalog API", async ({ page }) => {
  await page.goto("https://app.example.com/");

  const result = await page.evaluate(async (apiUrl) => {
    try {
      const response = await fetch(apiUrl, {
        mode: "cors",
        credentials: "omit",
        signal: AbortSignal.timeout(10_000),
      });
      const body = await response.json();
      return { status: response.status, isArray: Array.isArray(body), error: "" };
    } catch (error) {
      return { status: 0, isArray: false, error: String(error) };
    }
  }, "https://api.example.com/catalog");

  expect(result.error).toBe("");
  expect(result.status).toBe(200);
  expect(result.isArray).toBe(true);
});

Playwright's evaluation guide explains why page.evaluate executes inside the page. This test checks one anonymous GET; it does not exercise a preflighted write or credentialed session. Add those as separate controlled flows where your application requires them, and assert the visible result of the real user action too.

Retain a trace on failure. A rejected fetch can also mean CSP, DNS, TLS, or connectivity trouble, so label the failure as browser API access until the trace confirms CORS. Add the check to your website regression testing routine and rerun after API, proxy, hostname, or deployment changes.

Check the public release with Slop Audit

After repairing the API path, run Slop Audit on the deployed frontend URL. Its bounded mobile and desktop browser runs collect error-level console messages, uncaught page errors, and failed requests attributed to the submitted hostname or its subdomains. Use the report to capture remaining runtime findings alongside layout, accessibility, metadata, and crawl evidence.

Slop Audit does not validate a complete CORS allowlist, execute arbitrary API requests, sign in, or test every interaction. Its browser run constrains external domains, so a third-party API failure may reflect scan coverage rather than your visitors' CORS behavior. Keep your own browser test for external APIs, preflighted actions, credentials, and denied origins. Compare a rescan after the fix with the website audit report template so each remaining finding has an owner and a reproducible next step.