Skip to content

refactor(api_client): unify get_bytes behind CalloutClient#492

Open
leseb wants to merge 6 commits into
praxis-proxy:mainfrom
leseb:leseb/fix-issue-454-core-bump
Open

refactor(api_client): unify get_bytes behind CalloutClient#492
leseb wants to merge 6 commits into
praxis-proxy:mainfrom
leseb:leseb/fix-issue-454-core-bump

Conversation

@leseb

@leseb leseb commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Unifies get_bytes behind CalloutClient instead of a separate reqwest::Client, completing the work unblocked by the praxis-core 0.4.1 bump (#486). Content downloads now share circuit-breaker, callout-depth protection, and bounded reads via CalloutConfig::max_response_bytes. A per-request max_bytes post-collection check preserves ResponseTooLarge → 413 for file-level budgets.

  • Removed content_client field and read_bounded_body from ApiClient
  • Routed get_bytes through CalloutClient with tokio::time::timeout wrapping the full operation
  • Set max_response_bytes to the filter's max_body_bytes in openai_file_resolve
  • Added test for client-wide max_response_bytes rejection

Closes #454

…oxy#454)

Route content downloads through CalloutClient instead of a separate
reqwest::Client, gaining circuit-breaker and callout-depth protection.
The client-wide max_response_bytes caps memory during collection while
a per-request max_bytes post-check preserves ResponseTooLarge (413).

Closes praxis-proxy#454

Signed-off-by: Sébastien Han <seb@redhat.com>
@leseb
leseb requested review from a team and alexsnaps July 23, 2026 13:04
@leseb
leseb enabled auto-merge July 23, 2026 13:16
client: &CalloutClient,
request: CalloutRequest,
) -> Result<praxis_core::callout::CalloutResponse, ApiClientError> {
match client.execute(request).await {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue #454 requires preserving ResponseTooLarge so openai_file_resolve returns 413 for size limits. The per-request max_bytes post-check correctly maps to ResponseTooLarge -> 413 when the body is fully collected under max_response_bytes. When collection hits the client-wide max_response_bytes first, praxis-core read_body fails, execute_callout maps CalloutResult::Rejected to CalloutFailed, and resolve_error_response returns 502. Example: max_body_bytes=64 with a 70 MiB download and absent or understated metadata.bytes collects until the client-wide cap, then returns 502 instead of 413. get_bytes_client_wide_limit_rejects_oversized_chunked_response encodes CalloutFailed for this path rather than ResponseTooLarge. Map client-wide size rejections to ResponseTooLarge (or propagate a distinct size error from execute_callout) before they reach the filter.

leseb added 3 commits July 23, 2026 17:35
The get_bytes change to wrap execute_callout in tokio::time::timeout
increased the async future size by ~600 bytes. This pushes
sync_state_with_budget and resolve_state_history over clippy's
16 KiB stack-size threshold after merging the file_url feature.

Box::pin the internal sync_message_history and sync_persisted_history
calls to move those futures to the heap.

Signed-off-by: Sébastien Han <seb@redhat.com>

@praxis-bot praxis-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: refactor(api_client): unify get_bytes behind CalloutClient

Clean, well-executed refactoring that eliminates the separate reqwest::Client and routes all outbound requests through CalloutClient. The unification adds circuit-breaker protection, callout-depth accounting, and max_response_bytes enforcement to content downloads, closing the gap documented in #454.

CI passes (21/21 checks). Test coverage is good: the new get_bytes_client_wide_limit_rejects_oversized_chunked_response test validates the client-wide limit, and the existing tests were correctly updated to use test_client_with_limits with explicit max_response_bytes values.

The Box::pin additions in file_resolve/mod.rs are necessary to keep async future sizes bounded after the callout client change increases the transitive future size of get_bytes.

Severity Count
Critical 0
Large 0
Medium 1

One medium behavioral observation noted inline. No blocking issues.

let response = tokio::time::timeout(self.timeout, execute_callout(&self.client, request))
.await
.map_err(|_elapsed| ApiClientError::CalloutFailed {
detail: "content download timed out".to_owned(),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] Behavioral change in memory bounding: the old read_bounded_body rejected responses chunk-by-chunk at the per-request max_bytes limit, so peak allocation was bounded to max_bytes. Now, CalloutClient::read_body collects the full response up to max_response_bytes before this post-collection check runs.

In openai_file_resolve, max_response_bytes is set to max_body_bytes (default 64 MiB) while max_content_bytes passed here from fetch_content can be much smaller (derived from the per-file resolution budget). A backend returning a response larger than the per-file budget but within 64 MiB will now be fully collected before rejection, whereas the old code would halt at the per-file limit.

The PR description and doc comments document this tradeoff clearly, so this is noted for the record rather than as a requested change. If per-file budgets are typically orders of magnitude smaller than max_body_bytes, a future optimization could set a tighter max_response_bytes on a per-download basis.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor(apis): upgrade praxis-core for bounded callout reads

3 participants