Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
5835259
Add chunked content provider to response
ssubbotin Aug 5, 2026
5d6c835
Write chunked responses in the connection
ssubbotin Aug 5, 2026
5a52b50
Add tests for chunked responses
ssubbotin Aug 5, 2026
a96ac1f
Document chunked responses
ssubbotin Aug 5, 2026
b7465af
Add abort result and completion handler for chunked responses
ssubbotin Aug 6, 2026
0981e28
Keep chunked headers intact for HEAD responses
ssubbotin Aug 6, 2026
ec62cee
Remove a previously set Content-Length when enabling chunked transfer
ssubbotin Aug 6, 2026
f5ab73f
Carry response flags over in the move assignment operator
ssubbotin Aug 6, 2026
aefa8f4
Treat an exception thrown by the chunk provider as an abort
ssubbotin Aug 6, 2026
2026027
Document the contract for providers with no data at hand
ssubbotin Aug 6, 2026
8bf9162
Use HTTP/1.1 in the chunked response tests
ssubbotin Aug 6, 2026
485e609
Strengthen the large body test with real chunked decoding
ssubbotin Aug 6, 2026
44a443e
Keep skip_body out of response move-assignment
ssubbotin Aug 6, 2026
2fb5f6d
Treat mid-transfer write errors like abort and guard the completion h…
ssubbotin Aug 6, 2026
8c6cf80
Run the chunked completion handler for HEAD requests
ssubbotin Aug 6, 2026
408cadf
Make the chunk provider and the static file mutually exclusive
ssubbotin Aug 6, 2026
0a5759a
Add async chunk provider contract test
ssubbotin Aug 14, 2026
989da48
Add async chunk provider contract
ssubbotin Aug 14, 2026
40d3823
Add async chunk connection tests
ssubbotin Aug 14, 2026
0c5bd1d
Implement async chunk response writing
ssubbotin Aug 14, 2026
daa23f5
Defer reads during async chunk transfers
ssubbotin Aug 14, 2026
e2a1e15
Add async chunk responsiveness tests
ssubbotin Aug 14, 2026
6ddf86a
Harden async chunk responsiveness tests
ssubbotin Aug 14, 2026
13f7ed0
Add async chunk lifecycle tests
ssubbotin Aug 14, 2026
048032c
Fix async chunk transfer lifetime
ssubbotin Aug 14, 2026
39d6ea3
Finish async transfers during worker shutdown
ssubbotin Aug 14, 2026
a23d04b
Add async chunk HEAD and keep-alive tests
ssubbotin Aug 14, 2026
bba8f7c
Document asynchronous chunk streaming
ssubbotin Aug 14, 2026
905501c
Format asynchronous chunk changes
ssubbotin Aug 14, 2026
d84a470
Add async chunk regression coverage
ssubbotin Aug 14, 2026
7d85c79
Fix async chunk protocol and lifecycle handling
ssubbotin Aug 14, 2026
89f372c
Add response framing regression coverage
ssubbotin Aug 14, 2026
de9ff44
Fix response framing and HTTP version rejection
ssubbotin Aug 14, 2026
01eaca5
Add pipelined async response regressions
ssubbotin Aug 14, 2026
a32e2e9
Preserve pipelined input during async responses
ssubbotin Aug 14, 2026
ba689f0
Add async publication retry regression
ssubbotin Aug 14, 2026
29ce4dd
Allow retry after async publication failure
ssubbotin Aug 14, 2026
cd062c9
Contain async publication recovery
ssubbotin Aug 14, 2026
d2801a8
Add waiting peer lifecycle regressions
ssubbotin Aug 14, 2026
fed1ffb
Detect peer closure during async chunk waits
ssubbotin Aug 14, 2026
afdf007
Document async chunk failure outcomes
ssubbotin Aug 14, 2026
4661f9e
Use constexpr storage for the last chunk
ssubbotin Aug 14, 2026
8d82e11
Revert "Use constexpr storage for the last chunk"
ssubbotin Aug 14, 2026
b37e15a
Use constexpr storage for the last chunk
ssubbotin Aug 14, 2026
7d06d96
Add retained input completion race coverage
ssubbotin Aug 17, 2026
276037b
Handle terminal chunks before retained input overflow
ssubbotin Aug 17, 2026
adb968a
Preserve async response stream boundaries
ssubbotin Aug 17, 2026
bc1789c
Resume retained input across response paths
ssubbotin Aug 18, 2026
ed8549b
Resume retained input after static responses
ssubbotin Aug 18, 2026
d6bc826
Add regular response retained input regressions
ssubbotin Aug 19, 2026
99376dc
Complete regular response lifecycle handling
ssubbotin Aug 19, 2026
55e0b57
Make retained input regressions deterministic
ssubbotin Aug 19, 2026
539c5c7
Add deferred response executor affinity coverage
ssubbotin Aug 19, 2026
b2ef24d
Keep deferred response finalization on its executor
ssubbotin Aug 19, 2026
9a1b064
Test deferred HEAD response framing
ssubbotin Aug 19, 2026
2d99fb4
Serialize deferred response flag cleanup
ssubbotin Aug 19, 2026
b08d3e2
Test pipelined deferred HEAD state
ssubbotin Aug 20, 2026
d3f22b1
Isolate pipelined response flags
ssubbotin Aug 20, 2026
310fa5f
Test unmatched HEAD response boundary
ssubbotin Aug 20, 2026
1a58a58
Initialize response body policy per request
ssubbotin Aug 20, 2026
34c4d2e
Test ordinary deferred response ordering
ssubbotin Aug 20, 2026
7d54735
Preserve ordinary deferred response boundaries
ssubbotin Aug 20, 2026
3952ccc
Test queued deferred finalization shutdown
ssubbotin Aug 20, 2026
8adefe8
Track deferred responses through worker shutdown
ssubbotin Aug 20, 2026
5ecd40c
Test chunk providers on bodyless statuses
ssubbotin Aug 20, 2026
0360254
Suppress bodies for bodyless response statuses
ssubbotin Aug 20, 2026
b50d8f7
Stop parsing only while a response remains deferred
ssubbotin Aug 20, 2026
670aad2
Remove shadowed names from asynchronous response code
ssubbotin Aug 20, 2026
8ce0068
Test skipped response framing edge cases
ssubbotin Aug 20, 2026
cf6c4ce
Preserve skipped response framing and completion results
ssubbotin Aug 20, 2026
66f32ce
Preserve generated error length for HEAD responses
ssubbotin Aug 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
262 changes: 262 additions & 0 deletions docs/guides/streaming.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
A response body whose size is not known in advance, or which is simply too large
to fit in memory, can be produced on demand and sent using
[chunked transfer encoding](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encoding).

