Skip to content

fix(sites): stream chunked responses - #13314

Merged
HarshMN2345 merged 33 commits into
mainfrom
fix/sites-chunked-responses
Nov 3, 2026
Merged

HarshMN2345 merged 33 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 or an SSE endpoint reaches the visitor as the runtime produces 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.
  • Bumps openruntimes/executor to 1.3.0 (feat: add streamed response format 0.12.0 open-runtimes/executor#250), the first release that emits 0.12.0, in docker-compose.yml and the Console compose snapshot.
  • 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.

Tests:

  • testSiteStreaming (Sites e2e): an Astro page rendering three parts two seconds apart arrives part by part through the proxy. .env enables the flag for CI.
  • BodyMultipartStreamTest covers incremental parsing, the executor wire format, malformed separators and incomplete envelopes.
  • Locally against a full stack: SSE events arrived at +0.03, +2.03 and +4.04 s with streaming on, and all at +4.06 s with it off.

@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 182.34 195.55 🟢 +7.2%
⏱️ Latency P50 93.25 ms 88.04 ms 🟢 -5.6%
⏱️ Latency P95 229.03 ms 209.26 ms 🟢 -8.6%
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 88.04 209.26 12,426 195.55 -19.78
Account 172.21 310.48 654 10.87 -24.84
TablesDB 85.05 153.2 6,758 108.81 -20.65
Storage 81.85 192.96 3,270 54.72 -4.43
Functions 128.03 254.19 1,744 29.77 -13.11

Top API waits (after)

API request Max wait (ms)
account.name.update 559.34
functions.variables.update 478.8
account.prefs.update 475.51
functions.variables.create 423.57
functions.variables.delete 414.52

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

No new WARNING or ERROR findings from security rules.

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 S · Ready to merge

The incremental change keeps the test request and fixture parameter in sync and introduces no behavior defect.

Adds opt-in streaming for eligible Sites responses using the executor’s multipart response format, forwarding body chunks as they arrive while retaining buffered handling for previews and error pages. It adds the streaming configuration and executor version updates, an incremental multipart parser, and unit and end-to-end coverage for progressive delivery. The latest test adjustment uses code instead of status as the query parameter controlling the Astro response status.

Latest changes: The newest commits rename the Astro test page’s response-status query parameter from status to code and update the error-status test request accordingly.

Verdict New comments Fixed Still open
✅ Approved 0 0 0
📂 Walkthrough · 10
File Change
.env Sets the Compose default for Sites response streaming.
app/config/variables.php Registers and documents the Sites streaming environment variable.
app/controllers/general.php Selects streaming for eligible Sites requests and forwards response chunks while preserving buffered fallbacks.
apps/console/src/lib/docs/compose-generator/composeData.ts Passes the streaming setting through generated Compose data and updates the executor image version.
docker-compose.yml Passes the streaming setting through and updates the executor image version.
src/Appwrite/Utopia/Fetch/BodyMultipartStream.php Parses the executor multipart envelope incrementally.
src/Executor/Executor.php Requests the streaming response format and exposes response parts as they arrive.
tests/e2e/Services/Sites/SitesCustomServerTest.php Checks progressive delivery and buffered error-status behavior; the latest change aligns the error test query parameter with the fixture.
tests/resources/sites/astro/src/pages/stream.astro Provides progressive page content and query-controlled response status; the latest change uses code for the status query parameter.
tests/unit/Utopia/Fetch/BodyMultipartStreamTest.php Covers incremental parsing, executor wire format, malformed separators, and incomplete envelopes.

Reviewed the commits since d4797c8 · 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: d8e3f17

@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

@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 S · Looks good to merge. Summary

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