GuidesJQUERY

Fix jQuery $.ajax CORS errors

A jQuery $.ajax or $.getJSON call to another domain lands in the fail callback with status 0, textStatus "error" and no responseText when the server sends no Access-Control-Allow-Origin header; the real reason is in the browser console, not in the jqXHR object. For public GET requests, put https://proxy.cors.dev/ in front of the URL: free, no signup, no API key, JSON and XML responses up to 1 MiB within 10 seconds. crossDomain: true does not bypass CORS, and JSONP only works when the API supports a callback parameter.

The error: status 0 and an empty response

jQuery uses XMLHttpRequest, and the browser gives XMLHttpRequest no detail about a cross-origin response it refused to deliver: the request errors with status 0, which is the same value you get for a network failure or an aborted request. Open the browser console: a CORS block names the URL, your origin and the missing or wrong header. Status code: 200 in the Firefox version of the message means the server answered and only the header was missing, which is the normal case for a public API that was never meant for browsers.

What the fail callback receives
$.getJSON('https://store.steampowered.com/api/appdetails?appids=440')
  .fail(function (jqXHR, textStatus, errorThrown) {
    console.log(jqXHR.status);       // 0
    console.log(textStatus);         // "error"
    console.log(errorThrown);        // ""
    console.log(jqXHR.responseText); // undefined
  });

// The reason is only in the browser console:
// Access to XMLHttpRequest at 'https://store.steampowered.com/api/appdetails?appids=440'
// from origin 'https://www.yoursite.com' has been blocked by CORS policy:
// No 'Access-Control-Allow-Origin' header is present on the requested resource.

jQuery options that do not help

  • crossDomain: true only tells jQuery to treat the request as cross-origin, which it already detects from the URL. It changes no header and asks the browser for no permission.
  • headers: { ... } with anything beyond Accept and a simple Content-Type turns the call into a preflighted request: the browser sends OPTIONS first, and an API that ignores OPTIONS now fails twice. See response to preflight request doesn't pass access control check.
  • xhrFields: { withCredentials: true } makes things stricter: the response must then name your exact origin, not *, and add Access-Control-Allow-Credentials: true. Leave it off unless the API uses cookies you own.
  • dataType: 'jsonp' is not a CORS workaround for arbitrary APIs; it loads the URL as a script tag, so the server must wrap its JSON in your callback function. APIs without that answer plain JSON and the browser reports a syntax error.
  • Setting Access-Control-Allow-Origin in the request is meaningless: it is a response header, and only the server can send it.

Public APIs: prefix a proxy

When the API is not yours, the header has to come from somewhere else. A CORS proxy fetches the API from its own servers and relays the response with Access-Control-Allow-Origin set to your origin. With cors.dev the change is the URL and nothing else in your $.ajax call. The example reads the Steam store API, which returns JSON and sends no CORS header.

Prefix the proxy for public APIs
var PROXY = 'https://proxy.cors.dev/';

$.ajax({
  url: PROXY + 'https://store.steampowered.com/api/appdetails?appids=440',
  dataType: 'json',
  xhrFields: { withCredentials: false },
})
  .done(function (data) {
    var game = data['440'].data;
    $('#game').text(game.name + (game.is_free ? ' is free to play' : ' is a paid game'));
  })
  .fail(function (jqXHR) {
    // X-Cors-Error is set when the proxy refused or could not complete the request
    var code = jqXHR.getResponseHeader('X-Cors-Error') || jqXHR.status || 'network error';
    $('#game').text('Could not load the game: ' + code);
  });

Anonymous requests cover GET and HEAD to any public HTTPS host and forward only Accept, If-None-Match and If-Modified-Since; cookies are never forwarded, so withCredentials stays false. Responses keep the upstream status and Content-Type, so dataType: 'json' and dataType: 'xml' behave as they would against the API directly. A proxy-side failure sets X-Cors-Error, for example response_too_large above 1 MiB.

JSONP: still fine for the APIs that offer it

Before CORS existed, APIs offered JSONP: you pass a callback name and the server answers callback({...}) as a script. jQuery handles the plumbing when you set dataType: 'jsonp'. It still works where supported, GET only, and it runs whatever the server returns as code on your page, so use it for sources you trust.

JSONP: only when the API supports a callback parameter
// Works: the MediaWiki API wraps the JSON in your callback function
$.ajax({
  url: 'https://en.wikipedia.org/w/api.php',
  data: { action: 'query', prop: 'extracts', exintro: 1, titles: 'JQuery', format: 'json' },
  dataType: 'jsonp',
}).done(function (data) {
  console.log(Object.values(data.query.pages)[0].extract);
});

// Does not work: an API without JSONP support answers plain JSON,
// the browser tries to run it as a script and reports "Unexpected token ':'"

Your own API: send the headers

If you control the server, add Access-Control-Allow-Origin for your site origin (or * for public data), list the methods and request headers your calls use, and answer OPTIONS with 204. Framework guides: Express, Flask, Django, Laravel, Spring Boot. The full header reference is in CORS headers list.

Limits and pricing

cors.dev limits for jQuery calls
LimitFree, no keyPro, $5 per month
Price$0$5 per month; 7-day trial with 1,000 requests, no card
MethodsGET and HEADGET, HEAD, POST, PUT, PATCH and DELETE
DestinationsAny public HTTPS hostPublic HTTPS hosts enabled on your Connection
Response size1 MiB, text types only6 MiB, any content type
Request bodyNone1 MiB
Upstream deadline10 seconds10 seconds
RateShared pool with fair-use limits600 requests per minute, 10 concurrent per account
Monthly requestsNo quota500,000 per billing period, $5 per extra 500,000
Request headers forwardedAccept, If-None-Match, If-Modified-SincePlus Authorization and custom headers
Cookies forwardedNeverNever

Good questions.

Why is jqXHR.status 0 instead of a real status code?

The browser blocked the response before XMLHttpRequest could read it, so jQuery never sees a status. Status 0 also covers network failures and aborted requests, which is why the browser console, not the callback, tells you what happened.

Why does $.getJSON work for some APIs and not others?

The ones that work send Access-Control-Allow-Origin: * or your origin; api.github.com and most APIs built for browsers do. The ones that fail send nothing. Check with curl -sI -H "Origin: https://yoursite.com" <api-url> | grep -i access-control.

Does the jQuery version matter?

No. Every jQuery since 1.5 makes the same XMLHttpRequest, and CORS is enforced by the browser, not the library. The fixes are identical for jQuery 1, 2, 3 and 4.

Can I send an Authorization header through the proxy?

Not anonymously: Authorization and custom headers are rejected with header_not_allowed. A Pro Connection forwards them for the hosts you enable. Remember that anything in page JavaScript is visible to visitors, so send per-user tokens, never a shared secret.

Is JSONP safe?

It executes the response as a script on your page, so only use it with APIs you trust, and only for GET. For public data, a CORS proxy returns plain JSON that your code parses, without running anything.

What about $.ajax with type POST to another domain?

A cross-origin POST needs the same headers, and a JSON body triggers a preflight. For your own API, fix the server. For third-party APIs, a Pro Connection forwards POST, PUT, PATCH and DELETE with bodies up to 1 MiB.