Skip to content

0.34.x#226

Open
loks0n wants to merge 154 commits into0.33.xfrom
0.34.x
Open

0.34.x#226
loks0n wants to merge 154 commits into0.33.xfrom
0.34.x

Conversation

@loks0n
Copy link
Copy Markdown
Contributor

@loks0n loks0n commented Mar 13, 2026

No description provided.

Meldiron and others added 15 commits August 21, 2025 16:17
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>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 13, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d29c187e-8381-420b-ab45-9fec0cd3a823

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 0.34.x
📝 Coding Plan
  • Generate coding plan for human review comments

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

#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-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 6, 2026

Greptile Summary

This PR is a large refactor introducing adapter-based request/response classes for FPM, Swoole, and SwooleCoroutine, along with new telemetry instrumentation in Http::run(). Several issues raised in prior review threads remain unresolved (see inline comments from the previous round), and one new inconsistency was found: Swoole/Request::getReferer() ignores its $default parameter, returning a hardcoded empty string while the FPM adapter correctly passes $default through.

Confidence Score: 4/5

Safe to merge once prior-thread P1 items are addressed; the single new finding here is P2.

Multiple P1 items from prior review rounds remain open (activeRequests leak, \Exception vs \Throwable in hook catches, FPM onRequest/onStart ordering, Swoole getSize returning 0). The new finding (getReferer ignoring $default) is P2. Score of 4 reflects the unresolved P1s.

src/Http/Http.php (telemetry guard + Throwable catch), src/Http/Adapter/FPM/Server.php (start order), src/Http/Adapter/Swoole/Request.php (getReferer default + getSize override)

Important Files Changed

Filename Overview
src/Http/Http.php New telemetry layer wraps runInternal(); activeRequests counter lacks try/finally guard and onRequest catch uses \Exception instead of \Throwable — both flagged in prior review threads and still unresolved.
src/Http/Adapter/Swoole/Request.php New Swoole request adapter; getReferer() silently ignores the $default parameter unlike the FPM equivalent; getSize() not overridden so body size reports 0 on Swoole (flagged in prior thread).
src/Http/Adapter/FPM/Server.php onRequest() fires synchronously before onStart(), so start hooks run after the request is already processed — flagged in prior thread, still unresolved.
src/Http/Adapter/Swoole/Response.php sendStatus() passes (string) cast to Swoole's int-typed status() method — flagged in prior thread; otherwise clean.
src/Http/Adapter/SwooleCoroutine/Server.php $port passed without (int) cast to Swoole\Coroutine\Http\Server — flagged in prior thread; coroutine context cleanup with try/finally is correct.
src/Http/Request.php getSize() reads php://input which is empty in Swoole; no override in Swoole adapters so telemetry body size will be zero — flagged in prior thread.
src/Http/Adapter/FPM/Request.php New FPM request adapter; correctly implements all abstract methods including getReferer() passing $default through.
src/Http/Adapter/Swoole/Server.php Missing try/finally around call_user_func for coroutine context cleanup (flagged in prior thread); request container scope otherwise correct.

Reviews (7): Last reviewed commit: "Lower Brotli default compression level t..." | Re-trigger Greptile

Comment thread src/Http/Adapter/SwooleCoroutine/Server.php
*/
protected function sendStatus(int $statusCode): void
{
$this->swoole->status((string) $statusCode);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Suggested change
$this->swoole->status((string) $statusCode);
$this->swoole->status($statusCode);

Comment on lines +29 to +36
$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));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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 cleanup

Although 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:

Suggested change
$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]);
}

Comment thread src/Http/Request.php
Comment on lines 353 to 365
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');
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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
Comment thread src/Http/Http.php
Comment on lines +822 to +848
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;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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>
Comment thread src/Http/Http.php
$arguments = $this->getArguments($hook, [], []);
\call_user_func_array($hook->getAction(), $arguments);
}
} catch (\Exception $e) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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).

Suggested change
} catch (\Exception $e) {
} catch (\Throwable $e) {

Comment on lines +15 to +25
{
$request = new Request();
$response = new Response();

$this->container->set('fpmRequest', fn () => $request);
$this->container->set('fpmResponse', fn () => $response);

\call_user_func($callback, $request, $response);
}

public function onStart(callable $callback)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 onRequest fires before onStart, reversing initialization order

Http::start() calls $this->server->onRequest(…) first, then $this->server->onStart(…). For every other adapter that registers non-immediate callbacks this is fine, but here onRequest immediately invokes the callback, so the entire request is processed before the onStart callback (which sets up the 'server' resource and runs all Http::onStart() hooks) is ever called. Any start hook that sets up resources needed during request processing (database connections, config, etc.) will not have run yet, and injecting 'server' inside a route will fail with a "resource not found" error.

The fix is to store the start callback and fire it just before the request:

private $startCallback = null;

public function onRequest(callable $callback)
{
    $request = new Request();
    $response = new Response();

    $this->container->set('fpmRequest', fn () => $request);
    $this->container->set('fpmResponse', fn () => $response);

    if ($this->startCallback) {
        \call_user_func($this->startCallback, $this);
    }

    \call_user_func($callback, $request, $response);
}

public function onStart(callable $callback)
{
    $this->startCallback = $callback;
}

Brotli defaulted to level 11 (near-max), which is too slow for an HTTP
response path. Set a fast, HTTP-sensible default of 4 and add an
explicit Zstd default of 3. GZIP and Deflate have no public level
setter in utopia-php/compression 0.1.4, so their PHP defaults (6) are
left as-is.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.