Back to Blog

Phaser.js CORS Proxy Setup: Load External Assets Without Errors

Complete guide to fixing CORS errors in Phaser.js games. Learn how to configure crossOrigin, use CORS proxies, and load assets from external CDNs and APIs.

Phaser.js CORS Proxy Setup: Load External Assets Without Errors

Phaser is one of the most popular HTML5 game frameworks, powering thousands of browser games. When loading assets from external sources like CDNs, APIs, or other domains, developers frequently encounter CORS (Cross-Origin Resource Sharing) errors. This comprehensive guide covers everything you need to know about handling CORS in Phaser games.

Table of Contents

  1. Understanding CORS in Phaser
  2. Phaser’s Built-in CORS Settings
  3. Common CORS Error Scenarios
  4. Setting Up a CORS Proxy
  5. Loading Different Asset Types
  6. Dynamic Asset Loading
  7. Production Best Practices

Understanding CORS in Phaser

When your Phaser game runs in a browser, it’s subject to the Same-Origin Policy. This security feature prevents your game from loading resources from different domains unless those domains explicitly allow it through CORS headers.

How Phaser Loads Assets

Phaser’s loader creates HTML elements (like <img> for images, <audio> for sounds) and makes HTTP requests to fetch assets. For cross-origin requests, these elements need the crossOrigin attribute set, and the server must respond with appropriate Access-Control-Allow-Origin headers.

Why CORS Errors Occur

Access to image at 'https://external-cdn.com/sprite.png' from origin
'https://mygame.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

This error means:

  1. Your game (at mygame.com) requested an image from external-cdn.com
  2. The CDN didn’t include CORS headers allowing your domain
  3. The browser blocked the response

Phaser’s Built-in CORS Settings

Phaser provides several ways to configure cross-origin loading.

Game Configuration

Set crossOrigin in your game config:

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    loader: {
        crossOrigin: 'anonymous',
        baseURL: 'https://assets.example.com/'
    },
    scene: {
        preload: preload,
        create: create
    }
};

const game = new Phaser.Game(config);

Using setCORS in Preload

function preload() {
    // Set CORS mode for subsequent loads
    this.load.setCORS('anonymous');

    // Set base URL for all assets
    this.load.setBaseURL('https://assets.example.com/');

    // Now load assets
    this.load.image('player', 'sprites/player.png');
    this.load.image('enemy', 'sprites/enemy.png');
}

Per-Asset Configuration

For individual assets, you can specify CORS settings:

function preload() {
    // Load with specific cross-origin setting
    this.load.image({
        key: 'external-sprite',
        url: 'https://other-domain.com/sprite.png',
        crossOrigin: 'anonymous'
    });
}

CrossOrigin Values

ValueMeaning
'anonymous'Request without credentials (cookies, auth headers)
'use-credentials'Request with credentials (requires server support)
undefinedNo cross-origin request (same-origin only)

Common CORS Error Scenarios

Scenario 1: CDN Without CORS Headers

Problem: Your CDN doesn’t send CORS headers.

Solution: Configure your CDN to include CORS headers, or use a CORS proxy:

function preload() {
    const CORS_PROXY = 'https://corsproxy.io/?url=';
    const assetUrl = 'https://cdn-without-cors.com/image.png';

    this.load.image('asset', CORS_PROXY + encodeURIComponent(assetUrl));
}

Scenario 2: Local Development

