Skip to content

fix(sites): stream chunked responses - #13314

Open
HarshMN2345 wants to merge 24 commits into
mainfrom
fix/sites-chunked-responses
Open

HarshMN2345 wants to merge 24 commits into
mainfrom
fix/sites-chunked-responses

Conversation

@HarshMN2345

@HarshMN2345 HarshMN2345 commented Aug 23, 2026 •

Copy link
Copy Markdown
Member

Streams Sites responses through the proxy instead of buffering them, so a progressively rendered page reaches the visitor before the runtime has finished producing it.

  • Behind _APP_COMPUTE_STREAMING (default disabled). When enabled, the executor client requests response format 0.12.0 for non-preview site requests and reads the length-prefixed multipart envelope as it arrives, forwarding body chunks to the visitor.
  • Previews, branded 404 and error pages, and older executors keep the buffered path. A streamed response is not compressed.
  • A failure after the first chunk aborts the response through Response::abort() instead of appending an error page, and the execution is still recorded with its logs.
  • Streamed bytes are counted by the HTTP package's chunk(), so bandwidth metering is unchanged.

Requires open-runtimes/executor#250. No released executor emits 0.12.0, so until it ships every request takes the buffered path. After that release, the executor image in docker-compose.yml is bumped and a Sites e2e (testSiteStreaming, timing three chunks two seconds apart) is added.

Tests: BodyMultipartStreamTest covers incremental parsing, the executor wire format, malformed separators and incomplete envelopes.

@greptile-apps

greptile-apps Bot commented Aug 23, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds opt-in streaming for non-preview Sites responses while preserving buffered handling for unsupported executors and responses requiring branded output.

  • Adds an incremental parser for length-prefixed multipart executor responses.
  • Streams successful Site body chunks through the HTTP response and records their outbound byte count.
  • Adds configuration wiring and parser/response-accounting unit coverage.

Confidence Score: 4/5

The PR does not yet appear safe to merge because a malformed or truncated trailing executor envelope can be discovered only after the visitor response has already been ended successfully.

The body part's completion flag is still forwarded as the HTTP response-ending flag, while full multipart-envelope validation occurs afterward; the subsequent socket close therefore cannot reliably turn that already-completed response into an observable truncation.

Files Needing Attention: app/controllers/general.php

Important Files Changed

Filename Overview
app/controllers/general.php Adds the Site streaming gate and response callback, but still ends the visitor response at body-part completion before validating the remaining executor envelope.
src/Executor/Executor.php Negotiates the streaming response format, incrementally parses supported responses, and now rejects streams lacking a closing delimiter.
src/Appwrite/Utopia/Fetch/BodyMultipartStream.php Implements incremental length-prefixed multipart parsing with delimiter, chunk-size, and CRLF validation.
src/Appwrite/Utopia/Response.php Extends chunked-response accounting to include headers and streamed body bytes.
tests/unit/Utopia/Fetch/BodyMultipartStreamTest.php Covers incremental parsing, executor wire format, malformed separators, and incomplete envelopes.

Reviews (13): Last reviewed commit: "test: cover an envelope abandoned mid ru..." | Re-trigger Greptile

Comment thread src/Executor/Executor.php Resolved
@github-actions

github-actions Bot commented Aug 23, 2026 •

Copy link
Copy Markdown

✨ Benchmark results

Comparing main (before) → fix/sites-chunked-responses (after).

Metric Before After Change
🚀 Requests/sec 198.91 201.51 ⚪ +1.3%
⏱️ Latency P50 88.86 ms 86.49 ms ⚪ -2.7%
⏱️ Latency P95 199.56 ms 197.75 ms ⚪ -0.9%
Per-scenario breakdown & investigation details

Metrics below reflect the current branch (after). Δ P95 compares against the base.

Scenario P50 (ms) P95 (ms) Requests RPS Δ P95 (ms)
API total 86.49 197.75 12,540 201.51 -1.81
Account 168.47 301.59 660 11.19 -1.47
TablesDB 83.19 155.14 6,820 111.58 +0.26
Storage 81.29 182.96 3,300 56.08 +2.95
Functions 118.78 227.33 1,760 30.65 -7.86

Top API waits (after)

API request Max wait (ms)
storage.buckets.create 445.36
functions.create 408.2
account.name.update 394.91
functions.variables.update 390
account.prefs.update 371.36

A callback suppresses decoding in call(), so createExecution reads the buffered
body itself. It assumed multipart, but the executor answers its own failures
with JSON: deriving a boundary from "application/json" yields an empty string,
the parse returns nothing, and the ExecutorException ends up with an empty
message and an unknown type.

