-
-
Notifications
You must be signed in to change notification settings - Fork 14
Make Cors service coroutine-safe
#293
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR refactors the Cors service to be coroutine-safe by moving from instance properties to coroutine-local Context storage. This enables safe concurrent request handling in Swoole's coroutine environment and allows per-request CORS configuration overrides.
Key changes:
- Options are now stored in Context as an immutable
CorsOptionsvalue object instead of instance properties - The
HandleCorsmiddleware now callssetOptions()per-request rather than in the constructor - Added
getCorsConfig()extensibility hook for custom CORS configuration (e.g., tenant-specific origins)
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| src/http/src/CorsOptions.php | New readonly value object providing type-safe, immutable storage for normalized CORS options |
| src/http/src/Cors.php | Refactored to use Context for coroutine-local storage instead of instance properties |
| src/http/src/Middleware/HandleCors.php | Moved setOptions() call from constructor to process() method for per-request configuration; added getCorsConfig() hook |
| tests/Http/CorsTest.php | Updated tests to work with Context-based storage; added new tests for header/method normalization, origin matching, Vary header behavior, and coroutine isolation |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| public function __construct( | ||
| public array $allowedOrigins = [], | ||
| public array $allowedOriginsPatterns = [], | ||
| public bool $supportsCredentials = false, | ||
| public array $allowedHeaders = [], | ||
| public array $allowedMethods = [], | ||
| public array $exposedHeaders = [], | ||
| public ?int $maxAge = 0, | ||
| public bool $allowAllOrigins = false, | ||
| public bool $allowAllHeaders = false, | ||
| public bool $allowAllMethods = false, | ||
| ) { |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The array properties lack runtime type enforcement for their elements. While the docblocks specify string[] for most arrays, PHP's type system cannot enforce array element types in constructor parameters. Consider adding validation in the constructor to ensure all elements are strings for string[] properties, or at minimum add a note in the docblock that the caller is responsible for ensuring correct array element types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@albertcht This is technically true but not worth addressing in my opinion. The docblock accurately describes PHP's behavior, and setOptions() already calls array_map('strtolower', ...) on headers/methods which would fail on non-strings. Since these values come from config files (developer-controlled, not user input), any type issues would surface immediately in dev. Adding runtime validation for every array element is overkill here.
| /** | ||
| * Context key used by Cors class. | ||
| */ | ||
| private const CORS_CONTEXT_KEY = '__cors.options'; |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The hardcoded context key should match the constant defined in the Cors class. Currently, the test uses '__cors.options' but if the Cors class constant changes, tests will silently break. Consider making the CONTEXT_KEY constant public or providing a getter method in the Cors class to access it for testing purposes.
| private const CORS_CONTEXT_KEY = '__cors.options'; | |
| private const CORS_CONTEXT_KEY = Cors::CONTEXT_KEY; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@albertcht The suggested fix won't work - CONTEXT_KEY is private, so Cors::CONTEXT_KEY would be a fatal error. Making it public just for tests would leak implementation details. If someone changes the key in Cors, the tests would fail loudly anyway (options would return defaults instead of set values), so there's no silent breakage risk.
| \Hyperf\Coroutine\Coroutine::create(function () use ($service) { | ||
| // In a new coroutine, options should be empty (defaults) | ||
| $this->assertEquals([], $this->getOptionsFromContext()->allowedOrigins); | ||
|
|
||
| // Set different options in this coroutine | ||
| $service->setOptions(['allowedOrigins' => ['other.com']]); | ||
| $this->assertEquals(['other.com'], $this->getOptionsFromContext()->allowedOrigins); | ||
| }); |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test is creating a coroutine but not waiting for it to complete before asserting the final state. This can lead to race conditions where the test completes before the coroutine executes, making the test unreliable. Add coroutine synchronization using Coroutine::create's return value and wait for completion, or use a more explicit synchronization mechanism like WaitGroup.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@albertcht Copilot misunderstands Swoole coroutines here. With RunTestsInCoroutine, the parent yields while the child runs - it doesn't return immediately like spawning a thread. The assertions inside the child throw immediately if they fail, and the assertion after verifies the parent's context wasn't polluted. That's exactly what we're testing - no synchronization needed.
| // In a new coroutine, options should be empty (defaults) | ||
| $this->assertEquals([], $this->getOptionsFromContext()->allowedOrigins); |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test assumes that the Context in a new coroutine will have empty/default CORS options, but this isn't explicitly initialized. While the fallback in getOptionsFromContext returns a new CorsOptions(), this test validates coroutine isolation by checking the state before any explicit setOptions call. Consider explicitly documenting this assumption or initializing the context in the spawned coroutine to make the test intention clearer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@albertcht Same as above - Copilot misunderstands Swoole coroutines here
| { | ||
| if ($options) { | ||
| if ($options !== []) { | ||
| $this->setOptions($options); |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The constructor initializing options by calling setOptions directly introduces an anti-pattern in the coroutine-safe design. When Cors is instantiated as a singleton service (likely during app bootstrap), passing options to the constructor will store them in the bootstrap coroutine's context, which is never used since HandleCors middleware calls setOptions per-request. Consider either: (1) removing the ability to pass options to the constructor and documenting that setOptions must be called explicitly, or (2) deprecating constructor parameters in favor of always calling setOptions in the middleware. The current design is confusing because it suggests options can be set during construction, but those options may not be available in request coroutines.
| $this->setOptions($options); | |
| // Constructor options are deprecated and ignored to preserve coroutine-safe design. | |
| // Use setOptions() per request (e.g. in middleware) instead. | |
| @trigger_error( | |
| 'Passing options to ' . __CLASS__ . '::__construct() is deprecated and ignored. ' | |
| . 'Call setOptions() per request instead.', | |
| E_USER_DEPRECATED | |
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@albertcht The observation is partially correct - constructor options do write to bootstrap context - but it's harmless. I don't think it's an anti-pattern. The middleware calls setOptions() per-request which writes to the correct request context, so the bootstrap write is just ignored.
Constructor options are useful for testing (used throughout this PR's tests) and standalone usage. A deprecation warning would just spam test output for no real benefit.
|
@albertcht I don't think any of Copilots suggestions are worth implementing here (and some are incorrect). But let me know if you want any changes - happy to make them. |
Summary
The
Corsclass appears to be a direct port from the fruitcake/php-cors package. This PR refactors it to be coroutine-safe, which also allows per-request CORS configuration overrides.Changes
CorsOptionsvalue object, following the same patterns used byAuthManager, LogManager, and other Hypervel services.HandleCorsmiddleware now callssetOptions()inprocess()rather than the constructor, enabling configuration to vary per-request.getCorsConfig()method toHandleCorsfor easy customization (e.g., tenant-specific CORS origins).Files Changed
Usage Example
Extending for tenant-aware CORS:
Breaking Changes
None. The public API remains unchanged.