-
-
Notifications
You must be signed in to change notification settings - Fork 556
Add asynchronous chunked content provider for streaming responses #1213
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ssubbotin
wants to merge
89
commits into
CrowCpp:master
Choose a base branch
from
ssubbotin:feature/chunked-response-provider
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
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 5d6c835
Write chunked responses in the connection
ssubbotin 5a52b50
Add tests for chunked responses
ssubbotin a96ac1f
Document chunked responses
ssubbotin b7465af
Add abort result and completion handler for chunked responses
ssubbotin 0981e28
Keep chunked headers intact for HEAD responses
ssubbotin ec62cee
Remove a previously set Content-Length when enabling chunked transfer
ssubbotin f5ab73f
Carry response flags over in the move assignment operator
ssubbotin aefa8f4
Treat an exception thrown by the chunk provider as an abort
ssubbotin 2026027
Document the contract for providers with no data at hand
ssubbotin 8bf9162
Use HTTP/1.1 in the chunked response tests
ssubbotin 485e609
Strengthen the large body test with real chunked decoding
ssubbotin 44a443e
Keep skip_body out of response move-assignment
ssubbotin 2fb5f6d
Treat mid-transfer write errors like abort and guard the completion h…
ssubbotin 8c6cf80
Run the chunked completion handler for HEAD requests
ssubbotin 408cadf
Make the chunk provider and the static file mutually exclusive
ssubbotin 0a5759a
Add async chunk provider contract test
ssubbotin 989da48
Add async chunk provider contract
ssubbotin 40d3823
Add async chunk connection tests
ssubbotin 0c5bd1d
Implement async chunk response writing
ssubbotin daa23f5
Defer reads during async chunk transfers
ssubbotin e2a1e15
Add async chunk responsiveness tests
ssubbotin 6ddf86a
Harden async chunk responsiveness tests
ssubbotin 13f7ed0
Add async chunk lifecycle tests
ssubbotin 048032c
Fix async chunk transfer lifetime
ssubbotin 39d6ea3
Finish async transfers during worker shutdown
ssubbotin a23d04b
Add async chunk HEAD and keep-alive tests
ssubbotin bba8f7c
Document asynchronous chunk streaming
ssubbotin 905501c
Format asynchronous chunk changes
ssubbotin d84a470
Add async chunk regression coverage
ssubbotin 7d85c79
Fix async chunk protocol and lifecycle handling
ssubbotin 89f372c
Add response framing regression coverage
ssubbotin de9ff44
Fix response framing and HTTP version rejection
ssubbotin 01eaca5
Add pipelined async response regressions
ssubbotin a32e2e9
Preserve pipelined input during async responses
ssubbotin ba689f0
Add async publication retry regression
ssubbotin 29ce4dd
Allow retry after async publication failure
ssubbotin cd062c9
Contain async publication recovery
ssubbotin d2801a8
Add waiting peer lifecycle regressions
ssubbotin fed1ffb
Detect peer closure during async chunk waits
ssubbotin afdf007
Document async chunk failure outcomes
ssubbotin 4661f9e
Use constexpr storage for the last chunk
ssubbotin 8d82e11
Revert "Use constexpr storage for the last chunk"
ssubbotin b37e15a
Use constexpr storage for the last chunk
ssubbotin 7d06d96
Add retained input completion race coverage
ssubbotin 276037b
Handle terminal chunks before retained input overflow
ssubbotin adb968a
Preserve async response stream boundaries
ssubbotin bc1789c
Resume retained input across response paths
ssubbotin ed8549b
Resume retained input after static responses
ssubbotin d6bc826
Add regular response retained input regressions
ssubbotin 99376dc
Complete regular response lifecycle handling
ssubbotin 55e0b57
Make retained input regressions deterministic
ssubbotin 539c5c7
Add deferred response executor affinity coverage
ssubbotin b2ef24d
Keep deferred response finalization on its executor
ssubbotin 9a1b064
Test deferred HEAD response framing
ssubbotin 2d99fb4
Serialize deferred response flag cleanup
ssubbotin b08d3e2
Test pipelined deferred HEAD state
ssubbotin d3f22b1
Isolate pipelined response flags
ssubbotin 310fa5f
Test unmatched HEAD response boundary
ssubbotin 1a58a58
Initialize response body policy per request
ssubbotin 34c4d2e
Test ordinary deferred response ordering
ssubbotin 7d54735
Preserve ordinary deferred response boundaries
ssubbotin 3952ccc
Test queued deferred finalization shutdown
ssubbotin 8adefe8
Track deferred responses through worker shutdown
ssubbotin 5ecd40c
Test chunk providers on bodyless statuses
ssubbotin 0360254
Suppress bodies for bodyless response statuses
ssubbotin b50d8f7
Stop parsing only while a response remains deferred
ssubbotin 670aad2
Remove shadowed names from asynchronous response code
ssubbotin 8ce0068
Test skipped response framing edge cases
ssubbotin cf6c4ce
Preserve skipped response framing and completion results
ssubbotin 66f32ce
Preserve generated error length for HEAD responses
ssubbotin 6ea19bc
Test deferred response shutdown ownership
ssubbotin 2ae159d
Retain deferred responses through server shutdown
ssubbotin 8ad5723
Test static response read failures
ssubbotin 7acef7e
Report incomplete static response writes
ssubbotin 226dc85
Test deferred end with a body after worker shutdown
ssubbotin 0521c29
Keep the deferred lifecycle alive through end()
ssubbotin 9ddcc30
Test completion reporting on connection destruction
ssubbotin ae66583
Report unstarted deferred completion on destruction
ssubbotin 968a9ff
Canonicalize chunked framing and copy transfer header bytes
ssubbotin 7bd51ef
Confine deferred response finalization to the executor
ssubbotin 3a82db0
Close after early input instead of replaying pipelined bytes
ssubbotin 5304c2a
Drive the synchronous provider through the asynchronous engine
ssubbotin 215b5b1
Bound streaming writes, idle waits, and chunk sizes
ssubbotin 6080dea
Extract the streaming tests and harden transfer edge cases
ssubbotin 258bf3a
Harden protocol edges, threading contracts, and failure paths after r…
ssubbotin c7f5ac9
Close review gaps in normalization, exception safety, and provider gu…
ssubbotin 01e3d79
Keep the chunked completion handler through response move-assignment
ssubbotin 9b73471
Bound stream writes by progress instead of the request timeout
ssubbotin File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.