Repository navigation
feat(client): Interceptor, a per-attempt hook with static dispatch - #10
Merged
Merged
Conversation
`Client<I = NoInterceptor>` and `RequestBuilder<I>` are generic over an `Interceptor` with three hooks, all no-ops by default: - `before_send(&mut OutgoingRequest)`: once the attempt has a connection, before it is written. It can read the method, URL, attempt number, redirect hop and whether the connection is reused, and change that attempt's headers. It runs again on the pool's stale-connection retry and on each redirect hop, which is what a header that must be fresh per attempt needs. - `after_response(&ResponseHead, &Timing)`: at the response head, before the body; `Timing` gives the time to first byte and connection reuse. - `on_error(&io::Error, &Timing)`: a failed attempt, including one a retry then hides from the caller. `NoInterceptor` sets `Interceptor::ENABLED = false`, so the client skips building the hook arguments (and reading the clock): a client without an interceptor compiles to what it was. `headers_mut()` copies the request's headers only when called, so a read-only interceptor keeps the borrowed map. `ClientBuilder::build_with_interceptor` builds one; `Client::with_interceptor` gives the same pool, configuration and counters another interceptor. `BufferedResponse::reused_connection()` and `StreamingResponse::reused_connection()` say whether the response came over a pooled connection. bench_client gains `pool-hook`: the pool with empty hooks that do not opt out, to measure the plumbing against `pool`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y3TSRmEZAYU6RBUXhkn7WF
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
What
ClientandRequestBuilderbecome generic over anInterceptor: per-attempt hooks with static dispatch. The default type parameter isNoInterceptor, soClientkeeps meaning what it meant and existing code compiles unchanged.before_sendruns once the attempt has a connection, before anything is written. It can read the method, URL, attempt number, redirect hop, and whether the connection is reused, and it can change that attempt's headers.headers_mut()copies the request's headers only when it is called, so an interceptor that only reads keeps the borrowed map from perf(client): borrow request headers when nothing is merged; no copy into the request target #9.after_responseruns at the response head, before the body.Timinggives the time to first byte and whether the connection was reused.on_errorruns for a failed attempt, including one that a retry then hides from the caller.NoInterceptorsetsENABLED = false. The client then skips building the hook arguments and reading the clock, so a client without an interceptor compiles to what it was before.ClientBuilder::build_with_interceptor(i)builds a client with one.Client::with_interceptor(i)gives an existing client another interceptor while keeping its pool, configuration and counters.BufferedResponse::reused_connection()andStreamingResponse::reused_connection().The module docs and the README describe it as the extension point for per-attempt behaviour.
Overhead
examples/bench_client(new modepool-hook: empty hooks that do not opt out, so the arguments are built and the hooks called) againstbench_serveron loopback, 16 coroutines × 3000 requests. Five interleaved rounds on ms02; the table shows medians.Allocations do not change. Req/s varies by about ±5% between rounds on a shared host, and the three rows are within that.
Tests
interceptor_runs_on_every_attempt_including_the_stale_retry:before_sendcarries attempt 1, then 2 on the pool's retry, and is sent once;with_interceptor_shares_the_pool_and_reports_reuse: a buffered request, then a streaming one on the same pool with an interceptor.cargo fmt --check, clippy (default with-D warnings, and all features with all targets), and the full suite with all features (312 passed) and with default features, on Rust 1.99.0.🤖 Generated with Claude Code
https://claude.ai/code/session_01Y3TSRmEZAYU6RBUXhkn7WF