3 min read

cURL Works but fetch Fails: Compare CORS, Cookies and JSON

Convert a cURL request to fetch, compare method, headers and body, and separate browser CORS failures from HTTP errors and JSON parsing failures.

Maintained by Ali Aygör. Report a correction.

Start with a request you can compare

A successful terminal request and a failing browser request may reach the same URL through different rules. Before changing the server, compare the URL, method, request headers and body. Replace real credentials with placeholders in your investigation notes. The example below uses a synthetic order and an example.com URL; it is a conversion fixture, not a working order service.

Paste it into cURL to Fetch and check that POST, the JSON content type and the two body fields survive conversion. Compare the same command with cURL to Java if a backend integration works while the frontend fails. The converters generate code; they do not send this request or implement every cURL option.

curl 'https://example.com/v1/orders' \
  -X POST \
  -H 'Content-Type: application/json' \
  -H 'X-Request-ID: demo-order-42' \
  --data-raw '{"sku":"demo","quantity":2}'

Find whether the browser sent the POST

Inspect the browser Network panel before retrying. A cross-origin JSON POST commonly has an OPTIONS preflight. If the browser rejects that preflight, investigate the allowed origin, method and requested headers. If POST received a 500, examine that server error as well; a missing CORS header can keep JavaScript from reading its body.

The terminal client does not apply browser CORS checks. Adding mode: 'no-cors' is not a way to read the response: it produces an opaque response with inaccessible content. A credentialed cross-origin request needs an explicit allowed origin and credential permission, rather than a wildcard origin. Use the HAR analyzer to distinguish an absent recorded response from a real HTTP error.

Compare authentication separately

Check whether the original request used a cookie, an Authorization header, a client certificate or another mechanism. A conversion that preserves the JSON body still cannot manufacture the browser's authentication state. Browser cookies and forbidden request headers have their own rules; copying a Cookie header into fetch is not a replacement for those rules.

For an authorized cookie-based integration, review the fetch credentials setting and the cookie's SameSite and Secure attributes alongside the server configuration. Do not add credentials to unrelated endpoints simply to make an error disappear. Keep the original and converted request beside one another so a missing header or changed body is visible.

Test HTTP status and JSON parsing independently

The converter's generated response.json() call is a starting point. Add handling that matches the real API: fetch can resolve with a 404 or 500, a 204 has no JSON body, and a proxy may send HTML. The function below accepts a Response, so the checks can run with synthetic responses without contacting any service.

The tests should cover a successful object, an empty 204, an HTTP failure and an HTML response. Record which layer fails: request construction, browser access, HTTP status or parsing. That record is more useful than treating every failure as a CORS problem.

async function readJsonResponse(response) {
  if (!response.ok) throw new Error("HTTP " + response.status);
  if (response.status === 204) return null;
  const type = (response.headers.get("content-type") ?? "")
    .split(";")[0].trim().toLowerCase();
  if (type !== "application/json" && !type.endsWith("+json")) {
    throw new Error("Expected JSON, received " + (type || "no content type"));
  }
  return response.json();
}

const demo = new Response('{"ok":true}', {
  status: 200, headers: { "Content-Type": "application/json" }
});
console.log(await readJsonResponse(demo)); // { ok: true }

Verify the same failing case after the fix

Repeat the original browser action with the same redacted payload and expected authentication state. Confirm that the application request runs, JavaScript can read the response and the response matches the contract. If the service returns an error object, preserve its request identifier in the debugging record rather than repeatedly submitting an order.

Keep a small fixture for request conversion and separate tests for status and parsing. These checks catch future body or header changes even when the endpoint happens to be available during a manual test.

Specifications and references

Continue the investigation

Related tools

Browse all developer tools

Related workflows