Call `#!cpp response.set_chunked_content_provider(<provider>, <mime-type>)` with a
callable that produces the body one piece at a time. Crow sets
`Transfer-Encoding: chunked`, omits `Content-Length`, and calls the provider
repeatedly while writing the response.

## The provider

```cpp
bool provider(std::string& chunk);
```

Fill `chunk` with the next piece of the body and return `#!cpp true` while more
data is coming, `#!cpp false` on the last invocation. Leaving `chunk` empty is
allowed as an occasional occurrence and sends nothing. A provider that has no
data yet should block until data is available (or finish the transfer): the
provider is called again immediately, so returning `#!cpp true` with an empty
chunk in a tight loop spins the connection thread needlessly.

### Example

```cpp
auto app = crow::SimpleApp();

CROW_ROUTE(app, "/numbers")
([](const crow::request&, crow::response& res) {
int remaining = 100;
res.set_chunked_content_provider(
[remaining](std::string& chunk) mutable -> bool {
if (remaining == 0)
return false;
chunk = std::to_string(100 - remaining) + '\n';
--remaining;
return true;
},
"text/plain");
res.end();
});
```

## Aborting the transfer

A provider that discovers midway that the body cannot be finished (the source of
the data failed, for example) should not let the response end normally: without
`Content-Length`, the terminating frame is the only thing that tells the client
the body is complete. For this case the provider can return
`#!cpp crow::chunk_result` instead of `#!cpp bool`:

```cpp
crow::chunk_result provider(std::string& chunk);
```

Return `#!cpp crow::chunk_result::more` while more data is coming,
`#!cpp crow::chunk_result::done` on the last invocation, or
`#!cpp crow::chunk_result::abort` to stop the transfer. On `abort` Crow closes
the connection without sending the terminating frame, so the client sees a
truncated body instead of a seemingly complete one.

```cpp
CROW_ROUTE(app, "/file")
([](const crow::request&, crow::response& res) {
auto file = open_source_somehow();
res.set_chunked_content_provider(
[file](std::string& chunk) mutable -> crow::chunk_result {
if (!file->read(chunk))
return crow::chunk_result::abort; // reading failed: truncate the body
return chunk.empty() ? crow::chunk_result::done : crow::chunk_result::more;
},
"application/octet-stream");
res.end();
});
```

## Asynchronous provider

Use `#!cpp set_async_chunked_content_provider` when the data source becomes
ready asynchronously. The provider receives a completion callback for one
requested chunk:

```cpp
void provider(crow::response::async_chunk_completion_t complete);
```

The provider must return promptly and call `complete` exactly once for that
invocation. It may call `complete` inline before returning, or later from any
thread. Pass `#!cpp crow::chunk_result::more` when more data remains,
`#!cpp crow::chunk_result::done` for the final chunk, or
`#!cpp crow::chunk_result::abort` when the body cannot be completed. A nonempty
chunk passed with `done` is written before the terminating frame. `more` with
an empty chunk requests another chunk without writing a frame; `done` with an
empty chunk writes only the terminating frame. `abort` discards its chunk.

`complete` returns `#!cpp true` only when Crow accepts that result for
publication on the connection executor. It returns `#!cpp false` for a repeated
or inactive result and whenever the requested publication fails. Recovery may
still publish an internal abort after such a failure; the requested result
remains rejected and the return value remains `#!cpp false`. The callback does
not let publication exceptions escape through the provider thread.

A source that produces a `more` result can use the return value to stop work
after the client disconnects or the transfer otherwise becomes inactive:

```cpp
if (!complete(crow::chunk_result::more, std::move(data))) {
source->cancel();
return;
}
```

Existing providers may ignore the return value.

```cpp
auto source = open_async_source_somehow();

CROW_ROUTE(app, "/events")
([source](const crow::request&, crow::response& res) {
res.set_async_chunked_content_provider(
[source](crow::response::async_chunk_completion_t complete) {
source->read_next(
[complete = std::move(complete)](std::error_code error,
std::string data,
bool finished) mutable {
if (error) {
complete(crow::chunk_result::abort, "");
return;
}
complete(finished ? crow::chunk_result::done
: crow::chunk_result::more,
std::move(data));
});
},
"text/event-stream");
res.set_chunked_completion_handler([](bool clean) {
if (!clean)
CROW_LOG_WARNING << "asynchronous transfer did not finish cleanly";
});
res.end();
});
```

Crow posts every accepted completion result to the connection executor,
including a result delivered inline. At most one provider request and one
socket write are pending, and Crow requests the next chunk only after the
preceding write has finished. This bounds the transfer to one current payload
chunk and provides backpressure without an unbounded queue.

When `response::end()` completes a deferred response from another thread, Crow
dispatches response finalization to the connection executor. Synchronous providers,
socket writes, retained-input processing, and normal completion callbacks
therefore keep connection-executor affinity.

While a provider request is pending, Crow reads the connection to detect peer
closure and retains bytes for later pipelined requests without parsing them.
Retained input is limited to Crow's current HTTP parser header limit, whose
compile-time default is `CROW_HTTP_MAX_HEADER_SIZE`. Exceeding that limit while
the provider is pending aborts the transfer and closes the connection. If a
terminal result is already pending, Crow finishes that response cleanly and then
closes the connection. After clean completion without overflow, Crow parses the
saved bytes on the connection executor in arrival order before starting another
socket read. An aborted or failed transfer closes the connection and discards
the retained input. A peer closure while the provider is pending ends the
transfer promptly with `clean == false` and releases the provider; a late
provider result is rejected by returning `false`.

Calling `set_async_chunked_content_provider` discards any string body, static
file, or synchronous provider already configured on the response. Calling a
synchronous provider setter or a static-file setter afterward releases the
asynchronous provider. The response therefore has one body source.

Calling `clear()` releases either kind of chunk provider and resets its framing
state. If the response is then given an ordinary string body, Crow generates
the corresponding `Content-Length` normally, including on a keep-alive
connection.

Installing an empty `async_chunk_provider_t` still selects asynchronous
streaming. When Crow reaches the provider invocation, it treats the missing
callable as a provider failure: the connection closes without a terminating
frame, and the completion handler runs exactly once with `clean == false`.

## Completion handler

To find out how the transfer ended (to release the source of the data, or to log
a failure), set a handler that is called once after the body has been written:

```cpp
res.set_chunked_completion_handler([](bool clean) {
if (!clean)
CROW_LOG_WARNING << "chunked transfer did not finish cleanly";
});
```

The handler is called exactly once. `clean` is `#!cpp true` when the provider
finished normally (`#!cpp crow::chunk_result::done`, or `#!cpp false` from the
`bool` provider) and every write succeeded; it is `#!cpp false` when the
provider aborted, the peer closed, retained input exceeded its limit while no
terminal result was pending, or a write error occurred. An exception raised before provider completion is treated as an
abort. An asynchronous transfer that is still active during server shutdown
completes with `clean == false`.

During normal completion, the handler runs on the connection's thread after the
last body write and before the response is finalized. Server worker shutdown
also invokes it on that connection worker before the worker exits. If a
`Connection` is owned directly outside the server lifecycle, destruction is the
fallback cleanup path and invokes the handler on the destruction thread. For a
`HEAD` request the provider is never called, but the handler still runs with
`clean == true` when the response ends, so it is a reliable place to release
the data source. The handler should not throw: an exception that escapes it is
logged and swallowed.

## Notes

!!! note

The provider runs on the connection's thread while the response is being
written, so a provider that blocks keeps that thread busy for the whole
transfer. This note applies to `set_chunked_content_provider`. An
asynchronous provider must return promptly and signal readiness through its
completion callback.

!!! note

The connection deadline is cancelled for the duration of the transfer.
Without that, a body that takes longer to produce than the timeout would be
cut short by the connection being closed.

!!! note

A response to a `HEAD` request releases the provider without calling it: the
headers are sent and the body is skipped. The completion handler still runs,
with `clean == true`. Chunked framing remains represented in the headers,
with `Transfer-Encoding: chunked` retained and `Content-Length` omitted.
This completion behavior also applies when a `response` object is ended
directly, without a server connection.

!!! note

A write error in the middle of the transfer is treated like `abort` as far
as the connection is concerned: the terminating frame is not sent and the
connection is closed instead of being reused for keep-alive.

!!! note

Chunked providers require HTTP/1.1. When a handler configures either a
synchronous or asynchronous chunk provider for an HTTP/1.0 request, Crow
does not invoke the provider. It replaces the streaming response with a
finite `505 HTTP Version Not Supported` response, removes
`Transfer-Encoding`, `Content-Encoding`, and `Trailer`, adds
`Content-Length`, closes the connection, and invokes the completion handler
exactly once with `clean == false`. A `HEAD` request reports the length of
that finite error representation while sending no response body.

!!! note

Clean completion writes the terminating frame before the response is
finalized. If the request permits keep-alive, Crow clears the completed
response and parser state, restores the connection deadline, and reads the
next request from the same connection. `abort`, provider exceptions, and
write errors close the connection without a terminating frame. A late or
repeated asynchronous completion result is ignored.
1 change: 1 addition & 0 deletions include/crow/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ namespace crow
BAD_GATEWAY = 502,
SERVICE_UNAVAILABLE = 503,
GATEWAY_TIMEOUT = 504,
HTTP_VERSION_NOT_SUPPORTED = 505,
VARIANT_ALSO_NEGOTIATES = 506,
WEBDAV_INSUFFICIENT_STORAGE = 507
};
Expand Down
Loading