GuidesCAPACITOR
Capacitor and Ionic CORS errors
A Capacitor app's web code runs in a WebView with the origin capacitor://localhost on iOS and https://localhost on Android (Capacitor 6 and later), so an API without CORS headers fails on a device exactly as in a browser. The native fix is CapacitorHttp, which moves fetch and XMLHttpRequest onto native networking, but it is off by default and web or PWA builds still need CORS. https://proxy.cors.dev/ answered all 5 app and dev origins tested for GET with no signup (about 600 requests a minute per IP), and cors.dev Pro ($5 a month for 500,000 requests) accepts capacitor://localhost and ionic://localhost as registered origins for POST and auth headers.
Why a Capacitor app hits CORS
A Capacitor app renders its web code in a native WebView that serves your files from a local origin. Capacitor's configuration defaults are server.hostname localhost, server.iosScheme capacitor and server.androidScheme https, so the app's origin is capacitor://localhost on iOS and https://localhost on Android.
Capacitor 6 made https the default androidScheme for existing apps; on Capacitor 5 and earlier the Android origin was http://localhost unless configured. Capacitor's docs say iosScheme cannot be set to a scheme that WKWebView already handles, such as http or https.
Ionic's CORS guide lists two more origins: ionic://localhost for Cordova apps on iOS using cordova-plugin-ionic-webview 3.x, and http://localhost:8100 for ionic serve. Its table still shows http://localhost for Capacitor on Android, the default before Capacitor 6.
fetch() runs inside the WebView, so an API that sends no Access-Control-Allow-Origin for these origins fails on a device the same way it fails in a desktop browser.
| Where the app runs | Origin | Set by | proxy.cors.dev free answers |
|---|---|---|---|
| Capacitor on iOS | capacitor://localhost | server.iosScheme and server.hostname | * |
| Capacitor 6 or later on Android | https://localhost | server.androidScheme and server.hostname | Echoes the origin |
| Capacitor 5 or earlier on Android | http://localhost | server.androidScheme | Echoes the origin |
| Cordova with ionic-webview 3.x on iOS | ionic://localhost | Plugin default | * |
| ionic serve | http://localhost:8100 | Dev server | Echoes the origin |
Option 1: CapacitorHttp native requests
CapacitorHttp is bundled with @capacitor/core. Capacitor's docs: it "provides native http support via patching fetch and XMLHttpRequest to use native libraries". Native requests are not subject to CORS. The patch is off by default; set plugins.CapacitorHttp.enabled to true in capacitor.config.ts.
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.example.news',
appName: 'News',
webDir: 'dist',
plugins: {
CapacitorHttp: {
enabled: true,
},
},
};
export default config;The trade-offs, from the Capacitor and Ionic docs:
- Whole app: The patch replaces
fetchandXMLHttpRequestfor every request in the app, including those from third-party libraries. - Web builds: On the web it falls back to the browser's own
fetch, so a PWA orionic servestill needs CORS. Ionic's guide says the same for any web context. - Request bodies: When you call
CapacitorHttp.request()directly,datatakes only a string or JSON on native. - Large files: Transfers belong in
@capacitor/file-transferwith@capacitor/filesystem.
server.url is not a fix either. It points the WebView at a remote URL for live reload, and Capacitor's docs say it is not intended for use in production.
Option 2: one proxy for every platform
The BBC World RSS feed (https://feeds.bbci.co.uk/news/world/rss.xml, 21,220 bytes, text/xml) sends no Access-Control-Allow-Origin. On 28 October 2026 it answered 200 through https://proxy.cors.dev/ for every origin we sent: Access-Control-Allow-Origin: * for capacitor://localhost and ionic://localhost, and the exact origin echoed for https://localhost, http://localhost and http://localhost:8100.
The proxy works with or without CapacitorHttp, and the same code runs on iOS, Android, ionic serve and a deployed PWA. With CapacitorHttp on, native builds no longer need the proxy, but the web build still does. The snippet returns 10 headlines parsed with DOMParser.
const FEED = 'https://feeds.bbci.co.uk/news/world/rss.xml';
export async function loadHeadlines() {
const response = await fetch('https://proxy.cors.dev/' + FEED);
if (!response.ok) {
throw new Error(response.headers.get('X-Cors-Error') ?? `HTTP ${response.status}`);
}
const xml = new DOMParser().parseFromString(await response.text(), 'text/xml');
return [...xml.querySelectorAll('item')].slice(0, 10).map((item) => ({
title: item.querySelector('title')?.textContent ?? '',
link: item.querySelector('link')?.textContent ?? '',
published: item.querySelector('pubDate')?.textContent ?? '',
}));
}The free tier needs no signup and no key. The proxy answers the OPTIONS preflight itself with 204, and when it refuses a request, the X-Cors-Error response header names the reason.
Origins to register for cors.dev Pro
POST, PUT, PATCH and DELETE, request bodies up to 1 MiB, and Authorization or custom headers need cors.dev Pro. A Connection accepts app origins as well as web origins.
- Start the trial
Sign up with Google. The 7-day trial includes 1,000 requests and needs no card.
- List your origins
capacitor://localhost,https://localhostandionic://localhostas your builds need, plushttp://localhost:8100forionic serveand your PWA'shttpsorigin, up to 10 in total. - List the hosts and send the key
Enable up to 20 target hosts and send the publishable key in an
X-Cors-Keyheader.
Origin: null never matches a Connection, and an unlisted origin answers 403 origin_not_allowed. Every Capacitor app on iOS shares capacitor://localhost, and every Capacitor 6 or later app on Android shares https://localhost, so an origin check cannot tell your app from someone else's. The list of target hosts is what limits the key. Keep secret API keys on a server: anyone can unpack an app and read the key.
Limits on each tier
The free tier allows about 600 requests per minute per IP and answers 429 with a Retry-After header that JavaScript can read; there is no monthly quota. It refuses Authorization, and header names containing token, apikey, api-key, api_key, secret or password or ending in -key, with 400 header_not_allowed, and drops other custom headers, including names containing csrf or xsrf, listing them in X-Cors-Dropped-Headers.
| Limit | Free, no key | Pro, $5 a month |
|---|---|---|
| Price | Free, no signup | $5 a month, 7-day trial with 1,000 requests |
| Methods | GET, HEAD | GET, HEAD, POST, PUT, PATCH, DELETE |
| Monthly requests | No quota | 500,000 |
| Throughput | About 600 a minute per IP | 3,000 a minute per account, 100 in flight |
| Request headers | Accept, Accept-Language, If-None-Match, If-Modified-Since, Range | Adds Authorization, Content-Type, configured custom headers |
| Page origins | Any | Up to 10 exact origins per Connection |
| Response size and deadline | 6 MiB, 30 seconds | Same as free |
| Caching | None | GET up to 300 seconds, when the upstream allows it |
Good questions.
Why does my Ionic app work in ionic serve but fail on an iPhone?
The origin changes. ionic serve runs in a desktop browser on http://localhost:8100, while the iPhone build sends capacitor://localhost, or ionic://localhost on Cordova with cordova-plugin-ionic-webview 3.x. An API that allows the first origin but not the second blocks the device build.
Did Capacitor 6 change the Android origin?
Yes. Capacitor 6 made https the default server.androidScheme for existing apps, so the default Android origin is https://localhost. On Capacitor 5 and earlier it was http://localhost unless you configured it, which is the value Ionic's CORS table still shows.
Can server.url fix CORS in a production app?
No. server.url loads a remote URL in the WebView for live reload, and Capacitor's docs say it is not intended for use in production. Fix CORS with CapacitorHttp, an API that sends the right headers, or a proxy.
Does CapacitorHttp work in a PWA build?
Not as a CORS fix. On the web, CapacitorHttp uses the browser's own fetch, so a PWA build still needs CORS, as Ionic's guide notes. A proxy prefix such as https://proxy.cors.dev/ keeps one fetch() call working across iOS, Android and the PWA.
Is it safe to register capacitor://localhost on a Connection?
It works, but it does not identify your app: every Capacitor iOS app sends capacitor://localhost, and the X-Cors-Key inside the app is readable by anyone who unpacks it. On cors.dev Pro, the Connection's list of up to 20 target hosts restricts what that key can reach. Secret API keys belong on your own server.