A raw channel beside the JSON one
06-08-2026
A Phlo route normally answers in commands: apply() and chunk() speak the JSON protocol that the SPA engine understands. But some responses are not commands. A generated ZIP, a PDF, a CSV export, a line of model output arriving token by token: those are data, and wrapping them in a command channel helps nobody.
That is what the stream resource is for. On the server, stream() emits text or binary parts under any content type you name, with the right headers set once and buffering disabled so the parts actually leave the machine as they are produced:
route POST export => stream($this->rows, 'text/csv', 'export.csv')
On the client, app.stream() consumes it and dispatches on the response type: NDJSON and server-sent events arrive per record through your callback, JSON and blobs resolve in one go, text streams as it lands. It carries the CSRF token, so it is a first-class request rather than a bare fetch.
Landed today: when a response carries a Content-Disposition, the blob you get back is a File with that name on it, so a download link can say a.download = blob.name instead of rebuilding the filename in the browser. The server already decided what the file is called; now that decision travels with it. File extends Blob, so existing code is unaffected.
In use on this site for the installer, the CMS builder and the server-setup script.
The stream resource is in engine 1.0.1; the filename behaviour is unreleased. See the stream resource in the manual.