A PHP library for handling Cross-Origin Resource Sharing (CORS) in HTTP applications. Built on PSR-7, PSR-15, and PSR-17 standards, it provides a flexible middleware and a standalone service class that can be integrated into any PHP project or framework.
- ✅ PSR-15 compliant middleware
- ✅ Wildcard and pattern-based origin matching
- ✅ Preflight (
OPTIONS) request handling - ✅ Credential-aware CORS enforcement
- ✅ Path allowlist and blocklist with wildcard support
- ✅ Debug mode with error logging
- ✅ Runtime error collection (
X-CORS-Errorsheader) - ✅ Both
camelCaseandsnake_caseoption key support - ✅ Automatic
Varyheader management
- PHP 8.0+
psr/http-message^2.0psr/http-server-handler^1.0psr/http-server-middleware^1.0
composer require effectra/corsuse Effectra\Cors\CorsService;
use Effectra\Cors\CorsMiddleware;
$corsService = new CorsService([
'allowedOrigins' => ['https://example.com'],
'allowedMethods' => ['GET', 'POST', 'PUT', 'DELETE'],
'allowedHeaders' => ['Content-Type', 'Authorization'],
'exposedHeaders' => ['X-Custom-Header'],
'supportsCredentials'=> false,
'maxAge' => 3600,
]);
$middleware = new CorsMiddleware($corsService, [
'paths' => ['api/*'],
'excluded_paths' => ['api/internal/*'],
]);The core CORS engine. Inspects requests, validates origins, and injects the appropriate CORS response headers.
new CorsService(array $options = [])Optionally pass a configuration array at construction time. The same array can be applied later via setOptions().
| Key (camelCase) | Key (snake_case) | Type | Default | Description |
|---|---|---|---|---|
allowedOrigins |
allowed_origins |
string[] |
[] |
Exact origins allowed. Use ['*'] to allow all. |
allowedOriginsPatterns |
allowed_origins_patterns |
string[] |
[] |
Regex-style wildcard patterns, e.g. ['https://*.example.com']. |
allowedMethods |
allowed_methods |
string[] |
[] |
HTTP methods to allow. Use ['*'] to allow all. |
allowedHeaders |
allowed_headers |
string[] |
[] |
Request headers to allow. Use ['*'] to allow all. |
exposedHeaders |
exposed_headers |
string[] |
[] |
Response headers the browser may read. |
supportsCredentials |
supports_credentials |
bool |
false |
Allow cookies/auth. Incompatible with allowedOrigins: ['*']. |
maxAge |
max_age |
int|null |
0 |
Preflight cache duration in seconds. null omits the header entirely. |
Note: Both
camelCaseandsnake_casekeys are accepted everywhere.
Apply (or re-apply) a configuration array. Validates all values before storing them and throws InvalidArgumentException on invalid input.
$corsService->setOptions([
'allowedOrigins' => ['https://app.example.com'],
'allowedMethods' => ['GET', 'POST'],
]);Enable debug mode. When active, CORS errors are written to the PHP error log and exceptions are re-thrown instead of being swallowed.
$corsService->setDebugMode(true);Returns true when the request carries a non-empty Origin header — the sign of a cross-origin request.
if ($corsService->isCorsRequest($request)) {
// handle CORS
}Returns true when the request is an OPTIONS preflight — i.e., the method is OPTIONS and Access-Control-Request-Method is present.
if ($corsService->isPreflightRequest($request)) {
$response = $corsService->handlePreflightRequest($request);
}Checks whether the request Origin is permitted by the current configuration. Supports exact matches and wildcard patterns. Also validates the origin URL format.
$allowed = $corsService->isOriginAllowed($request); // true | falseBuilds and returns a complete preflight response (204 No Content on success, 403 Forbidden when the origin is not allowed). Delegates header injection to addPreflightRequestHeaders().
$response = $corsService->handlePreflightRequest($request);addPreflightRequestHeaders(ResponseInterface $response, RequestInterface $request): ResponseInterface
Injects all relevant preflight headers into an existing response:
Access-Control-Allow-OriginAccess-Control-Allow-CredentialsAccess-Control-Allow-MethodsAccess-Control-Allow-HeadersAccess-Control-Max-Age
Returns the response unchanged when the origin is not allowed.
$response = $corsService->addPreflightRequestHeaders($response, $request);Injects CORS headers for a real (non-preflight) request:
Access-Control-Allow-OriginAccess-Control-Allow-CredentialsAccess-Control-Expose-Headers
$response = $corsService->addActualRequestHeaders($response, $request);Appends a value to the Vary response header without creating duplicates. Used internally to ensure correct caching behaviour when the origin or method varies per-request.
$response = $corsService->varyHeader($response, 'Origin');Returns all CORS errors collected during the current request cycle, keyed by error type (origin, method, preflight, configuration, etc.).
$errors = $corsService->getErrors();
// ['origin' => "Origin 'http://evil.com' not allowed"]Returns true if any CORS errors have been recorded.
if ($corsService->hasErrors()) {
// inspect or log $corsService->getErrors()
}Resets the internal error collection. Useful when reusing the service across multiple requests in long-running processes.
$corsService->clearErrors();A PSR-15 middleware that wraps CorsService and integrates it into a request/response pipeline.
new CorsMiddleware(CorsService $cors, array $config = [])| Config Key | Type | Default | Description |
|---|---|---|---|
paths / allowed_paths |
string[] |
[] |
Paths CORS is applied to. Empty means all paths. |
excluded_paths |
string[] |
[] |
Paths where CORS processing is skipped entirely. |
handle_preflight |
bool |
true |
Whether to handle OPTIONS preflight requests automatically. |
handle_errors |
bool |
true |
Whether to catch exceptions and expose errors via header. |
The PSR-15 entry point. Execution flow:
- Skip — if the request is not a CORS request, or the path is excluded / not in the allowlist, the request passes through to the next handler unchanged.
- Preflight — if
handle_preflightistrueand the request is a preflightOPTIONS, a complete preflight response is returned immediately (without calling the next handler). - Actual request — the next handler is called and CORS headers are added to its response.
- Error header — if
handle_errorsistrueand errors were collected, they are serialised intoX-CORS-Errorsas JSON. - Exception safety — uncaught exceptions return a
500response with CORS headers whenhandle_errorsistrue; otherwise they are re-thrown.
All setters return $this for method chaining.
Set (or replace) the list of paths CORS should be applied to.
$middleware->setPaths(['api/*', 'webhooks/*']);Set (or replace) the list of paths to exclude from CORS handling.
$middleware->setExcludedPaths(['api/internal/*']);Enable or disable automatic preflight handling.
$middleware->setHandlePreflight(false); // delegate to the next handlerEnable or disable graceful error handling. When disabled, exceptions propagate normally.
$middleware->setHandleErrors(false);Returns the underlying CorsService instance — useful for inspecting errors after a request.
$service = $middleware->getCorsService();
$errors = $service->getErrors();Both paths and excluded_paths support three matching strategies:
| Pattern | Example | Matches |
|---|---|---|
| Exact | api/users |
/api/users only |
Wildcard * |
api/* |
/api/users, /api/posts, … |
Single char ? |
v?/resource |
/v1/resource, /v2/resource |
Paths can also be scoped to a specific hostname by using the hostname as the array key:
'paths' => [
'app.example.com' => ['dashboard/*'],
'api.example.com' => ['v1/*', 'v2/*'],
],CorsService validates the origin format before checking allow-lists:
- Standard URLs are validated with PHP's
FILTER_VALIDATE_URL. localhost(with optional port) is accepted without a scheme for local development.- Invalid origin formats are rejected and recorded in the error log.
Wildcard patterns in allowedOrigins (e.g. https://*.example.com) are automatically compiled to regex and matched against each incoming request origin.
When supportsCredentials is true:
allowedOriginsmust not contain'*'— anInvalidArgumentExceptionis thrown.exposedHeadersmust not contain'*'.Access-Control-Allow-Credentials: trueis added to every matched response.
Errors are collected internally rather than silently discarded:
$corsService->setDebugMode(true); // also writes to PHP error_log()
// After processing a request:
if ($corsService->hasErrors()) {
print_r($corsService->getErrors());
}
// Clear for the next request
$corsService->clearErrors();When handle_errors is enabled on CorsMiddleware, all collected errors are exposed via the X-CORS-Errors response header as a JSON string.
$corsService = new CorsService([
'allowedOrigins' => ['*'],
'allowedMethods' => ['*'],
'allowedHeaders' => ['*'],
]);$corsService = new CorsService([
'allowedOrigins' => ['https://*.example.com'],
'allowedMethods' => ['GET', 'POST'],
'allowedHeaders' => ['Content-Type', 'Authorization'],
'supportsCredentials' => true,
]);$middleware = new CorsMiddleware($corsService, [
'paths' => ['api/*'],
'excluded_paths' => ['api/health', 'api/internal/*'],
'handle_preflight' => true,
'handle_errors' => true,
]);$middleware = (new CorsMiddleware($corsService))
->setPaths(['api/*'])
->setExcludedPaths(['api/internal/*'])
->setHandlePreflight(true)
->setHandleErrors(true);use Effectra\Cors\CorsService;
$corsService = new CorsService([
'allowedOrigins' => ['https://frontend.example.com'],
'allowedMethods' => ['GET', 'POST'],
'allowedHeaders' => ['Content-Type'],
]);
if ($corsService->isPreflightRequest($request)) {
return $corsService->handlePreflightRequest($request);
}
if ($corsService->isCorsRequest($request)) {
$response = $corsService->addActualRequestHeaders($response, $request);
}| Header | Set by | Condition |
|---|---|---|
Access-Control-Allow-Origin |
Preflight & actual request | Origin is allowed |
Access-Control-Allow-Credentials |
Preflight & actual request | supportsCredentials is true |
Access-Control-Allow-Methods |
Preflight only | Origin is allowed |
Access-Control-Allow-Headers |
Preflight only | Origin is allowed |
Access-Control-Max-Age |
Preflight only | maxAge is not null |
Access-Control-Expose-Headers |
Actual request | exposedHeaders is non-empty |
Vary |
Both | Dynamic origin or method matching is active |
X-CORS-Errors |
Middleware | Errors collected and handle_errors is enabled |
Contributions are welcome! Please open an issue or submit a pull request. For major changes, open an issue first to discuss what you would like to change.
The effectra/cors package is open-sourced software licensed under the MIT license.