GuidesGODOT WEB

Godot web export CORS errors

In a Godot 4 Web export, HTTPRequest runs on the browser's fetch, so a request that works in the editor fails once the game runs in a browser and the API sends no Access-Control-Allow-Origin. On itch.io the request comes from https://html-classic.itch.zone, not from your itch.io page. Prefix the URL with https://proxy.cors.dev/ in Web builds only: GET is free with no signup (about 600 requests a minute per IP), and POST for leaderboards needs cors.dev Pro at $5 a month for 500,000 requests.

Why HTTPRequest fails only in Web exports

In the editor and in desktop or mobile builds, Godot uses its own networking, and CORS does not apply. In a Web export, Godot's HTTP classes run on the browser's fetch API (platform/web/js/libs/library_godot_fetch.js in the engine source), so the browser enforces CORS on every request. That is why the same HTTPRequest call works in the editor and fails in the browser.

Godot's HTTPClient class reference (Godot 4.7 stable docs), which the HTTPRequest page points to: "When performing HTTP requests from a project exported to Web, keep in mind the remote server may not allow requests from foreign origins due to CORS. If you host the server in question, you should modify its backend to allow requests from foreign origins by adding the Access-Control-Allow-Origin: * HTTP header." When you do not run the server, you cannot add that header.

The origin your game sends

The editor's Run in Browser button serves the export from http://localhost:8060 by default (editor settings export/web/http_host and export/web/http_port), and requests carry that origin.

itch.io runs HTML5 games inside an iframe served from https://html-classic.itch.zone. On https://itch.io/embed-upload/3263210, checked on 28 October 2026, the page origin was https://itch.io and the game frame loaded from https://html-classic.itch.zone/html/3263210/.... Requests from the game carry Origin: https://html-classic.itch.zone, not your itch.io page. Games that turn on itch.io's SharedArrayBuffer support are served from https://html.itch.zone instead.

Where a Godot 4 game runs, checked October 2026
Where it runsOrigin an API seesCORS appliesproxy.cors.dev free answers
Editor, desktop and mobile buildsNone, native networkingNoNot needed
Run in Browserhttp://localhost:8060YesEchoes http://localhost:8060
itch.iohttps://html-classic.itch.zoneYesEchoes the origin
itch.io with SharedArrayBufferhttps://html.itch.zoneYesEchoes the origin
Your own websiteYour domainYesEchoes the origin

