Skip to content

A streaming response cannot be aborted, so a truncated body reads as complete #171

Description

@EdmondDantes

A streaming response that fails halfway is delivered as a complete one. The handler has no way to disown what it has already begun, so a truncated body reaches the client as a well-formed, successful answer.

What happens

Once the first chunk has gone out through send(), the status and headers are committed. If the handler then throws, the dispose path finds a streaming response that was never closed and finishes it for the handler:

/* src/core/http_connection.c:2783-2787 */
if (http_response_is_streaming(...)) {
    if (!http_response_is_closed(...)) {
        /* Handler fell through without end() — emit the terminator. */
        (void)http_connection_send(conn, "0\r\n\r\n", 5);
    }

That terminator is what tells the client the body is whole. A CSV export that throws a database error after 5 of 10 MB answers 200 OK, chunked, with half the file and a clean end: curl -o exits 0 and writes a file the application never meant to produce. On HTTP/2 and HTTP/3 the equivalent close ends the stream normally rather than resetting it.

HttpResponse exposes no method for the other outcome. Nothing in the class list (src/http_response.c) closes a response as failed.

Why the handler cannot work around it

end() finishes the response cleanly, which is the wrong signal. Leaving the response open reaches the same terminator through the dispose path. Throwing past the handler reaches it too. There is no combination of the current API that produces a truncated body the client can recognise as truncated.

Suggested shape

An abort() on HttpResponse, valid only once streaming has started, that closes the response as failed:

  • HTTP/1.1: close the connection without writing the terminating chunk. A client reading a chunked body sees the connection end mid-body, which is the protocol's only way to say "this is incomplete" (curl reports 18, CURLE_PARTIAL_FILE).
  • HTTP/2: RST_STREAM on the stream, INTERNAL_ERROR unless the caller names another code.
  • HTTP/3: the same through the H3 stream reset path.

Open questions for whoever takes it: whether abort() should also be what the dispose path uses when a handler unwinds with an exception rather than falling through normally, and whether a keep-alive HTTP/1.1 connection is worth trying to preserve (it is not: without the terminator the framing is lost, so the connection has to go).

Where it bites now

laravel-spawn forwards StreamedResponse and BinaryFileResponse bodies through send() (YanGusik/laravel-spawn#57). A body callback that throws before the first chunk is answered with 500; past that point the adapter can only log, and the client is told everything is fine. This is the one hole that fix could not close.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions