Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
89 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
6ea19bc
Test deferred response shutdown ownership
ssubbotin Aug 20, 2026
2ae159d
Retain deferred responses through server shutdown
ssubbotin Aug 20, 2026
8ad5723
Test static response read failures
ssubbotin Aug 20, 2026
7acef7e
Report incomplete static response writes
ssubbotin Aug 20, 2026
226dc85
Test deferred end with a body after worker shutdown
ssubbotin Aug 21, 2026
0521c29
Keep the deferred lifecycle alive through end()
ssubbotin Aug 21, 2026
9ddcc30
Test completion reporting on connection destruction
ssubbotin Aug 21, 2026
ae66583
Report unstarted deferred completion on destruction
ssubbotin Aug 21, 2026
968a9ff
Canonicalize chunked framing and copy transfer header bytes
ssubbotin Aug 21, 2026
7bd51ef
Confine deferred response finalization to the executor
ssubbotin Aug 21, 2026
3a82db0
Close after early input instead of replaying pipelined bytes
ssubbotin Aug 21, 2026
5304c2a
Drive the synchronous provider through the asynchronous engine
ssubbotin Aug 21, 2026
215b5b1
Bound streaming writes, idle waits, and chunk sizes
ssubbotin Aug 21, 2026
6080dea
Extract the streaming tests and harden transfer edge cases
ssubbotin Aug 21, 2026
258bf3a
Harden protocol edges, threading contracts, and failure paths after r…
ssubbotin Aug 21, 2026
c7f5ac9
Close review gaps in normalization, exception safety, and provider gu…
ssubbotin Aug 24, 2026
01e3d79
Keep the chunked completion handler through response move-assignment
ssubbotin Aug 24, 2026
9b73471
Bound stream writes by progress instead of the request timeout
ssubbotin Sep 4, 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
123 changes: 123 additions & 0 deletions docs/guides/streaming.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
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();
});
```

## 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";
});
```

`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 or a
write error occurred. The handler runs on the connection's thread, after the
last write and before the response is finalized. 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 source of
the data. 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.

!!! 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 never calls the provider: the headers are sent
and the body is skipped. The completion handler still runs, with
`clean == true`.

!!! 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.
156 changes: 156 additions & 0 deletions include/crow/http_connection.h
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,10 @@ namespace crow
{
do_write_static();
}
else if (res.is_chunked_type())
{
do_write_chunked();
}
else
{
do_write_general();
Expand Down Expand Up @@ -331,6 +335,158 @@ namespace crow
parser_.clear();
Comment thread
ssubbotin marked this conversation as resolved.
}

/// Format a chunk size the way chunked transfer encoding wants it: lowercase hex.
static std::string chunk_size_to_hex(std::size_t value)
{
static const char digits[] = "0123456789abcdef";
if (value == 0)
{
return "0";
}
std::string out;
while (value != 0)
{
out.insert(out.begin(), digits[value & 0xF]);
value >>= 4;
}
return out;
}

void do_write_chunked()
{
error_code ec;
asio::write(adaptor_.socket(), buffers_, ec); // Write the response start / headers
if (ec)
{
CROW_LOG_ERROR << ec << " - buffer write error happened while sending response start / headers. Writing stopped premature.";
}

// Producing the body may take arbitrarily long, so the connection must not be
// closed by the deadline while chunks are still on their way.
cancel_deadline_timer();

// do_write_sync() clears the response on every write, so the provider and the
// completion handler have to be taken out of it before the loop starts.
auto provider = std::move(res.chunk_provider_ex_);
res.chunk_provider_ex_ = nullptr;
if (!provider && res.chunk_provider_)
Comment thread
ssubbotin marked this conversation as resolved.
Outdated
{
provider = [bool_provider = std::move(res.chunk_provider_)](std::string& chunk) {
return bool_provider(chunk) ? response::chunk_result::more : response::chunk_result::done;
};
}
res.chunk_provider_ = nullptr;
auto completion_handler = std::move(res.chunk_complete_);
res.chunk_complete_ = nullptr;

std::string chunk;
std::string chunk_header;
std::vector<asio::const_buffer> buffers{3};
auto result = provider ? response::chunk_result::more : response::chunk_result::done;
while (result == response::chunk_result::more && !ec)
{
chunk.clear();
// An exception from the provider must not escape into the Asio stack; it is
// treated as an abort: no terminating frame, forced close, completion(false).
try
{
result = provider(chunk);
}
catch (const std::exception& e)
{
CROW_LOG_ERROR << "An uncaught exception occurred in the chunk provider: " << e.what();
result = response::chunk_result::abort;
}
catch (...)
{
CROW_LOG_ERROR << "An uncaught exception occurred in the chunk provider.";
result = response::chunk_result::abort;
}
if (result == response::chunk_result::abort || chunk.empty())
{
continue;
}

chunk_header = chunk_size_to_hex(chunk.size());
chunk_header += crlf;
buffers[0] = asio::const_buffer(chunk_header.data(), chunk_header.size());
buffers[1] = asio::const_buffer(chunk.data(), chunk.size());
buffers[2] = asio::const_buffer(crlf.data(), crlf.size());
ec = do_write_sync(buffers);
if (ec)
{
CROW_LOG_ERROR << ec << " - buffer write error happened while sending a chunk. Writing stopped premature.";
}
}

// The terminating frame marks the body as complete, so it is only sent when the
// provider finished cleanly and every previous write succeeded.
if (result == response::chunk_result::done && !ec)
{
static const std::string last_chunk = "0\r\n\r\n";
Comment thread
ssubbotin marked this conversation as resolved.
Outdated
std::vector<asio::const_buffer> tail{1};
tail[0] = asio::const_buffer(last_chunk.data(), last_chunk.size());
ec = do_write_sync(tail);
if (ec)
{
CROW_LOG_ERROR << ec << " - buffer write error happened while sending the last chunk.";
}
}

// A write failure leaves the message framing just as incomplete as an explicit
// abort (the terminating frame never made it out), so the connection policy is
// the same for both: force the close and never reuse the socket for keep-alive.
const bool aborted = (result == response::chunk_result::abort);
const bool force_close = aborted || static_cast<bool>(ec);
if (force_close)
{
// Close the connection forcefully, without the terminating frame, so that the
// client sees a truncated body instead of a seemingly complete one.
adaptor_.shutdown_readwrite();
adaptor_.close();
CROW_LOG_DEBUG << this << " from write (chunked, " << (aborted ? "aborted" : "write error") << ")";
}

if (completion_handler)
{
// An exception from the handler must not escape into the Asio stack or skip
// the cleanup below; it is logged and swallowed.
try
{
completion_handler(result == response::chunk_result::done && !ec);
}
catch (const std::exception& e)
{
CROW_LOG_ERROR << "An uncaught exception occurred in the chunked completion handler: " << e.what();
}
catch (...)
{
CROW_LOG_ERROR << "An uncaught exception occurred in the chunked completion handler.";
}
}

if (close_connection_ && !force_close)
{
adaptor_.shutdown_readwrite();
adaptor_.close();
CROW_LOG_DEBUG << this << " from write (chunked)";
}

res.end();
res.clear();
buffers_.clear();
parser_.clear();

// The deadline was cancelled for the duration of the transfer, so a kept-alive
// connection has to be put back into reading state explicitly.
if (!force_close && !close_connection_ && need_to_start_read_after_complete_)
{
need_to_start_read_after_complete_ = false;
start_deadline();
do_read();
Comment thread
ssubbotin marked this conversation as resolved.
Outdated
}
}

void do_write_general()
{
error_code ec;
Expand Down
Loading
Loading