Quick answer: CORS errors happen because the browser blocks cross-origin HTTP requests that lack proper response headers from the server. The fix is on the server side: add Access-Control-Allow-Origin headers. If you cannot modify the server, route requests through a proxy you control. Construct 3 cannot bypass CORS from the client side — it is a browser security feature, not a Construct 3 limitation.
You add the AJAX plugin to your Construct 3 project, set up a request to an external API, and trigger it. The On completed event never fires. Instead, On error fires, and when you open the browser console you see a red error message about CORS policy. The request never reaches the server — or more precisely, the browser refuses to give your game access to the response. This is one of the most confusing errors for game developers because it is not a bug in your code or in Construct 3.
The Symptom
You use the AJAX plugin’s Request URL action to call an external API — a leaderboard service, a weather API, a custom backend, or any URL that is not on the same domain as your game. The request appears to do nothing. Your On completed event with the corresponding tag never triggers.
Opening the browser developer tools (F12 > Console) reveals an error message like:
Access to XMLHttpRequest at 'https://api.example.com/scores'
from origin 'https://your-game.itch.io' has been blocked by
CORS policy: No 'Access-Control-Allow-Origin' header is
present on the requested resource.
Sometimes the error mentions a “preflight request” that failed. Sometimes it says the header value does not match the origin. The error message varies, but it always mentions CORS and always refers to the server response, not your request.
A particularly confusing variant: the request works perfectly in Construct 3’s preview mode (served from localhost or preview.construct.net) but fails when the game is exported and hosted on a different domain like itch.io or your own site.
What Causes This
CORS (Cross-Origin Resource Sharing) is a browser security mechanism. It has nothing to do with Construct 3 specifically — any web application faces the same restriction. Here are the specific causes:
1. The API server does not send CORS headers. By default, browsers block cross-origin responses unless the server explicitly allows them by including an Access-Control-Allow-Origin header. Many APIs designed for server-to-server communication do not include this header because they were never intended to be called directly from a browser.
2. The CORS header does not include your game’s origin. The server might send Access-Control-Allow-Origin: https://example.com, but your game is hosted on https://your-game.itch.io. The origin must match exactly, including the protocol (http vs https) and port number.
3. Preflight request fails. When you send anything other than a simple GET request — for example, a POST with Content-Type: application/json, or any request with custom headers — the browser first sends an OPTIONS request (called a “preflight”). If the server does not respond to this OPTIONS request with the correct CORS headers, the actual request is never sent.
4. Preview vs export origin mismatch. During preview, Construct 3 serves your game from localhost or its own preview domain. Some API servers are configured to allow localhost for development but do not include your production domain. When you export and deploy, the origin changes and CORS blocks the request.
5. Mixed content (HTTP vs HTTPS). If your game is served over HTTPS but you are requesting an HTTP URL, most browsers block the request entirely as “mixed content” before CORS even comes into play. This produces a different error message but the symptom is the same.
The Fix
Step 1: Identify the exact error. Open the browser developer tools and read the full CORS error message. It tells you exactly what is wrong.
// Common CORS error messages and what they mean:
// "No 'Access-Control-Allow-Origin' header is present"
// → Server does not send CORS headers at all
// "The 'Access-Control-Allow-Origin' header has a value
// 'https://other.com' that is not equal to the supplied origin"
// → Server allows a different origin, not yours
// "Response to preflight request doesn't pass access control"
// → Server does not handle OPTIONS requests properly
// "The value of the 'Access-Control-Allow-Credentials'
// header must be 'true'"
// → Request includes credentials but server does not allow them
Step 2: Fix the server CORS configuration (if you control the server). Add the required headers to your API server responses. Here is what the headers need to look like:
// Required response headers from your API server:
Access-Control-Allow-Origin: https://your-game.itch.io
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type
// Or to allow ANY origin (less secure, but fine for public APIs):
Access-Control-Allow-Origin: *
// Node.js / Express example:
const cors = require('cors');
app.use(cors({
origin: ['https://your-game.itch.io', 'http://localhost:50000']
}));
// PHP example (add to the top of your API script):
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit();
}
Step 3: Set up a proxy server if you cannot modify the API. When you are calling a third-party API that does not support CORS (and you cannot change their server), the solution is to proxy the request through a server you control.
// Instead of calling the third-party API directly:
// WRONG (blocked by CORS):
AJAX: Request "https://api.thirdparty.com/data"
Tag: "GetData"
// RIGHT (call your own proxy):
AJAX: Request "https://your-server.com/proxy/data"
Tag: "GetData"
// Your proxy server (Node.js example):
app.get('/proxy/data', async (req, res) => {
const response = await fetch('https://api.thirdparty.com/data');
const data = await response.json();
res.json(data);
// CORS headers are already handled by the cors middleware
});
Step 4: Handle preflight OPTIONS requests. If you are sending POST requests with JSON content type or custom headers, the browser sends a preflight OPTIONS request first. Your server must respond to it correctly.
// In Construct 3 - sending a POST with JSON:
AJAX: Set request header "Content-Type" to "application/json"
AJAX: Post to URL "https://your-server.com/api/score"
Data: "{"score": " & Score & ", "name": "" & PlayerName & ""}"
Tag: "PostScore"
// This triggers a preflight because Content-Type: application/json
// is not a "simple" content type. Your server must handle OPTIONS.
// Server-side (Node.js):
app.options('/api/score', (req, res) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'POST, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type');
res.sendStatus(200);
});
Step 5: Ensure consistent origin handling for preview and production.
// On your server, allow both preview and production origins:
const allowedOrigins = [
'http://localhost:50000', // C3 local preview
'https://preview.construct.net', // C3 remote preview
'https://your-game.itch.io', // itch.io deployment
'https://yourdomain.com' // custom domain
];
app.use(cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
}
}));
Step 6: Use the scripting API for more control over requests. The AJAX plugin works for basic requests, but the scripting API gives you access to the full Fetch API with better error handling.
// Scripting API - using fetch() directly
try {
const response = await fetch("https://your-server.com/api/data", {
method: "GET",
headers: {
"Content-Type": "application/json"
}
});
if (!response.ok) {
console.error("HTTP error:", response.status);
return;
}
const data = await response.json();
console.log("Received:", data);
} catch (err) {
// This catches network errors AND CORS errors
console.error("Request failed:", err.message);
}
Why This Works
CORS is enforced by the browser, not by the network. The request actually reaches the server in most cases — the browser just refuses to give your JavaScript code access to the response unless the server explicitly says “this origin is allowed.” This is why the fix is always on the server side.
Access-Control-Allow-Origin headers tell the browser “I, the server, consent to letting JavaScript on this origin read my response.” Without this header, the browser assumes the server does not want its responses read by client-side code from other domains.
Proxy servers work because CORS only applies to browser-based requests. Server-to-server HTTP requests have no origin concept and are not subject to CORS. Your proxy server fetches data from the third-party API without any CORS restriction, then serves it to your game with the correct headers.
Preflight handling is required because the browser uses the OPTIONS request to check permissions before sending the actual request. If the OPTIONS response does not include the right CORS headers, the browser cancels the real request without ever sending it.
"CORS errors are not a Construct 3 bug. They are the browser protecting users from unauthorized cross-origin data access. The fix is always on the server. If you cannot change the server, you need a proxy. There is no client-side workaround and that is by design."
Related Issues
If your game saves data via AJAX but the save/load system is not restoring state correctly, see Fix: Construct 3 Save/Load System Not Restoring State. For loops that process AJAX responses and skip items, check Fix: Construct 3 For Each Loop Skipping Instances. If your platform game loads level data from an API, see Fix: Construct 3 Platform Behavior Falling Through Platforms for issues with dynamically created platforms. And if AJAX-loaded particle configurations are not rendering on mobile, see Fix: Construct 3 Particles Not Showing on Mobile.
CORS is a server fix. Always a server fix.