Problem: Loading files directly from filesystem (file://) protocol.

Solution: Use a local development server:

# Using Python
python -m http.server 8080

# Using Node.js
npx serve .

# Using PHP
php -S localhost:8080

Scenario 3: Different Subdomains

Problem: Game at game.example.com loading from assets.example.com.

Solution: Configure assets subdomain to allow the game subdomain:

# On assets.example.com
add_header 'Access-Control-Allow-Origin' 'https://game.example.com';

Scenario 4: Canvas Tainted Error

Problem: After loading, you get “The canvas has been tainted by cross-origin data.”

Cause: The image loaded but crossOrigin wasn’t set, tainting the canvas.

Solution: Always set crossOrigin for external images you’ll manipulate:

this.load.setCORS('anonymous');
this.load.image('sprite', 'https://external.com/sprite.png');

Setting Up a CORS Proxy

When you can’t control the source server, use a CORS proxy service like corsproxy.io.

Basic Proxy Usage

class AssetLoader {
    constructor(scene) {
        this.scene = scene;
        this.corsProxy = 'https://corsproxy.io/?url=';
        this.useProxy = true; // Toggle for development
    }

    getUrl(url) {
        if (this.useProxy && this.isExternal(url)) {
            return this.corsProxy + encodeURIComponent(url);
        }
        return url;
    }

    isExternal(url) {
        return url.startsWith('http://') || url.startsWith('https://');
    }

    loadImage(key, url) {
        this.scene.load.image(key, this.getUrl(url));
    }

    loadSpritesheet(key, url, frameConfig) {
        this.scene.load.spritesheet(key, this.getUrl(url), frameConfig);
    }

    loadAudio(key, url) {
        this.scene.load.audio(key, this.getUrl(url));
    }
}

// Usage in scene
function preload() {
    this.assetLoader = new AssetLoader(this);
    this.assetLoader.loadImage('player', 'https://external-cdn.com/player.png');
}

Conditional Proxy Based on Environment

const IS_DEVELOPMENT = window.location.hostname === 'localhost';

const config = {
    corsProxy: IS_DEVELOPMENT ? '' : 'https://corsproxy.io/?url=',

    assets: {
        local: '/assets/',
        remote: 'https://game-assets.example.com/'
    }
};

function getAssetUrl(path, forceProxy = false) {
    // Local assets don't need proxy
    if (path.startsWith('/') || path.startsWith('./')) {
        return path;
    }

    // Apply proxy if needed
    if (forceProxy || config.corsProxy) {
        return config.corsProxy + encodeURIComponent(path);
    }

    return path;
}

Loading Different Asset Types

Images and Spritesheets

function preload() {
    this.load.setCORS('anonymous');

    // Single image
    this.load.image('logo', proxyUrl('https://cdn.example.com/logo.png'));

    // Spritesheet
    this.load.spritesheet('characters', proxyUrl('https://cdn.example.com/chars.png'), {
        frameWidth: 32,
        frameHeight: 48
    });

    // Texture atlas
    this.load.atlas(
        'atlas',
        proxyUrl('https://cdn.example.com/atlas.png'),
        proxyUrl('https://cdn.example.com/atlas.json')
    );
}

Audio Files

function preload() {
    // Audio with multiple formats for browser compatibility
    this.load.audio('music', [
        proxyUrl('https://cdn.example.com/music.ogg'),
        proxyUrl('https://cdn.example.com/music.mp3')
    ]);

    // Audio sprite
    this.load.audioSprite(
        'sfx',
        proxyUrl('https://cdn.example.com/sfx.json'),
        [
            proxyUrl('https://cdn.example.com/sfx.ogg'),
            proxyUrl('https://cdn.example.com/sfx.mp3')
        ]
    );
}

JSON and Data Files

function preload() {
    // Level data
    this.load.json('level1', proxyUrl('https://api.example.com/levels/1'));

    // Tilemap
    this.load.tilemapTiledJSON('map', proxyUrl('https://cdn.example.com/map.json'));
}

Video

function preload() {
    // Note: Video loading with CORS requires special handling
    this.load.video('intro', proxyUrl('https://cdn.example.com/intro.mp4'), true);
}

function create() {
    const video = this.add.video(400, 300, 'intro');
    video.play();
}

Dynamic Asset Loading

Sometimes you need to load assets at runtime, not just during preload.

Loading During Gameplay

class GameScene extends Phaser.Scene {
    loadExternalImage(key, url) {
        return new Promise((resolve, reject) => {
            // Use proxy for external URLs
            const finalUrl = url.includes('://')
                ? 'https://corsproxy.io/?url=' + encodeURIComponent(url)
                : url;

            this.load.image(key, finalUrl);

            this.load.once('complete', () => {
                resolve(this.textures.get(key));
            });

            this.load.once('loaderror', (file) => {
                reject(new Error(`Failed to load: ${file.url}`));
            });

            this.load.start();
        });
    }

    async loadUserAvatar(playerId, avatarUrl) {
        try {
            await this.loadExternalImage(`avatar_${playerId}`, avatarUrl);
            return this.add.image(100, 100, `avatar_${playerId}`);
        } catch (error) {
            console.error('Avatar load failed:', error);
            return this.add.image(100, 100, 'default_avatar');
        }
    }
}

Fetching Remote Data

async function fetchLevelData(levelId) {
    const apiUrl = `https://api.mygame.com/levels/${levelId}`;
    const proxyUrl = 'https://corsproxy.io/?url=' + encodeURIComponent(apiUrl);

    try {
        const response = await fetch(proxyUrl);
        if (!response.ok) throw new Error('Network response was not ok');
        return await response.json();
    } catch (error) {
        console.error('Failed to fetch level:', error);
        return null;
    }
}

// In your scene
async function loadLevel(levelId) {
    const levelData = await fetchLevelData(levelId);
    if (levelData) {
        this.buildLevel(levelData);
    } else {
        this.loadFallbackLevel();
    }
}

Base64 Encoding Fallback

For small images, you can bypass CORS entirely by converting to base64:

async function loadImageAsBase64(url) {
    const proxyUrl = 'https://corsproxy.io/?url=' + encodeURIComponent(url);
    const response = await fetch(proxyUrl);
    const blob = await response.blob();

    return new Promise((resolve) => {
        const reader = new FileReader();
        reader.onloadend = () => resolve(reader.result);
        reader.readAsDataURL(blob);
    });
}

// Usage
async function preloadWithBase64() {
    const base64Image = await loadImageAsBase64('https://external.com/sprite.png');
    this.textures.addBase64('sprite', base64Image);
}

Production Best Practices

1. Cache Proxy Responses

class CachedAssetLoader {
    constructor(scene) {
        this.scene = scene;
        this.cache = new Map();
    }

    async loadWithCache(key, url) {
        if (this.cache.has(url)) {
            return this.cache.get(url);
        }

        const proxyUrl = 'https://corsproxy.io/?url=' + encodeURIComponent(url);

        return new Promise((resolve, reject) => {
            this.scene.load.image(key, proxyUrl);

            this.scene.load.once('complete', () => {
                this.cache.set(url, key);
                resolve(key);
            });

            this.scene.load.once('loaderror', reject);
            this.scene.load.start();
        });
    }
}

2. Implement Fallback Assets

function preload() {
    // Set up error handler
    this.load.on('loaderror', (file) => {
        console.warn(`Failed to load: ${file.key}, using fallback`);

        // Load a local fallback
        switch(file.type) {
            case 'image':
                this.load.image(file.key, 'assets/fallback-image.png');
                break;
            case 'audio':
                this.load.audio(file.key, 'assets/fallback-audio.mp3');
                break;
        }
    });

    // Attempt to load external assets
    this.load.image('player', proxyUrl('https://cdn.example.com/player.png'));
}

3. Host Critical Assets Locally

// Asset configuration
const assets = {
    // Critical assets - always local
    core: {
        player: 'assets/sprites/player.png',
        tiles: 'assets/sprites/tiles.png',
        ui: 'assets/ui/interface.png'
    },

    // Optional assets - can be external
    extras: {
        skins: 'https://cdn.example.com/skins/',
        music: 'https://cdn.example.com/music/'
    }
};

function preload() {
    // Load core assets directly (no proxy needed)
    Object.entries(assets.core).forEach(([key, url]) => {
        this.load.image(key, url);
    });
}

// Load extras on demand
async function loadExtraSkin(skinId) {
    const url = assets.extras.skins + skinId + '.png';
    return loadWithProxy('skin_' + skinId, url);
}

4. Monitor Load Performance

function preload() {
    const startTime = performance.now();

    this.load.on('complete', () => {
        const loadTime = performance.now() - startTime;
        console.log(`Assets loaded in ${loadTime.toFixed(2)}ms`);

        // Track in analytics
        if (window.analytics) {
            analytics.track('assets_loaded', { duration: loadTime });
        }
    });

    this.load.on('progress', (progress) => {
        // Update loading bar
        this.loadingBar.setProgress(progress);
    });
}

5. Use Environment-Based Configuration

// config.js
const environments = {
    development: {
        assetsUrl: '/assets/',
        corsProxy: '',
        debug: true
    },
    staging: {
        assetsUrl: 'https://staging-cdn.example.com/',
        corsProxy: 'https://corsproxy.io/?url=',
        debug: true
    },
    production: {
        assetsUrl: 'https://cdn.example.com/',
        corsProxy: 'https://corsproxy.io/?url=',
        debug: false
    }
};

const env = process.env.NODE_ENV || 'development';
export const config = environments[env];

Conclusion

CORS issues in Phaser games are common but manageable. The key strategies are:

  1. Configure Phaser’s CORS settings using setCORS('anonymous') for external resources
  2. Use a CORS proxy like corsproxy.io when you can’t control the source server
  3. Host critical assets locally to avoid CORS issues entirely
  4. Implement fallbacks for graceful degradation when external assets fail

By combining these approaches, you can build Phaser games that reliably load assets from any source while providing a great experience for your players.

Related blog posts

Create a free Account to fix CORS Errors in Production

Say goodbye to CORS errors and get back to building great web applications. It's free!

CORSPROXY Dashboard