The Steam news API (https://api.steampowered.com/ISteamNews/GetNewsForApp/v2/?appid=440&count=3&maxlength=300&feeds=steam_community_announcements, 2,083 bytes of JSON) sends no Access-Control-Allow-Origin, so a browser blocks it. On 28 October 2026 it answered 200 through https://proxy.cors.dev/ with the header echoing https://html-classic.itch.zone, https://html.itch.zone and http://localhost:8060.

Add the proxy in Web builds only

Put https://proxy.cors.dev/ in front of the full target URL; the free tier needs no signup and no key. In GDScript, add the prefix only when OS.has_feature("web") is true (the web feature tag means the game runs in a browser), so the editor and native builds keep calling the API directly.

steam_news.gd: the proxy prefix in Web exports only
extends Node

const PROXY := "https://proxy.cors.dev/"
const NEWS_URL := "https://api.steampowered.com/ISteamNews/GetNewsForApp/v2/?appid=440&count=3&maxlength=300&feeds=steam_community_announcements"

func _ready() -> void:
	var http := HTTPRequest.new()
	add_child(http)
	http.request_completed.connect(_on_news_completed)
	var url := PROXY + NEWS_URL if OS.has_feature("web") else NEWS_URL
	var error := http.request(url)
	if error != OK:
		push_error("Could not start the request: %s" % error_string(error))

func _on_news_completed(result: int, code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
	if result != HTTPRequest.RESULT_SUCCESS or code != 200:
		push_error("News request failed: result %d, HTTP %d" % [result, code])
		return
	var data = JSON.parse_string(body.get_string_from_utf8())
	if data == null:
		push_error("The response was not JSON")
		return
	for item in data["appnews"]["newsitems"]:
		print(item["title"])

The request_completed handler checks result == HTTPRequest.RESULT_SUCCESS and the response code, then parses the body with JSON.parse_string(body.get_string_from_utf8()). We ran this script in Godot 4.7.2 headless, direct and through the proxy, and it printed three Team Fortress 2 news titles both times.

One HTTPRequest node handles one request at a time: request() returns ERR_BUSY while the previous request is running, so create one node per parallel request.

The free tier's rules:

  • Methods: GET and HEAD only; POST answers 405 method_not_allowed.
  • Rate: About 600 requests per minute per IP and no monthly quota; past that, 429 with a Retry-After header.
  • Headers: Authorization, and names containing token, apikey, api-key, api_key, secret or password or ending in -key, answer 400 header_not_allowed. Other custom headers, including names containing csrf or xsrf such as X-CSRF-Token, are dropped and listed in X-Cors-Dropped-Headers.
  • Size: Responses up to 6 MiB within a 30 second upstream deadline, on both tiers.

When the proxy itself refuses a request, the X-Cors-Error response header names the reason; X-Cors-Source: upstream marks status codes that came from the API.

Leaderboards and other POST requests

A POST from Origin: https://html-classic.itch.zone got 405 method_not_allowed on the free tier. Score submissions and other POST, PUT, PATCH or DELETE calls need cors.dev Pro: $5 a month for 500,000 requests, with a 7-day trial of 1,000 requests and no card. Pro forwards bodies up to 1 MiB and Authorization, Content-Type and the custom headers you configure, at 3,000 requests a minute per account and 100 in flight.

  1. Create a Connection

    Sign up with Google; a Connection lists up to 10 exact page origins and up to 20 target hosts.

  2. Allow your origins and host

    Register https://html-classic.itch.zone (or https://html.itch.zone) and http://localhost:8060, and enable your API's host. Any other origin answers 403 origin_not_allowed, and Origin: null never matches.

  3. Send the key from GDScript

    HTTPRequest.request() takes custom headers as a PackedStringArray, for example ["X-Cors-Key: YOUR_CONNECTION_KEY", "Content-Type: application/json"], then HTTPClient.METHOD_POST and the body string.

Every itch.io game shares the html-classic.itch.zone origin, so an origin check cannot tell your game from another itch.io game; the Connection's list of target hosts is what limits the publishable key. Never put a secret API key in a Web export: players can read everything the game downloads.

Other Web export limits

Godot's "Exporting for the Web" page lists limits beyond CORS. Web builds support only the HTTP client, HTTP requests, the WebSocket client and WebRTC, and HTTP is subject to the same-origin policy. HTTP requests cannot progress more than once per frame, chunked responses are not supported, and host verification cannot be disabled.

Threaded Web exports need cross-origin isolation headers, Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp. Since Godot 4.3 the default Web export is single-threaded, which Godot calls more compatible with stores like itch.io.

Good questions.

Why does my game work in the editor but not on itch.io?

The editor and native builds use Godot's own networking, where CORS does not apply. A Web export sends requests through the browser's fetch, from the https://html-classic.itch.zone origin on itch.io, so the browser blocks any response that lacks an Access-Control-Allow-Origin header for it.

Can a Godot project setting turn off CORS?

No. The browser enforces CORS, not the engine, and no export option changes it. Either the server adds Access-Control-Allow-Origin or the Web build sends its requests through a CORS proxy.

Do WebSockets in a Godot Web export need a CORS proxy?

No. Browser WebSocket connections are not blocked by CORS, though the server may check the Origin header itself. cors.dev proxies HTTP requests only and does not carry WebSockets.

Can a game on itch.io call an http:// API?

Not directly: an https page cannot call plain http:// URLs, because the browser blocks them as mixed content. The proxy accepts http targets behind its https address, so https://proxy.cors.dev/http://... works from an itch.io game.

Which origin do I register for an itch.io game on cors.dev Pro?

https://html-classic.itch.zone, not your https://itch.io page, because itch.io serves HTML5 games in an iframe from that host. With itch.io's SharedArrayBuffer option on, register https://html.itch.zone, and add http://localhost:8060 if you test with Run in Browser.