Conversation
Co-authored-by: Eldad A. Fux <eldad.fux@gmail.com>
Co-authored-by: Eldad A. Fux <eldad.fux@gmail.com>
Co-authored-by: Eldad A. Fux <eldad.fux@gmail.com>
fix: prevent callable strings
Fix: Cookie headers
Feat: Improve cookie handling
Feat: Allow array for headers
Headers can now be arrays (after recent changes allowing array headers). The getSize() method was attempting to directly implode headers, causing a warning when a header value was an array. This fix properly handles both string and array header values by joining array values with commas (standard HTTP header format) before calculating the request size. Added test case to verify the fix works correctly with array headers.
feat: remove validators and use utopia validators lib
* Use utopia-php/di for resource injection * Move resource ownership into utopia-php/di * Update DI branch dependency * update getting started * update * update * update appwrite base version * update to use php 8.2 * fix: restore php 8.2 test runtime * chore: use container scopes * remove utopia keyword * remove optional container in run * remove optional container in run * renaming * remove public getContainer * fix getcontainer * fix getcontainer * update * remove tests * make public * remove tests * add scoped request containers * cleanup * feat: request scopes * fixes --------- Co-authored-by: loks0n <22452787+loks0n@users.noreply.github.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
#230) * feat: split Swoole adapters, add compression support, and adopt utopia-php/servers - Split Swoole adapter into Swoole (SWOOLE_PROCESS) and SwooleCoroutine (coroutine-based) servers - Add response compression support with configurable min size and algorithm selection - Migrate Hook to utopia-php/servers and Route now extends Servers\Hook - Add View class for template rendering - Add trusted IP header support and IP validation in Request - Enhance Response with cookie management, content-type helpers, and chunked transfer - Add utopia-php/servers and utopia-php/compression dependencies - Fix server-swoole.php test server to work with non-coroutine Swoole adapter - Disable Swoole cookie parsing to preserve raw Cookie headers * fix: address Greptile review comments on PR #230 - Remove Content-Length before re-adding after compression to prevent duplicate headers - Defer onStart callback into coroutine event loop for SwooleCoroutine adapter consistency - Add null-coalescing fallback for preg_replace in View::render - Add void return types to compression setter methods for API consistency * add telemetry
Greptile SummaryThis PR introduces the 0.34.x branch, adding split Swoole/SwooleCoroutine adapters, telemetry (OpenTelemetry-compatible metrics), compression support, and DI container isolation per request. The most significant new finding is in
Confidence Score: 3/5Needs fixes before merging — the P1 \Exception-only catch in request hooks lets PHP Errors bypass all error handlers, and prior review threads flagged several additional open issues (activeRequests counter leak, Swoole getSize returning zero, missing port cast). One confirmed P1 defect (request-hook errors escaping the error handler) plus several open items from previous review threads that appear unresolved. Score reflects that this is a significant architectural PR that is close but needs targeted fixes on Http.php before it is safe to ship. src/Http/Http.php requires the most attention — P1 catch-block fix, removal of dead OPTIONS branch, and the previously flagged activeRequests try/finally gap. Important Files Changed
Reviews (4): Last reviewed commit: "Skip action if response already sent by ..." | Re-trigger Greptile |
| */ | ||
| protected function sendStatus(int $statusCode): void | ||
| { | ||
| $this->swoole->status((string) $statusCode); |
There was a problem hiding this comment.
Unnecessary
(string) cast in sendStatus
Swoole\Http\Response::status() has the signature status(int $http_status_code, string $reason = ''): bool. $statusCode is already typed as int by the parent abstract method — casting it to string before passing it in is at best a no-op (PHP coerces it back) and could cause a TypeError if Swoole ever enforces strict typing internally. The cast should be removed.
| $this->swoole->status((string) $statusCode); | |
| $this->swoole->status($statusCode); |
| $this->server->on('request', function (SwooleRequest $request, SwooleResponse $response) use ($callback) { | ||
| $requestContainer = new Container($this->container); | ||
| $requestContainer->set('swooleRequest', fn () => $request); | ||
| $requestContainer->set('swooleResponse', fn () => $response); | ||
|
|
||
| Coroutine::getContext()[self::REQUEST_CONTAINER_CONTEXT_KEY] = $requestContainer; | ||
|
|
||
| \call_user_func($callback, new Request($request), new Response($response)); |
There was a problem hiding this comment.
Missing
try/finally cleanup for coroutine context
The SwooleCoroutine\Server variant wraps the callback invocation in a try/finally to unset(Coroutine::getContext()[self::REQUEST_CONTAINER_CONTEXT_KEY]) after each request, ensuring stale per-request containers don't linger if an exception escapes. The process-mode Swoole\Server does not do this:
Coroutine::getContext()[self::REQUEST_CONTAINER_CONTEXT_KEY] = $requestContainer;
\call_user_func($callback, new Request($request), new Response($response));
// No cleanupAlthough Swoole destroys each request coroutine's context automatically when the coroutine exits, adding the matching try/finally here keeps the two adapters consistent and guards against any future change that reuses coroutines across requests:
| $this->server->on('request', function (SwooleRequest $request, SwooleResponse $response) use ($callback) { | |
| $requestContainer = new Container($this->container); | |
| $requestContainer->set('swooleRequest', fn () => $request); | |
| $requestContainer->set('swooleResponse', fn () => $response); | |
| Coroutine::getContext()[self::REQUEST_CONTAINER_CONTEXT_KEY] = $requestContainer; | |
| \call_user_func($callback, new Request($request), new Response($response)); | |
| Coroutine::getContext()[self::REQUEST_CONTAINER_CONTEXT_KEY] = $requestContainer; | |
| try { | |
| \call_user_func($callback, new Request($request), new Response($response)); | |
| } finally { | |
| unset(Coroutine::getContext()[self::REQUEST_CONTAINER_CONTEXT_KEY]); | |
| } |
| public function getSize(): int | ||
| { | ||
| return \mb_strlen(\implode("\n", $this->generateHeaders()), '8bit') + \mb_strlen(\file_get_contents('php://input'), '8bit'); | ||
| $headers = $this->generateHeaders(); | ||
| $headerStrings = []; | ||
| foreach ($headers as $key => $value) { | ||
| if (\is_array($value)) { | ||
| $headerStrings[] = $key . ': ' . \implode(', ', $value); | ||
| } else { | ||
| $headerStrings[] = $key . ': ' . $value; | ||
| } | ||
| } | ||
| return \mb_strlen(\implode("\n", $headerStrings), '8bit') + \mb_strlen(\file_get_contents('php://input'), '8bit'); | ||
| } |
There was a problem hiding this comment.
getSize() reads php://input — incorrect for Swoole adapters
The base getSize() calls file_get_contents('php://input') to measure the request body size. This works under PHP-FPM, but in Swoole the request body is only accessible via $swoole->rawContent() — PHP's php://input stream is empty in that context.
Neither Swoole/Request.php nor SwooleCoroutine/Request.php override getSize(). Because Http::run() now calls $this->requestBodySize->record($request->getSize(), ...) for the new telemetry, all Swoole requests will report zero bytes for the body in the http.server.request.body.size metric.
Fix: override getSize() in Swoole/Request.php to use rawContent() for the body component:
public function getSize(): int
{
$headers = $this->generateHeaders();
$headerStrings = [];
foreach ($headers as $key => $value) {
$headerStrings[] = is_array($value)
? $key . ': ' . implode(', ', $value)
: $key . ': ' . $value;
}
return mb_strlen(implode("\n", $headerStrings), '8bit')
+ mb_strlen($this->swoole->rawContent(), '8bit');
}SwooleCoroutine/Request.php extends Swoole/Request.php, so it will inherit the fix.
* Use Swoole parsed cookies again * Skip Swoole test without extension * Remove Swoole adapter unit test
| public function run(Request $request, Response $response): static | ||
| { | ||
| $this->activeRequests->add(1, [ | ||
| 'http.request.method' => $request->getMethod(), | ||
| 'url.scheme' => $request->getProtocol(), | ||
| ]); | ||
|
|
||
| $start = microtime(true); | ||
| $result = $this->runInternal($request, $response); | ||
|
|
||
| $requestDuration = microtime(true) - $start; | ||
| $attributes = [ | ||
| 'url.scheme' => $request->getProtocol(), | ||
| 'http.request.method' => $request->getMethod(), | ||
| 'http.route' => $this->route?->getPath(), | ||
| 'http.response.status_code' => $response->getStatusCode(), | ||
| ]; | ||
| $this->requestDuration->record($requestDuration, $attributes); | ||
| $this->requestBodySize->record($request->getSize(), $attributes); | ||
| $this->responseBodySize->record($response->getSize(), $attributes); | ||
| $this->activeRequests->add(-1, [ | ||
| 'http.request.method' => $request->getMethod(), | ||
| 'url.scheme' => $request->getProtocol(), | ||
| ]); | ||
|
|
||
| return $result; | ||
| } |
There was a problem hiding this comment.
activeRequests counter leaks when runInternal throws
run() increments the active-requests counter before calling runInternal(), but the matching decrement sits after the call with no try/finally guard. runInternal can throw — for example, when an error handler itself raises an exception the code explicitly re-throws:
throw new Exception('Error handler had an error: ' . $e->getMessage(), 500, $e);If that path is taken the add(-1) call on line 842 is skipped, and the http.server.active_requests gauge is permanently over-counted by one for every such request. Over time this makes the metric untrustworthy.
The fix is to wrap the post-call telemetry in a try/finally:
$start = microtime(true);
try {
$result = $this->runInternal($request, $response);
} finally {
$requestDuration = microtime(true) - $start;
$attributes = [
'url.scheme' => $request->getProtocol(),
'http.request.method' => $request->getMethod(),
'http.route' => $this->route?->getPath(),
'http.response.status_code' => $response->getStatusCode(),
];
$this->requestDuration->record($requestDuration, $attributes);
$this->requestBodySize->record($request->getSize(), $attributes);
$this->responseBodySize->record($response->getSize(), $attributes);
$this->activeRequests->add(-1, [
'http.request.method' => $request->getMethod(),
'url.scheme' => $request->getProtocol(),
]);
}
return $result;* Skip action if response already sent by init hook If an init hook (e.g. cache) calls $response->send(), the action should not run — the response has already been delivered to the client. Without this guard the action executes in full, wasting worker time on work whose output is immediately discarded. Shutdown hooks still run so metrics, billing, and other teardown logic are unaffected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix: pass Response to execute() so isSent() guard is in scope The 0.34.x execute() method had dropped the Response parameter compared to 0.33.x, meaning $response->isSent() in the action guard would be an undefined variable. Restore the parameter and update all call sites in the test suite. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
| $arguments = $this->getArguments($hook, [], []); | ||
| \call_user_func_array($hook->getAction(), $arguments); | ||
| } | ||
| } catch (\Exception $e) { |
There was a problem hiding this comment.
Request hook errors not caught by error handler
The onRequest hook catch block uses \Exception, but PHP fatal errors (e.g., TypeError, ArithmeticError, Error) extend \Throwable, not \Exception. Any PHP internal error thrown inside a request hook will bypass this catch block entirely, skipping the error handler and propagating to the Swoole/FPM layer uncaught. The execute() method correctly uses \Throwable (line 745) — the same fix is needed here and at the start() hook catch (line 646).
| } catch (\Exception $e) { | |
| } catch (\Throwable $e) { |
No description provided.