Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## 1.2.1 under development

- no changes in this release.
- Bug #24: Fix invalid CORS headers and add optional preflight request handling to `CorsAllowAllMiddleware` (@samdark)

## 1.2.0 March 10, 2026

Expand Down
13 changes: 12 additions & 1 deletion docs/guide/en/cors-allow-all-middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,15 @@ use Yiisoft\HttpMiddleware\CorsAllowAllMiddleware;
$middleware = new CorsAllowAllMiddleware();
```

There are no constructor arguments, the middleware is ready to use out of the box.
To handle CORS preflight requests without passing them to the request handler, provide a PSR-17 response factory:

```php
use Psr\Http\Message\ResponseFactoryInterface;
use Yiisoft\HttpMiddleware\CorsAllowAllMiddleware;

/** @var ResponseFactoryInterface $responseFactory */
$middleware = new CorsAllowAllMiddleware($responseFactory);
```

The constructor argument is optional for backward compatibility. Without a response factory, preflight requests are
passed to the next request handler and CORS headers are added to its response.
59 changes: 51 additions & 8 deletions src/CorsAllowAllMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@

namespace Yiisoft\HttpMiddleware;

use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

use function count;
use function in_array;

/**
* Adds Cross-Origin Resource Sharing (CORS) headers allowing everything to the response.
*
Expand All @@ -20,18 +24,57 @@
*/
final class CorsAllowAllMiddleware implements MiddlewareInterface
{
/**
* @param ResponseFactoryInterface|null $responseFactory Factory used to short-circuit preflight requests.
*/
public function __construct(
private readonly ?ResponseFactoryInterface $responseFactory = null,
) {}

public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$response = $handler->handle($request);
$origins = $request->getHeader('Origin');
$origin = count($origins) === 1 ? $origins[0] : '';
$isPreflight = $request->getMethod() === 'OPTIONS'
&& $request->getHeaderLine('Access-Control-Request-Method') !== '';

$response = $isPreflight && $this->responseFactory !== null
? $this->responseFactory->createResponse(204)
: $handler->handle($request);

$exposedHeaders = [];
/** @var array<string, string[]> $headers */
$headers = $response->getHeaders();
foreach ($headers as $name => $_) {
if (strtolower($name) !== 'set-cookie') {
$exposedHeaders[] = $name;
}
}

return $response
->withHeader('Allow', '*')
->withHeader('Vary', 'Origin')
->withHeader('Access-Control-Allow-Origin', '*')
$vary = array_map(trim(...), explode(',', strtolower($response->getHeaderLine('Vary'))));
if (!in_array('origin', $vary, true)) {
$response = $response->withAddedHeader('Vary', 'Origin');
}

$response = $response
->withHeader('Access-Control-Allow-Origin', $origin === '' ? '*' : $origin)
->withHeader('Access-Control-Allow-Methods', 'GET,OPTIONS,HEAD,POST,PUT,PATCH,DELETE')
Comment thread
samdark marked this conversation as resolved.
->withHeader('Access-Control-Allow-Headers', '*')
->withHeader('Access-Control-Expose-Headers', '*')
->withHeader('Access-Control-Allow-Credentials', 'true')
->withHeader('Access-Control-Max-Age', '86400');

if ($origin === '') {
return $response
->withHeader('Access-Control-Allow-Headers', '*')
->withHeader('Access-Control-Expose-Headers', '*');
}

$requestedHeaders = $request->getHeaderLine('Access-Control-Request-Headers');
if ($requestedHeaders !== '') {
$response = $response->withHeader('Access-Control-Allow-Headers', $requestedHeaders);
}
if ($exposedHeaders !== []) {
$response = $response->withHeader('Access-Control-Expose-Headers', implode(',', $exposedHeaders));
}

return $response->withHeader('Access-Control-Allow-Credentials', 'true');
}
}
111 changes: 108 additions & 3 deletions tests/CorsAllowAllMiddlewareTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
namespace Yiisoft\HttpMiddleware\Tests;

use HttpSoft\Message\Response;
use HttpSoft\Message\ResponseFactory;
use HttpSoft\Message\ServerRequest;
use PHPUnit\Framework\TestCase;
use Yiisoft\HttpMiddleware\CorsAllowAllMiddleware;
use Yiisoft\HttpMiddleware\Tests\Support\FakeRequestHandler;

use function PHPUnit\Framework\assertNull;
use function PHPUnit\Framework\assertSame;

final class CorsAllowAllMiddlewareTest extends TestCase
Expand All @@ -25,16 +27,119 @@ public function testBase(): void
assertSame($request, $requestHandler->getLastRequest());
assertSame(
[
'Allow' => ['*'],
'Vary' => ['Origin'],
'Access-Control-Allow-Origin' => ['*'],
'Access-Control-Allow-Methods' => ['GET,OPTIONS,HEAD,POST,PUT,PATCH,DELETE'],
'Access-Control-Max-Age' => ['86400'],
'Access-Control-Allow-Headers' => ['*'],
'Access-Control-Expose-Headers' => ['*'],
'Access-Control-Allow-Credentials' => ['true'],
'Access-Control-Max-Age' => ['86400'],
],
$response->getHeaders(),
);
}

public function testCredentialedRequest(): void
{
$request = (new ServerRequest())
->withHeader('Origin', 'https://example.com');
$requestHandler = new FakeRequestHandler(
(new Response())
->withHeader('X-Request-Id', '42')
->withHeader('Set-Cookie', 'token=secret')
->withHeader('Vary', 'Accept-Encoding'),
);

$response = (new CorsAllowAllMiddleware())->process($request, $requestHandler);

assertSame('https://example.com', $response->getHeaderLine('Access-Control-Allow-Origin'));
assertSame('true', $response->getHeaderLine('Access-Control-Allow-Credentials'));
assertSame('X-Request-Id,Vary', $response->getHeaderLine('Access-Control-Expose-Headers'));
assertSame(['Accept-Encoding', 'Origin'], $response->getHeader('Vary'));
}

public function testMultipleOriginsAreNotReflected(): void
{
$request = (new ServerRequest())
->withHeader('Origin', ['https://example.com', 'https://example.org']);

$response = (new CorsAllowAllMiddleware())->process($request, new FakeRequestHandler());

assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin'));
assertSame('', $response->getHeaderLine('Access-Control-Allow-Credentials'));
}

public function testExistingOriginInVaryIsNotDuplicated(): void
{
$handlerResponse = (new Response())->withHeader('Vary', 'Accept-Encoding, ORIGIN');

$response = (new CorsAllowAllMiddleware())->process(
new ServerRequest(),
new FakeRequestHandler($handlerResponse),
);

assertSame(['Accept-Encoding, ORIGIN'], $response->getHeader('Vary'));
}

public function testRequestWithFactoryIsHandled(): void
{
$request = new ServerRequest();
$requestHandler = new FakeRequestHandler();

(new CorsAllowAllMiddleware(new ResponseFactory()))->process($request, $requestHandler);

assertSame($request, $requestHandler->getLastRequest());
}

public function testOptionsWithoutRequestedMethodIsHandled(): void
{
$request = (new ServerRequest())->withMethod('OPTIONS');
$requestHandler = new FakeRequestHandler();

(new CorsAllowAllMiddleware(new ResponseFactory()))->process($request, $requestHandler);

assertSame($request, $requestHandler->getLastRequest());
}

public function testOptionsWithEmptyRequestedMethodIsHandled(): void
{
$request = (new ServerRequest())
->withMethod('OPTIONS')
->withHeader('Access-Control-Request-Method', '');
$requestHandler = new FakeRequestHandler();

(new CorsAllowAllMiddleware(new ResponseFactory()))->process($request, $requestHandler);

assertSame($request, $requestHandler->getLastRequest());
}

public function testPreflightWithoutFactoryIsHandled(): void
{
$request = (new ServerRequest())
->withMethod('OPTIONS')
->withHeader('Access-Control-Request-Method', 'POST');
$requestHandler = new FakeRequestHandler(new Response(202));

$response = (new CorsAllowAllMiddleware())->process($request, $requestHandler);

assertSame($request, $requestHandler->getLastRequest());
assertSame(202, $response->getStatusCode());
}

public function testPreflight(): void
{
$request = (new ServerRequest())
->withMethod('OPTIONS')
->withHeader('Origin', 'https://example.com')
->withHeader('Access-Control-Request-Method', 'POST')
->withHeader('Access-Control-Request-Headers', 'Content-Type, Authorization');
$requestHandler = new FakeRequestHandler();

$response = (new CorsAllowAllMiddleware(new ResponseFactory()))->process($request, $requestHandler);

assertNull($requestHandler->getLastRequest());
assertSame(204, $response->getStatusCode());
assertSame('https://example.com', $response->getHeaderLine('Access-Control-Allow-Origin'));
assertSame('Content-Type, Authorization', $response->getHeaderLine('Access-Control-Allow-Headers'));
assertSame('true', $response->getHeaderLine('Access-Control-Allow-Credentials'));
}
}
Comment thread
samdark marked this conversation as resolved.
Loading