CORS Errors

Contents

What is CORS?

Cross-Origin Resource Sharing (CORS) is a mechanism that browsers and webviews — like the ones powering Capacitor and Cordova — use to restrict HTTP and HTTPS requests made from scripts to resources in a different origin for security reasons, mainly to protect your user's data and prevent attacks that would compromise your app.

In order to know if an external origin supports CORS, the server has to send some special headers for the browser to allow the requests.

An origin is the combination of the protocol, domain, and port from which your Ionic app or the external resource is served. For example, apps running in Capacitor have capacitor://localhost (iOS) or http://localhost (Android) as their origin.

When the origin where your app is served (e.g. http://localhost:8100 with ionic serve) and the origin of the resource being requested (e.g. https://api.example.com) don't match, the browser's Same Origin Policy takes effect and CORS is required for the request to be made.

CORS errors are common in web apps when a cross-origin request is made but the server doesn't return the required headers in the response (is not CORS-enabled):

XMLHttpRequest cannot load https://api.example.com. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:8100' is therefore not allowed access.

How does CORS work

Request with preflight

By default, when a web app tries to make a cross-origin request the browser sends a preflight request before the actual request. This preflight request is needed in order to know if the external resource supports CORS and if the actual request can be sent safely, since it may impact user data.

A preflight request is sent by the browser if:

  • The method is:
    • PUT
    • DELETE
    • CONNECT
    • OPTIONS
    • TRACE
    • PATCH
  • Or if it has a header other than:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width
  • Or if it has a Content-Type header other than:
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain
  • Or if a ReadableStream or event listeners in XMLHttpRequestUpload are used.

If any of the conditions above are met, a preflight request with the OPTIONS method is sent to the resource URL.

Let's suppose we are making a POST request to a fictional JSON API at https://api.example.com with a Content-Type of application/json. The preflight request would be like this (some default headers omitted for clarity):

OPTIONS / HTTP/1.1
Host: api.example.com
Origin: http://localhost:8100
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type

If the server is CORS enabled, it will parse the Access-Control-Request-* headers and understand that a POST request is trying to be made from http://localhost:8100 with a custom Content-Type.

The server will then respond to this preflight with which origins, methods, and headers are allowed by using the Access-Control-Allow-* headers:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:8100
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type

If the returned origin and method don't match the ones from the actual request, or any of the headers used are not allowed, the request will be blocked by the browser and an error will be shown in the console. Otherwise, the request will be made after the preflight.

In our example, since the API expects JSON, all POST requests will have a Content-Type: application/json header and always be preflighted.

Simple requests

Some requests are always considered safe to send and don't need a preflight if they meet all of the following conditions:

  • The method is:
    • GET
    • HEAD
    • POST
  • Have only these headers:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width
  • The Content-Type header is:
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain
  • No ReadableStream or event listeners in XMLHttpRequestUpload are used.

In our example API, GET requests don't need to be preflighted because no JSON data is being sent, and so the app doesn't need to use the Content-Type: application/json header. They will always be simple requests.

CORS Headers

Server Headers (Response)

Header Value Description
Access-Control-Allow-Origin origin or * Specifies the origin to be allowed, like http://localhost:8100 or * to allow all origins.
Access-Control-Allow-Methods methods Which methods are allowed when accessing the resource: GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH.
Access-Control-Allow-Headers headers Used in response to a preflight request to indicate which headers can be used when making the actual request, aside from the simple headers, which are always allowed.
Access-Control-Allow-Credentials true or false Whether or not the request can be made with credentials.
Access-Control-Expose-Headers headers Specifies the headers that the browser is allowed to access.
Access-Control-Max-Age seconds Indicates how long the results of a preflight request can be cached.

Browser Headers (Request)

The browser automatically sends the appropriate headers for CORS in every request to the server, including the preflight requests. Please note that the headers below are for reference only, and should not be set in your app code (the browser will ignore them).

All Requests

Header Value Description
Origin origin Indicates the origin of the request.

Preflight Requests

Header Value Description
Access-Control-Request-Method method Used to let the server know what method will be used when the actual request is made.
Access-Control-Request-Headers headers Used to let the server know what non-simple headers will be used when the actual request is made.

Solutions for CORS Errors

A. Enabling CORS in a server you control

The correct and easiest solution is to enable CORS by returning the right response headers from the web server or backend and responding to preflight requests, as it allows to keep using XMLHttpRequest, fetch, or abstractions like HttpClient in Angular.

Ionic apps may be run from different origins, but only one origin can be specified in the Access-Control-Allow-Origin header. Therefore we recommend checking the value of the Origin header from the request and reflecting it in the Access-Control-Allow-Origin header in the response.

Please note that all of the Access-Control-Allow-* headers have to be sent from the server, and don't belong in your app code.

Here are some of the origins your Ionic app may be served from:

Capacitor

Platform Origin
iOS capacitor://localhost
Android http://localhost

Replace localhost with your own hostname if you have changed the default in the Capacitor config.

Ionic WebView 3.x plugin on Cordova

Platform Origin
iOS ionic://localhost
Android http://localhost

Replace localhost with your own hostname if you have changed the default in the plugin config.

Ionic WebView 2.x plugin on Cordova

Platform Origin
iOS http://localhost:8080
Android http://localhost:8080

Replace port 8080 with your own if you have changed the default in the plugin config.

Local development in the browser

Command Origin
ionic serve http://localhost:8100 or http://YOUR_MACHINE_IP:8100
npm run start or ng serve http://localhost:4200 for Ionic Angular apps.

Port numbers can be higher if you are serving multiple apps at the same time.


Allowing any origin with Access-Control-Allow-Origin: * is guaranteed to work in all scenarios but may have security implications — like some CSRF attacks — depending on how the server controls access to resources and use sessions and cookies.

For more information on how to enable CORS in different web and app servers, please check enable-cors.org

CORS can be easily enabled in Express/Connect apps with the cors middleware:

const express = require('express');
const cors = require('cors');
const app = express();

const allowedOrigins = [
  'capacitor://localhost',
  'ionic://localhost',
  'http://localhost',
  'http://localhost:8080',
  'http://localhost:8100'
];

// Reflect the origin if it's in the allowed list or not defined (cURL, Postman, etc.)
const corsOptions = {
  origin: (origin, callback) => {
    if (allowedOrigins.includes(origin) || !origin) {
      callback(null, true);
    } else {
      callback(new Error('Origin not allowed by CORS'));
    }
  }
}

// Enable preflight requests for all routes
app.options('*', cors(corsOptions));

app.get('/', cors(corsOptions), (req, res, next) => {
  res.json({ message: 'This route is CORS-enabled for an allowed origin.' });
})

app.listen(3000, () => {
  console.log('CORS-enabled web server listening on port 3000');
});

B. Working around CORS in a server you can't control

Don't leak your keys!

If you are trying to connect to a 3rd-party API, first check in its documentation that is safe to use it directly from the app (client-side) and that it won't leak any secret/private keys or credentials, as it's easy to see them in clear text in Javascript code. Many APIs don't support CORS on purpose, in order to force developers to use them in the server and protect important information or keys.

1. Native-only apps (iOS/Android)

Use the HTTP plugin from Ionic Native to make the requests natively from outside the webview. Please note that this plugin doesn't work in the browser, so the development and testing of the app must always be done in a device or simulator going forward.

Usage in Ionic Angular 4
import { Component } from '@angular/core';
import { HTTP } from '@ionic-native/http/ngx';

@Component({
  selector: 'app-home',
  templateUrl: './home.page.html',
  styleUrls: ['./home.page.scss'],
})
export class HomePage {
  constructor(private http: HTTP) {}

  async getData() {
    try {
      const url = 'https://api.example.com';
      const params = {};
      const headers = {};

      const response = await this.http.get(url, params, headers);

      console.log(response.status);
      console.log(JSON.parse(response.data)); // JSON data returned by server
      console.log(response.headers);

    } catch (error) {
      console.error(error.status);
      console.error(error.error); // Error message as string
      console.error(error.headers);
    }
  }
}

2. Native + PWAs

Send the requests through an HTTP/HTTPS proxy that bypasses them to the external resources and adds the necessary CORS headers to the responses. This proxy must be trusted or under your control, as it will be intercepting most traffic made by the app.

Also, keep in mind that the browser or webview will not receive the original HTTPS certificates but the one being sent from the proxy if it's provided. URLs may need to be rewritten in your code in order to use the proxy.

Check cors-anywhere for a Node.js CORS proxy that can be deployed in your own server. Using free hosted CORS proxies in production is not recommended.

C. Disabling CORS or browser web security

Please be aware that CORS exists for a reason (security of user data and to prevent attacks against your app). It's not possible or advisable to try to disable CORS.

Older webviews like UIWebView on iOS don't enforce CORS but are deprecated and are very likely to disappear soon. Modern webviews like iOS WKWebView or Android WebView (both used by Capacitor) do enforce CORS and provide huge security and performance improvements.

If you are developing a PWA or testing in the browser, using the --disable-web-security flag in Google Chrome or an extension to disable CORS is a really bad idea. You will be exposed to all kind of attacks, you can't ask your users to take the risk, and your app won't work once in production.

Sources
Contributors
Native Errors Developer Tips