This affected every non-preview site request, since that is where the callback
is installed, and it applied whether or not streaming engaged. Switch on the
response's own content type instead, and fail loudly if the JSON will not parse
rather than reporting an empty error.

Also pin the executor's streamed envelope as a fixture, so a wire change on
either side of the contract fails here, and cover part ordering, trailing data
after the closing delimiter, and the malformed-input paths.
Comment thread app/controllers/general.php Resolved
Replace the anonymous state class and its four booleans with a tri-state
local: null while undecided, false once the response is buffered, true once
content is on the wire. Streaming is decided on the first body run, where the
status code and headers are guaranteed to have arrived.

Any status at or above 400 now takes the buffered path, which subsumes the
separate static-404 case and the deferred empty-body check, and keeps branded
pages working. Terminating the response no longer needs its own flag, since
chunk() is a no-op once the response has been sent.

Collapse the executor's redundant streaming flag into the reader it tracked,
and trim comments to the density of the surrounding files.
@HarshMN2345
HarshMN2345 force-pushed the fix/sites-chunked-responses branch from 1e43064 to e1b739f Compare August 23, 2026 17:13
HarshMN2345 and others added 3 commits August 23, 2026 22:58
A stream that stopped before the closing delimiter was reported as a complete
execution, and the proxy wrote the terminating chunk regardless, so a visitor
received a partial page under a successful status with nothing to indicate the
content was cut short.

Fail the execution when the envelope did not finish, and drop the connection
instead of terminating it cleanly, since content is already on the wire by then
and a broken transfer is the only remaining way to signal truncation.

Validate the CRLF after a content run and after a part terminator as well: both
were skipped over unread, so a malformed envelope could still parse.
The close on truncation sat at the end of the handler, which an executor
timeout never reached: it is rethrown as an AppwriteException from the catch,
so the error hook appended the branded page to a visitor's partial body and
terminated it as a complete response. Close where the failure is caught
instead, and mark the response sent so no later writer touches it.

Decode the headers part before committing to streaming, and fail the request
when it cannot be decoded rather than serving the page with none of its
headers. Guard the buffered path's decode the same way, since it stored null.

Drop the guard around the branded overrides: each already requires a status
that the streamed path cannot have, so it only indented them.
Comment thread app/controllers/general.php Resolved
Matches Schedule\Source\Functions, which holds a promoted readonly \Closure
with a typed docblock. Every caller already passes a closure, and mixed only
existed because a property cannot be declared callable.
The terminating chunk in the streamed branch could never run: the body part's
final callback ends the response inside the part handler, some 250 lines
earlier, and every other route there marks the response sent as well.

The part buffer kept every trailing part, though only the status code and
headers are ever read from it and only before the first body byte. Logs and
errors are capped at 5MB each and are already held by the executor client that
the rest of the handler reads them from, so a streamed request was holding them
twice. Keep only what is still readable.

An empty boundary could not occur: the executor sets the content type and the
format echo in one statement, and the branch could not have recovered anyway.

Emitting part of a run before the rest arrives is the reader's whole purpose
and nothing covered it: turning it into a run-buffering reader left the suite
green. Pin the malformed size message too, since deleting that guard tripped a
later one and passed. Both mutants now fail. The executor writes seven parts,
so the fixture carries seven.
Streaming had no switch: once an executor advertised the format, every
non-preview site request streamed, and the only way back was to roll the
executor image.

Default it off. A streamed response also skips compression, since that lives in
send() and a chunked body has no length to compress against, so an operator
should be choosing the trade rather than inheriting it on upgrade.
The existing truncation test stops after a whole part. A runtime dying mid body
leaves a different state: content already forwarded, the run never finished.
Letting isComplete() tolerate that state now fails.
@hansi-codes

hansi-codes Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

🔵 Tier A · Mergeable after minor fixes

The previously reported missing HTTP-level coverage for the enabled proxy streaming path remains open; these additions only extend unit tests.

This PR adds gated incremental streaming for non-preview Sites requests by negotiating the executor’s multipart format, parsing response parts as they arrive, and forwarding body chunks while retaining buffered fallbacks. It also adds streamed-byte accounting and configuration plumbing, with tests for multipart parsing, byte accounting, response schema serialization, and content-disposition filenames.

Latest changes: The newest commits extend ResponseTest with collection/table schema-type serialization cases and content-disposition filename edge cases.

