Skip to content

A handler cannot ask whether the client is still there #175

Closed

Description

@EdmondDantes

HttpResponse::sendable() is the only predicate a streaming handler has, and it answers four questions at once: the response is closed, or sealed by sendFile(), or has no stream ops, or the per-stream queue is full. Three of those are permanent and one is transient, so a caller cannot tell "yield and continue" from "stop".

isClosed() does not fill the gap — it reports that this code called end(), not that the peer left.

The consequence is in the field twice. Our own SSE example carried if (!$response->sendable()) break; // peer gone — stop early until #174; laravel-spawn wrapped the same call as Sse::connected() with the same wording; and YanGusik/laravel-spawn#60 truncated a proxied body at ~100 KB with that loop, under a declared Content-Length, which HTTP/2 then rejected.

Proposal

HttpResponse::isWritable(): bool — output is still possible: end() was not called, the response is not sealed by sendFile(), and the stream was not killed by the peer. Every input is a one-way latch, so a false answer stays false and the predicate is sound, unlike queue depth.

Backed by a new optional is_alive op in http_response_stream_ops_t (include/php_http_server.h:706). No backend needs new state: H2 has stream->peer_closed, the worker path has stream_credit_is_dead, H1 has the connection check h1_stream_append_chunk already makes, H3 the equivalent. A NULL slot answers from the response alone.

Part of the response body contract recorded in dev/PLAN.md.

Activity

  1. added a commit that references this issue on Aug 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions