effectra / cors
PHP library for enabling CORS (Cross-Origin Resource Sharing) in HTTP requests/responses. It follows PSR guidelines, and can be easily integrated into PHP projects.
Requires
- effectra/http-message: ^1.0
- psr/http-message: ^2.0@dev
- psr/http-server-handler: ^1.0@dev
- psr/http-server-middleware: ^1.0@dev
Requires (Dev)
- barryvdh/laravel-ide-helper: ^3.7
- illuminate/http: ^13.22
- illuminate/support: ^13.22
- mrpunyapal/peststan: ^0.2.12
- pestphp/pest: ^3.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan: ^2.2
- spatie/laravel-package-tools: ^1.93
README
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.
Features
- ✅ 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
Requirements
- PHP 8.0+
psr/http-message^2.0psr/http-server-handler^1.0psr/http-server-middleware^1.0
Installation
composer require effectra/cors
Quick Start
use 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/*'], ]);
Classes
CorsService
The core CORS engine. Inspects requests, validates origins, and injects the appropriate CORS response headers.
Constructor
new CorsService(array $options = [])
Optionally pass a configuration array at construction time. The same array can be applied later via setOptions().
Configuration Options
| 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.
Public Methods
setOptions(array $options): void
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'], ]);
setDebugMode(bool $debug): self
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);
isCorsRequest(RequestInterface $request): bool
Returns true when the request carries a non-empty Origin header — the sign of a cross-origin request.
if ($corsService->isCorsRequest($request)) { // handle CORS }
isPreflightRequest(RequestInterface $request): bool
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); }
isOriginAllowed(RequestInterface $request): bool
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 | false
handlePreflightRequest(RequestInterface $request): ResponseInterface
Builds 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);
addActualRequestHeaders(ResponseInterface $response, RequestInterface $request): ResponseInterface
Injects CORS headers for a real (non-preflight) request:
Access-Control-Allow-OriginAccess-Control-Allow-CredentialsAccess-Control-Expose-Headers
$response = $corsService->addActualRequestHeaders($response, $request);
varyHeader(ResponseInterface $response, string $header): ResponseInterface
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');
getErrors(): array
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"]
hasErrors(): bool
Returns true if any CORS errors have been recorded.
if ($corsService->hasErrors()) { // inspect or log $corsService->getErrors() }
clearErrors(): void
Resets the internal error collection. Useful when reusing the service across multiple requests in long-running processes.
$corsService->clearErrors();
CorsMiddleware
A PSR-15 middleware that wraps CorsService and integrates it into a request/response pipeline.
Constructor
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. |
process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
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.
Fluent Setter Methods
All setters return $this for method chaining.
setPaths(array $paths): self
Set (or replace) the list of paths CORS should be applied to.
$middleware->setPaths(['api/*', 'webhooks/*']);
setExcludedPaths(array $paths): self
Set (or replace) the list of paths to exclude from CORS handling.
$middleware->setExcludedPaths(['api/internal/*']);
setHandlePreflight(bool $handle): self
Enable or disable automatic preflight handling.
$middleware->setHandlePreflight(false); // delegate to the next handler
setHandleErrors(bool $handle): self
Enable or disable graceful error handling. When disabled, exceptions propagate normally.
$middleware->setHandleErrors(false);
getCorsService(): CorsService
Returns the underlying CorsService instance — useful for inspecting errors after a request.
$service = $middleware->getCorsService(); $errors = $service->getErrors();
Path Matching
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/*'], ],
Origin Validation
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.
Credential-Aware Enforcement
When supportsCredentials is true:
allowedOriginsmust not contain'*'— anInvalidArgumentExceptionis thrown.exposedHeadersmust not contain'*'.Access-Control-Allow-Credentials: trueis added to every matched response.
Error Handling
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.
Advanced Examples
Allow all origins (public API)
$corsService = new CorsService([ 'allowedOrigins' => ['*'], 'allowedMethods' => ['*'], 'allowedHeaders' => ['*'], ]);
Subdomain wildcard
$corsService = new CorsService([ 'allowedOrigins' => ['https://*.example.com'], 'allowedMethods' => ['GET', 'POST'], 'allowedHeaders' => ['Content-Type', 'Authorization'], 'supportsCredentials' => true, ]);
Restrict CORS to specific routes
$middleware = new CorsMiddleware($corsService, [ 'paths' => ['api/*'], 'excluded_paths' => ['api/health', 'api/internal/*'], 'handle_preflight' => true, 'handle_errors' => true, ]);
Fluent configuration
$middleware = (new CorsMiddleware($corsService)) ->setPaths(['api/*']) ->setExcludedPaths(['api/internal/*']) ->setHandlePreflight(true) ->setHandleErrors(true);
Standalone usage (without a middleware stack)
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); }
Response Headers Reference
| 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 |
Contributing
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.
License
The effectra/cors package is open-sourced software licensed under the MIT license.