GuidesFLUTTER WEB

Fix the Flutter web CORS error

On Flutter web, package:http throws ClientException: XMLHttpRequest error and Dio reports a connection error when the API you call sends no Access-Control-Allow-Origin header; the same code works on Android, iOS and desktop because only browsers enforce CORS. For public GET requests, put https://proxy.cors.dev/ in front of the URL on web builds: free, no signup, no API key, JSON and XML responses up to 1 MiB within 10 seconds. For local development you can start Chrome without web security, and for an API you own you add the headers on the server.

The error and where the real message is

Dart on the web sends HTTP through the XMLHttpRequest of the browser, and the browser tells your code nothing about a CORS failure except that the request errored. The reason is in the DevTools console of the browser running your app, never in the Dart exception. Open it before changing any code: a missing header, a failed preflight and a network failure all look identical from Dart.

What the failure looks like on web
# package:http
ClientException: XMLHttpRequest error., uri=https://store.steampowered.com/api/appdetails?appids=440

# Dio
DioException [connection error]: The connection errored: The XMLHttpRequest onError callback was called.

# Browser DevTools console (the real reason; Dart never sees it)
Access to XMLHttpRequest at 'https://store.steampowered.com/api/appdetails?appids=440'
from origin 'http://localhost:5000' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

Check whether the API sends the header at all with curl -sI -H "Origin: http://localhost:5000" <api-url> | grep -i access-control-allow-origin. No output means every browser will block the response, no matter which Flutter package you use. If the message mentions a preflight instead, the request carried a custom header or a non-simple method and the API did not answer OPTIONS correctly; see preflight request.

Why only the web build fails

The same-origin policy is a browser rule: a page on http://localhost:5000 may read a response from https://store.steampowered.com only if that response names that page origin or * in Access-Control-Allow-Origin. Mobile and desktop Flutter builds talk to the network directly and have no such rule, which is why the bug appears the day you first run flutter run -d chrome. Nothing in Flutter can switch it off: the browser controls the request headers, and dart:io is unavailable on web.

Development: a Chrome profile without web security

While you build, you can run the debug browser with the same-origin policy off. flutter run passes flags to Chrome with --web-browser-flag, and Chrome honours --disable-web-security only together with a non-default --user-data-dir. Use a throwaway directory so your normal profile keeps its protections.

Development only: run Chrome without web security
# Chrome ignores --disable-web-security unless it also gets a separate profile directory
flutter run -d chrome \
  --web-browser-flag "--disable-web-security" \
  --web-browser-flag "--user-data-dir=/tmp/flutter-no-cors"

# This changes nothing for your users: a release build still runs in their normal browser.

This is a workaround for your machine only. Your users run release builds in their own browsers, where the policy is on, so an app that only works with the flag is still broken. The trade-offs of the flag itself are in disable CORS in Chrome.

Production: a proxy prefix for public APIs

When the API is not yours, no header you add will fix it. A CORS proxy makes the request from a server and relays the response with Access-Control-Allow-Origin for your origin. Prefix the URL only on web with kIsWeb; native builds keep calling the API directly. The example uses the Steam store API, which returns JSON and sends no CORS header.

Prefix the proxy on web builds only
import 'dart:convert';

import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:http/http.dart' as http;

const proxy = 'https://proxy.cors.dev/';

/// Mobile and desktop builds call the API directly; only browsers enforce CORS.
Uri apiUri(String url) => Uri.parse(kIsWeb ? '$proxy$url' : url);

Future<Map<String, dynamic>> loadApp(int appId) async {
  final response = await http.get(
    apiUri('https://store.steampowered.com/api/appdetails?appids=$appId'),
  );

  if (response.statusCode != 200) {
    // Set by the proxy when it refused or could not complete the request,
    // absent when the API itself answered with an error status.
    final code = response.headers['x-cors-error'] ?? 'upstream error';
    throw Exception('Request failed: ${response.statusCode} ($code)');
  }
  return jsonDecode(response.body) as Map<String, dynamic>;
}

Anonymous requests cover GET and HEAD to any public HTTPS host, forward only Accept, If-None-Match and If-Modified-Since, and never forward cookies. Keep requests simple: a custom header such as X-Api-Key triggers a preflight and is rejected anonymously with header_not_allowed. Pro Connections forward Authorization and custom headers, and allow credential-like query names such as key that anonymous requests reject; remember a web build is public, so only per-user tokens belong there.

Your own API: add the headers on the server

If you control the backend, fix it there and skip every workaround: answer with Access-Control-Allow-Origin for your web origin, list the methods and request headers your app uses, and answer OPTIONS with 204. Each framework has a middleware for it: Express, FastAPI, Django, Spring Boot, Laravel. Firebase and Supabase endpoints already send the headers for standard calls.

Limits and pricing

cors.dev limits for a Flutter web app
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
CachingNone, no-storeOpt-in X-Cors-Cache, 1 to 300 seconds

Good questions.

Does --disable-web-security work in a release build?

No. It is a flag on the Chrome instance flutter run starts on your computer. Visitors open your deployed app in their own browser with the same-origin policy on, so production needs server headers or a proxy.

Why does the request work in Postman and on Android but not on web?

Postman and native builds are not browsers and do not apply CORS. Only a browser demands Access-Control-Allow-Origin before handing a cross-origin response to page code. See Postman versus the browser for the full comparison.

Why does Dio say the XMLHttpRequest onError callback was called?

Dio on web uses the browser XMLHttpRequest, which reports a CORS block as a generic network error. The message is the same for a blocked response, a failed preflight and an unreachable host; the browser console tells them apart.

Can I send an Authorization header through the proxy?

Not anonymously: only Accept, If-None-Match and If-Modified-Since are forwarded. A Pro Connection forwards Authorization and custom headers for the hosts you enable. Anything a web build sends is visible to the user, so send per-user tokens, never a shared secret.

Do I need the prefix on mobile too?

No. Wrap it in kIsWeb so Android, iOS and desktop builds call the API directly. Routing native traffic through a proxy only adds latency and shares one set of IP addresses between all your users.

What about Image.network CORS errors with CanvasKit?

The CanvasKit renderer reads image pixels, so images also need CORS headers from their host. The free proxy relays text responses only; Pro relays any content type up to 6 MiB, or host the images on a CDN that sends Access-Control-Allow-Origin: *.