Verdict New comments Fixed Still open
✅ Approved 0 0 1
Fix with agent prompt
### Issue 1
app/controllers/general.php:667-668
**Cover the enabled streaming path through the proxy**

The new tests exercise the parser and byte counter but never run the executor-to-proxy integration, so they would still pass if Sites responses were buffered or lost their status/headers. Please add HTTP-level coverage with streaming enabled for progressive delivery and a buffered error/preview fallback; AGENTS.md requires e2e coverage for HTTP-visible behavior, and existing Sites/Proxy suites provide the comparable test surface.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
📂 Walkthrough · 9
File Change
.env Documents the Sites streaming configuration flag.
app/config/variables.php Defines the Sites streaming configuration variable.
app/controllers/general.php Negotiates and forwards streamed Site responses while retaining buffered fallback handling.
docker-compose.yml Passes the Sites streaming flag into the application container.
src/Appwrite/Utopia/Fetch/BodyMultipartStream.php Incrementally parses length-prefixed multipart response parts.
src/Appwrite/Utopia/Response.php Counts headers and body bytes written through chunked responses.
src/Executor/Executor.php Requests the streaming response format and consumes response parts through a callback.
tests/unit/Utopia/Fetch/BodyMultipartStreamTest.php Covers incremental multipart parsing, executor wire-format bytes, and malformed or truncated input.
tests/unit/Utopia/ResponseTest.php Tests streamed-byte accounting, collection/table schema type output, and content-disposition filename handling.
⏳ Still open from earlier reviews · 1

Reviewed the commits since 04d5ed4 · Details · Comment @hansi-codes review to re-run, or mention @hansi-codes with a question.

@hansi-codes hansi-codes Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Tier A · Looks good to merge. Summary

Comment on lines +667 to +668
responseFormat: $canStreamResponse ? Executor::RESPONSE_FORMAT_STREAM : Executor::RESPONSE_FORMAT_ARRAY_HEADERS,
onPart: $onExecutionPart

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cover the enabled streaming path through the proxy

The new tests exercise the parser and byte counter but never run the executor-to-proxy integration, so they would still pass if Sites responses were buffered or lost their status/headers. Please add HTTP-level coverage with streaming enabled for progressive delivery and a buffered error/preview fallback; AGENTS.md requires e2e coverage for HTTP-visible behavior, and existing Sites/Proxy suites provide the comparable test surface.

Prompt To Fix With AI
This is a comment left during a code review.
Path: app/controllers/general.php
Line: 667-668

Comment:
**Cover the enabled streaming path through the proxy**

The new tests exercise the parser and byte counter but never run the executor-to-proxy integration, so they would still pass if Sites responses were buffered or lost their status/headers. Please add HTTP-level coverage with streaming enabled for progressive delivery and a buffered error/preview fallback; AGENTS.md requires e2e coverage for HTTP-visible behavior, and existing Sites/Proxy suites provide the comparable test surface.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

🟡 Minor · testing · Reply if this doesn't apply.

…ponses

# Conflicts:
#	tests/unit/Utopia/ResponseTest.php
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Security rules

WARNING (2 new) — review signal only; does not fail the job.

php.appwrite.weak-secret-env-default (2)

Secret-like env var (SECRET, KEY, PASS, PASSWORD, TOKEN) read with a non-empty literal default. If the variable is unset the process silently runs with a shared, guessable value. Default to '' and fail or disable the feature when it is empty.

  • src/Appwrite/Auth/EncryptionKey.php:40 — EncryptionKey::version()
    • What's wrong: _APP_OPENSSL_KEY_CURRENT falls back to '1' when unset, so every install that does not set it shares that value.
    • How to fix: Read System::getEnv('_APP_OPENSSL_KEY_CURRENT', '') and fail or disable the dependent connection when it is empty; keep the example value in .env / app/config/variables.php.
  • src/Appwrite/Auth/EncryptionKey.php:64 — EncryptionKey::accepted()
    • What's wrong: _APP_OPENSSL_KEY_OLDEST falls back to '1' when unset, so every install that does not set it shares that value.
    • How to fix: Read System::getEnv('_APP_OPENSSL_KEY_OLDEST', '') and fail or disable the dependent connection when it is empty; keep the example value in .env / app/config/variables.php.
6 existing findings tracked in .semgrep/baseline.json
  • php.appwrite.guest-write-without-abuse-limit (3)
  • php.appwrite.secret-compare-timing (1)
  • php.appwrite.weak-secret-env-default (2)

Posted by Checks / Rules. Re-runs update this comment in place. Rule details and baseline: .semgrep/README.md.

