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.
Access-Control-Allow-Origin: * allows any website to access your resources. Always specify exact origins in production.
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
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;
};
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;
};
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;
};
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;
};
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;
};
};
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"}' ]
];
};
| 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) |
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 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');
});
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
For comprehensive testing instructions including curl commands, browser DevTools usage, and troubleshooting common CORS errors, see the CORS Testing Guide.
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.
Save 39% on CORS in Action with promotional code hossainco at manning.com/hossain