CORS in Perl PSGI scripts

The Plack::Middleware::CrossOrigin module provides a complete CORS server-side implementation for PSGI/Plack applications. Below are secure, modern examples for implementing CORS in Perl.

⚠️ Security Warning: Using Access-Control-Allow-Origin: * allows any website to access your resources. Always specify exact origins in production.

Installation

Install the module using your preferred method:

# CPAN
cpan Plack::Middleware::CrossOrigin

# cpanm
cpanm Plack::Middleware::CrossOrigin

# Debian/Ubuntu
apt-get install libplack-middleware-crossorigin-perl

Method 1: Secure Origin List (Recommended)

Specify a space-separated list of allowed origins:

#!/usr/bin/env perl
use Plack::Builder;

my $app = sub {
    my $env = shift;
    return [ 200, [ 'Content-Type' => 'application/json' ],
             [ '{"message":"CORS enabled"}' ] ];
};

builder {
    # Recommended: Specific origins
    enable 'CrossOrigin',
        origins => 'https://example.com https://app.example.com',
        methods => ['GET', 'POST', 'PUT', 'DELETE'],
        headers => ['Content-Type', 'Authorization'],
        max_age => 86400;

    $app;
};

Method 2: Regex-Based Origin Matching

Use regex patterns to match multiple subdomains:

use Plack::Builder;

my $app = sub {
    return [ 200, [ 'Content-Type' => 'text/plain' ], [ 'Hello' ] ];
};

builder {
    # Match origins using regex
    enable 'CrossOrigin',
        origins => qr{^https?://(.+\.)?example\.com$},
        credentials => 1,
        methods => ['GET', 'POST', 'PUT', 'DELETE'],
        headers => ['Content-Type', 'Authorization'];

    $app;
};

Method 3: Full Configuration

Example with all available configuration options:

use Plack::Builder;

my $app = sub {
    my $env = shift;
    return [
        200,
        [ 'Content-Type' => 'application/json' ],
        [ '{"status":"success"}' ]
    ];
};

builder {
    enable 'CrossOrigin',
        # Allowed origins (space-separated string or regex)
        origins => 'https://example.com https://app.example.com',

        # Allowed HTTP methods
        methods => ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],

        # Allowed request headers
        headers => ['Content-Type', 'Authorization', 'X-Requested-With'],

        # Exposed response headers
        expose_headers => ['Content-Length', 'X-Custom-Header'],

        # Allow credentials (cookies, HTTP auth)
        credentials => 1,

        # Preflight cache duration (seconds)
        max_age => 86400; # 24 hours

    $app;
};

Method 4: Environment-Based Configuration

Configure different origins for development and production:

use Plack::Builder;

my $app = sub { [ 200, [], [ 'OK' ] ] };

# Get environment
my $env_name = $ENV{PLACK_ENV} || 'development';

builder {
    if ($env_name eq 'development') {
        # Development - allow localhost
        enable 'CrossOrigin',
            origins => 'http://localhost:3000 http://localhost:8080';
    } else {
        # Production - strict origins
        enable 'CrossOrigin',
            origins => 'https://example.com https://app.example.com',
            credentials => 1,
            max_age => 86400;
    }

    $app;
};

Method 5: Path-Specific CORS

Apply different CORS policies to different API paths:

use Plack::Builder;

my $app = sub {
    my $env = shift;
    my $path = $env->{PATH_INFO};

    if ($path eq '/api/data') {
        return [ 200, [ 'Content-Type' => 'application/json' ],
                 [ '{"data":"value"}' ] ];
    }

    return [ 404, [ 'Content-Type' => 'text/plain' ], [ 'Not Found' ] ];
};

builder {
    # Public API - open CORS
    mount '/public' => builder {
        enable 'CrossOrigin', origins => '*';
        $app;
    };

    # Private API - restricted CORS
    mount '/api' => builder {
        enable 'CrossOrigin',
            origins => 'https://example.com',
            credentials => 1;
        $app;
    };
};

Method 6: Manual Implementation (Without Middleware)

For complete control, implement CORS manually:

use Plack::Builder;
use Plack::Request;

my @ALLOWED_ORIGINS = (
    'https://example.com',
    'https://app.example.com'
);

my $app = sub {
    my $env = shift;
    my $req = Plack::Request->new($env);
    my $origin = $req->header('Origin') || '';

    # Check if origin is allowed
    my $origin_allowed = grep { $_ eq $origin } @ALLOWED_ORIGINS;

    # Handle preflight OPTIONS request
    if ($req->method eq 'OPTIONS') {
        my @headers = (
            'Content-Type' => 'text/plain',
        );

        if ($origin_allowed) {
            push @headers,
                'Access-Control-Allow-Origin' => $origin,
                'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, OPTIONS',
                'Access-Control-Allow-Headers' => 'Content-Type, Authorization',
                'Access-Control-Max-Age' => '86400',
                'Vary' => 'Origin';
        }

        return [ 204, \@headers, [] ];
    }

    # Handle actual request
    my @response_headers = ( 'Content-Type' => 'application/json' );

    if ($origin_allowed) {
        push @response_headers,
            'Access-Control-Allow-Origin' => $origin,
            'Access-Control-Allow-Credentials' => 'true',
            'Vary' => 'Origin';
    }

    return [
        200,
        \@response_headers,
        [ '{"message":"Manual CORS implementation"}' ]
    ];
};

Configuration Reference

Parameter Type Description
origins String or Regex Allowed origins (space-separated string like 'https://example.com' or regex like qr{^https://.*\.example\.com$})
methods ArrayRef Allowed HTTP methods (e.g., ['GET', 'POST', 'PUT'])
headers ArrayRef Allowed request headers (e.g., ['Content-Type', 'Authorization'])
expose_headers ArrayRef Response headers to expose to client (e.g., ['Content-Length'])
credentials Boolean Allow credentials (cookies, HTTP auth). Set to 1 or 0
max_age Integer Preflight cache duration in seconds (e.g., 86400 for 24 hours)

Framework Integration

Dancer2

use Dancer2;
use Plack::Builder;

# Your Dancer2 app
get '/api/data' => sub {
    return { message => 'CORS enabled' };
};

# Wrap with CORS
builder {
    enable 'CrossOrigin',
        origins => 'https://example.com',
        credentials => 1;

    dance;
};

Mojolicious

# Mojolicious has built-in CORS support
$app->hook(after_build_tx => sub {
    my $tx = shift;
    $tx->res->headers->header('Access-Control-Allow-Origin' => 'https://example.com');
});

Deployment

Run your PSGI application with plackup or Starman:

# Development
plackup app.psgi

# Production with Starman
starman --workers 10 --port 5000 app.psgi

# With environment
PLACK_ENV=production starman app.psgi

Testing Your CORS Configuration

For comprehensive testing instructions including curl commands, browser DevTools usage, and troubleshooting common CORS errors, see the CORS Testing Guide.

Additional Resources

Who’s behind this

Monsur Hossain and Michael Hausenblas

Contribute

The content on this site stays fresh thanks to help from users like you! If you have suggestions or would like to contribute, fork us on GitHub.

Buy the book

Save 39% on CORS in Action with promotional code hossainco at manning.com/hossain