@appwrite

appwrite Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

🔵 Tier A · Mergeable after minor fixes

Limited by an open minor finding: Exercise the proxy streaming path, not only the parser

Adds an opt-in Sites response streaming path that requests the executor's streaming multipart format and forwards body chunks as they arrive. The parser reads length-prefixed parts incrementally, while previews and responses requiring buffered overrides retain the buffered path.

Verdict New comments Fixed Still open
✅ Approved 1 0 0
Finding Where
🟡 Exercise the proxy streaming path, not only the parser tests/unit/Utopia/Fetch/BodyMultipartStreamTest.php:198
Fix with agent prompt
### Issue 1
tests/unit/Utopia/Fetch/BodyMultipartStreamTest.php:198-202
**Exercise the proxy streaming path, not only the parser**

These tests establish multipart parsing and callback order, but none runs the new Executor-to-Sites response path. A regression that stops registering the callback, commits headers incorrectly, or buffers until the executor completes would still pass; the existing Sites proxy E2E tests (for example `SitesCustomServerTest::testSSRLogsPersistAndCanBeManaged`) do not enable/assert streaming either. Please add coverage for progressive client delivery and the buffered fallback cases (preview/error or an older executor), which are central to this change.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
📂 Walkthrough · 8
File Change
.env Documents the new streaming environment variable as disabled by default.
app/config/variables.php Registers the streaming environment setting and its description.
app/controllers/general.php Enables streaming for eligible Sites requests and handles buffered fallbacks and completion.
apps/console/src/lib/docs/compose-generator/composeData.ts Adds the streaming variable to the generated compose environment.
docker-compose.yml Passes the streaming variable into the application container.
src/Appwrite/Utopia/Fetch/BodyMultipartStream.php Adds an incremental parser for the executor's length-prefixed multipart stream.
src/Executor/Executor.php Adds the streaming response format and callback-based transport parsing.
tests/unit/Utopia/Fetch/BodyMultipartStreamTest.php Covers incremental parsing, malformed envelopes, and the expected executor wire format.
🔇 Filtered out · 2

Findings Hansi considered but did not post.

Finding Why
Streamed executions persist an empty response body Duplicate of another finding in this review
Persist the streamed response body Verifier: Streaming is limited to sites (general.php 583–585), and responseBody was not persisted for proxy executions before this PR either. ExecutionCompleted dispatches the execution snapshot at 852–857 before responseBody is assigned at 868; the base branch has the same ordering (766–782). The Log listener persists that earlier snapshot. The later responseBody assignment is used to send the buffered HTTP response, which streamed responses now send directly via chunk(). Thus inspection APIs do not lose a previously persisted body, and function execution APIs cannot enter this streaming path.

Reviewed 32e9702 · Details · Comment @appwrite review to re-run, or mention @appwrite with a question.

@pkg-pr-new

pkg-pr-new Bot commented Nov 3, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@13314

commit: 32e9702

@appwrite appwrite Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Tier A · Looks good to merge. Summary

Comment on lines +198 to +202
public function testStatusAndHeadersCompleteBeforeBodyBegins(): void
{
// The proxy relies on this: it cannot set a status code or headers once content is out.
$order = [];
$stream = new BodyMultipartStream('X', function (string $name, string $data, bool $isLast) use (&$order): void {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Exercise the proxy streaming path, not only the parser

These tests establish multipart parsing and callback order, but none runs the new Executor-to-Sites response path. A regression that stops registering the callback, commits headers incorrectly, or buffers until the executor completes would still pass; the existing Sites proxy E2E tests (for example SitesCustomServerTest::testSSRLogsPersistAndCanBeManaged) do not enable/assert streaming either. Please add coverage for progressive client delivery and the buffered fallback cases (preview/error or an older executor), which are central to this change.

Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/unit/Utopia/Fetch/BodyMultipartStreamTest.php
Line: 198-202

Comment:
**Exercise the proxy streaming path, not only the parser**

These tests establish multipart parsing and callback order, but none runs the new Executor-to-Sites response path. A regression that stops registering the callback, commits headers incorrectly, or buffers until the executor completes would still pass; the existing Sites proxy E2E tests (for example `SitesCustomServerTest::testSSRLogsPersistAndCanBeManaged`) do not enable/assert streaming either. Please add coverage for progressive client delivery and the buffered fallback cases (preview/error or an older executor), which are central to this change.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

🟡 Minor · testing · Reply if this doesn't apply.

This branch has not been deployed

No deployments
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.

1 participant