feat(llm-client): harden HTTP provider transport - #271
Conversation
WalkthroughThe HTTP client now enforces redirects, timeouts, response limits, and redaction. Typed HTTP error displays omit body content. SSE decoding rejects frames larger than 8 MiB. ChangesResponse safety controls
Estimated code review effort: 4 (Complex) | ~45 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/libsy-llm-client/src/client.rs`:
- Around line 526-554: Update collect_response_body to avoid preallocating based
on response.content_length(); initialize bytes with an empty buffer or small
fixed capacity while preserving the existing limit and truncation behavior. Add
a regression test covering an inflated Content-Length with minimal body data,
verifying collection does not reserve the advertised size.
In `@crates/switchyard-translation/src/helpers.rs`:
- Around line 267-271: Align SseFrameSizeTracker’s line-boundary detection with
decode_stream’s line.trim_end().is_empty() behavior so whitespace-only lines
reset frame_bytes and do not mark current_line_has_content. Add a regression
test covering two frames separated by " \n", verifying the second frame starts
with a reset byte count.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: d43299c3-4b1c-4fa3-a3a9-666fbac50409
📒 Files selected for processing (6)
crates/libsy-llm-client/README.mdcrates/libsy-llm-client/src/backend.rscrates/libsy-llm-client/src/client.rscrates/libsy/src/core/algorithm.rscrates/protocol/src/client.rscrates/switchyard-translation/src/helpers.rs
|
|
Comment#1 above addressed in e8081d1 |
|
Comment#2 addressed in a61a981 |
|
comment#5 addressed in 4fb0a2a |
|
Some small (potential) regressions to follow-up on: Valid data:{...} fields without the optional space are silently ignored. sse_regressions.rs (line 75) reproduces this. Backend debug output leaks credentials embedded in URL userinfo. redaction.rs (line 31) reproduces this. Static header and typed provider-error redaction otherwise passed. |
| } | ||
|
|
||
| fn is_sse_frame_boundary(line: &str) -> bool { | ||
| line.as_bytes().iter().all(is_sse_whitespace) |
There was a problem hiding this comment.
Confirmed on head 889ce95 with a \r\r-delimited SSE stream. This still fails with ResponseTranslation(...) instead of decoding the frame, so bare-CR line endings are not handled on this path yet.
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
369b3fa to
00e2226
Compare
What
Hardens the reusable
switchyard-llm-clientHTTP transport used for providerdispatch. The PR establishes five generic safety guarantees:
without limit.
This is intentionally independent of the NeMo Relay dynamic-plugin implementation.
It changes the shared client and translation behavior for every caller rather than
placing Relay-specific protections in #270.
Review guide
1. Redirect rejection at the provider authority boundary
Motivation: A configured provider request can contain credentials and a full
prompt. Following an upstream redirect would allow a provider—or a compromised
endpoint—to move that request to a different authority. Redirects are therefore
returned as ordinary non-success HTTP responses instead of being followed.
TranslatingLlmClientconstructs a no-redirect shared client.configured_provider_requests_do_not_follow_redirectsverifies the redirect target receives no request.2. Configurable connection and idle-read timeouts
Motivation: Without transport timeouts, an unreachable or silent provider can
retain request resources indefinitely. The defaults are a 10-second connection
timeout and a 120-second idle-read timeout. The read timeout resets after every
successful read, so an active long-running stream is not limited to 120 seconds.
Callers serving providers that may legitimately remain silent longer can override
the values or disable the idle-read timeout.
HttpTransportConfigandnew_with_transport_config.LlmClientError::Timeoutvariant.routed_llm_client_exposes_timeout_variantexercises a short timeout through the public configuration path.3. Bounded buffered and error response bodies
Motivation: A provider controls both its response body and advertised
Content-Length. Relying on that metadata can reserve large amounts of memorybefore any bytes arrive, while collecting an unbounded body allows concurrent
responses to exhaust the process. The client now grows buffers only from received
chunks, rejects buffered success bodies above 64 MiB, and retains at most 64 KiB
of an HTTP error body with an explicit truncation marker.
collect_response_bodycollector.upstream_error_body_is_bounded_and_redacted_from_display.response_body_does_not_preallocate_advertised_content_lengthadvertises 64 MiB while yielding only two bytes.4. Diagnostic redaction without weakening typed errors
Motivation: Provider credentials can be stored in static target headers, and
provider error bodies can echo prompts or secrets. Redacting only
Displayisinsufficient because structured tracing commonly records errors with
?error,which uses
Debug. Header values and sensitive provider fields are now redactedfrom both ordinary formatting paths. The bounded body/message fields remain
available to code that explicitly matches the typed error.
HttpBackendConfig::Debugemits names but not values, covered bydebug_redacts_static_header_values.LlmClientErrordisplay and debug formatting, covered by the protocol redaction tests.5. Bounded SSE frames before line buffering
Motivation:
AsyncBufReadExt::lines()must accumulate a complete line beforereturning it. A provider that never emits an SSE frame boundary could otherwise
grow that allocation without limit. The translation layer now checks raw transport
chunks before the line reader and rejects a frame above 8 MiB. The byte limiter and
parser share the same ASCII-whitespace boundary semantics so valid small frames do
not accumulate into a false oversized-frame failure.
decode_stream_with_limitandSseFrameSizeTracker.6. Public documentation
switchyard-llm-clientREADME documents credentials, retries, timeout overrides, body limits, redaction, and the SSE limit.Unreleasedchangelog entry records the behavior change for all client consumers.Why
switchyard-llm-clientis the reusable provider-dispatch boundary for theSwitchyard server, libsy examples, and the external Relay plugin. It handles
credentials and untrusted network responses, so its defaults must protect:
These are generic client guarantees, not routing-algorithm behavior. PR #270 can
build and function independently, while consuming these safer defaults once this
PR lands.
Relates to: #270
How tested
uv run ruff check .clean (N/A: Rust-only change)uv run mypy switchyardclean (N/A: Rust-only change)uv run pytest tests/green (N/A: Rust-only change)cargo test -p switchyard-llm-clientcargo test -p switchyard-translationcargo test -p switchyard-protocol -p switchyard-libsycargo clippy --workspace --all-targets -- -D warningscargo fmt --all -- --checkChecklist
snake_caseof the primary class. (N/A: Rust-only change.)switchyard/__init__.py.__all__if intended for downstream use. (N/A: no Python symbols.)--helpupdated if customer-facing surface changed.Signed-off-by: Your Name <email>) per the DCO.Notes for reviewers
TranslatingLlmClient::newremains source-compatible and applies the safe defaults. The only new public surface isHttpTransportConfigplusnew_with_transport_configfor callers that need different timeout behavior.DisplayandDebugrendering is redacted.switchyard-translation, before its line reader. This is the allocation boundary and protects every caller of the shared decoder.Summary by CodeRabbit