Skip to content

fix(integrations): propagate backend error body in non-2xx responses (#1296)#1330

Merged
senamakel merged 5 commits into
tinyhumansai:mainfrom
oxoxDev:fix/1296-composio-error-body
May 7, 2026
Merged

fix(integrations): propagate backend error body in non-2xx responses (#1296)#1330
senamakel merged 5 commits into
tinyhumansai:mainfrom
oxoxDev:fix/1296-composio-error-body

Conversation

@oxoxDev
Copy link
Copy Markdown
Contributor

@oxoxDev oxoxDev commented May 7, 2026

Summary

  • Surface the backend's error field (or truncated raw body) in the bail / Sentry message whenever IntegrationClient::post, IntegrationClient::get, or ComposioClient::raw_delete receives a non-2xx response.
  • Pre-fix the body was read then discarded (let _body_text = …), so Backend returned 400 … had no detail; now the same path yields Backend returned 400 … : Insufficient balance (or Toolkit "X" is not enabled, etc.).
  • Diagnostic-only — no behavior change on the happy path.
  • Phase A of bug(onboarding): Composio GMAIL_FETCH_EMAILS returns 400 — profile building always fails #1296. Phase B (staging Sentry repro to identify the actual 400 reason) and Phase C (frontend onboarding resilience) follow once this lands.

Problem

Onboarding's ContextGatheringStep (Building your profile) always fails for fresh users with Gmail connected. The pipeline dies at Stage 1 with composio execute_tool failed: Backend returned 400 Bad Request and we cannot tell which 400 it is — backend executeTool.ts raises BadRequestError from three sites (tool is required, toolkit-not-allowlisted, Insufficient balance) and the openhuman client throws away the body that would name the cause. integrations/client.rs:82-96 was explicitly flagged in the issue as the diagnostic gap.

Solution

  • Add extract_error_detail(body, max_bytes) in src/openhuman/integrations/client.rs. When the body parses as the standard {success:false, error:"<msg>"} envelope, return the inner string verbatim (the authoritative failure message); otherwise truncate the raw body at a UTF-8 char boundary so the diagnostic stays bounded.
  • Hard-cap detail length at 500 bytes (MAX_ERROR_BODY_LEN) so the new path can't bloat tracing/Sentry payloads or user-facing toasts.
  • Use the helper in IntegrationClient::post, IntegrationClient::get, and ComposioClient::raw_delete. Both the report_error payload and the anyhow::bail! message now include the detail.
  • Tests: 7 new unit tests for extract_error_detail (envelope happy path, whitespace trim, missing/blank error, non-JSON fallback, empty body, multi-byte UTF-8 truncation), 3 axum-backed HTTP tests for post 400 envelope / post 500 raw body / get 403 envelope, and 2 raw_delete envelope-detail tests on the composio side. All 10 + 24 pass locally.

Submission Checklist

If a section does not apply to this change, mark the item as N/A with a one-line reason. Do not delete items.

  • Tests added or updated (happy path + at least one failure / edge case) per docs/TESTING-STRATEGY.md
  • Diff coverage ≥ 80% — every new line in extract_error_detail, the modified non-2xx branches, and the raw_delete change is exercised by the new tests. cargo test openhuman::integrations::client → 10/10; cargo test openhuman::composio::client → 24/24.
  • N/A: behaviour-only change to error messages — no feature added/removed/renamed, so the coverage matrix in docs/TEST-COVERAGE-MATRIX.md is unchanged.
  • N/A: no feature row touched, see previous item.
  • No new external network dependencies introduced (mock backend used per docs/TESTING-STRATEGY.md)
  • N/A: error-message change only, not a release-cut surface — no manual smoke-checklist update needed.
  • N/A (Phase A only) — this PR uses Refs #1296, not Closes, because the user-visible failure (onboarding still dead-ends at "Almost there!") is not yet fixed. Closing bug(onboarding): Composio GMAIL_FETCH_EMAILS returns 400 — profile building always fails #1296 requires Phase B/C follow-ups (see ## Related).

Impact

  • Runtime: Rust core sidecar / in-process. Desktop only (matches sidecar runtime scope).
  • Performance: negligible — one extra serde_json::from_str + at-most-500-byte truncation on the error path. Happy path unchanged.
  • Security: extract_error_detail truncates at 500 bytes at a UTF-8 char boundary so an unbounded backend body can't blow up tracing/Sentry. Backend error messages are written by the backend and the body was already being read into memory; no new sensitive data flows.
  • Compatibility: no API change. Bail-message format extended (Backend returned X for Y URLBackend returned X for Y URL: <detail>); any external grep-on-bail-message consumer would need to extend its match. None known.
  • No dev:app smoke run — diagnostic-only change with no happy-path UX delta. The exact code path (read body → extract envelope error → propagate) is exercised by post_400_propagates_backend_error_envelope_message against an axum mock that mirrors the real errorHandler.ts shape ({success:false, error:"…"}, status 400). Live verification of the new message landing in Sentry is deferred to Phase B post-merge.

Related

  • Refs bug(onboarding): Composio GMAIL_FETCH_EMAILS returns 400 — profile building always fails #1296 — Phase A (this PR) closes the diagnostic gap; the user-visible "Almost there!" failure is not yet resolved.
  • Follow-up PR(s)/TODOs:
    • Phase B: once this merges + staging deploys, watch staging Sentry for the next integrations.post.non_2xx event tagged with path=/agent-integrations/composio/execute. The new message body will name the actual cause (Insufficient balance / Toolkit "x" is not enabled / etc.).
    • Phase C (cause-dependent): if the cause is "Insufficient balance" → backend PR on tinyhumansai/backend to grant signup credits OR exempt onboarding tool calls from balance check; AND/OR a small openhuman frontend follow-up that on a 400 in ContextGatheringStep skips the gmail stage gracefully (mark failed, advance pipeline to LinkedIn) instead of dead-ending with "Almost there!".
    • The Closes #1296 keyword will live on whichever follow-up actually flips onboarding back to working for fresh users.

AI Authored PR Metadata (required for Codex/Linear PRs)

Keep this section for AI-authored PRs. For human-only PRs, mark each field N/A.

Linear Issue

Commit & Branch

  • Branch: fix/1296-composio-error-body
  • Commit SHA: tip on push (3 micro-commits, all GPG-signed)

Validation Run

  • N/A: pnpm --filter openhuman-app format:check — no frontend code changed
  • N/A: pnpm typecheck — no TypeScript code changed
  • Focused tests: cargo test --lib openhuman::integrations::client (11/11), cargo test --lib openhuman::composio::client (24/24)
  • Rust fmt/check (if changed): cargo fmt --check clean on changed files; cargo check --manifest-path Cargo.toml clean (warnings pre-existing main, unrelated)
  • N/A: Tauri fmt/check — no Tauri shell code changed

Validation Blocked

  • command: N/A
  • error: N/A
  • impact: N/A

Behavior Changes

  • Intended behavior change: non-2xx response from backend now includes the error envelope field (or truncated raw body) in the propagated error message + Sentry tag. No happy-path change.
  • User-visible effect: future onboarding failures of this shape will surface the actual cause in console + Sentry; the user-facing "Almost there!" screen is unchanged for now (see Phase C in ## Related).

Parity Contract

  • Legacy behavior preserved: yes — every prior caller (apify, parallel, twilio, google_places, stock_prices, composio) gets the same Result<T, anyhow::Error> shape on success and on failure. Only the failure message string is enriched.
  • Guard/fallback/dispatch parity checks: extract_error_detail falls back to truncated raw body when the envelope is malformed or missing the error field, so non-conforming backends still produce a usable diagnostic instead of an empty string.

oxoxDev and others added 3 commits May 7, 2026 15:58
…ponses (tinyhumansai#1296)

Pre-fix the post/get error paths read the response body then discarded it
(`let _body_text = …`), so callers only saw `Backend returned 400 …` with
no detail — masking the actual cause (Insufficient balance, Toolkit not
enabled, etc.). Now extract the `error` field from the standard
`{success:false,error:"…"}` envelope, fall back to truncated raw text,
and include it in both the `report_error` payload and the bail message.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…yhumansai#1296)

Same diagnostic gap as integrations/client post/get — non-2xx DELETE
responses bailed with a generic `Backend returned 4xx for DELETE …`
that hid the actual reason. Use the shared `extract_error_detail`
helper so DELETE failures (delete_connection, disable_trigger) carry
the same detail as POST/GET failures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…inyhumansai#1296)

Tightens `disable_trigger_surfaces_non_2xx_status` to also assert the
envelope error string lands in the bail message, and adds
`delete_connection_surfaces_envelope_error_detail` covering the
delete_connection path. Closes the asymmetry where post/get had
explicit envelope-detail tests but DELETE only asserted the status
code.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@oxoxDev oxoxDev requested a review from a team May 7, 2026 11:04
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 7, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR improves HTTP error reporting across Composio and integrations clients by extracting backend error details from response bodies, bounding their length for safety, and propagating them in error messages instead of reporting only HTTP status codes.

Changes

Backend Error Detail Extraction Across HTTP Clients

Layer / File(s) Summary
Error Extraction Utilities
src/openhuman/integrations/client.rs
Introduce MAX_ERROR_BODY_LEN constant (500 bytes) and extract_error_detail function that parse backend JSON { success: false, error: ... } envelopes, extract the error field, and fall back to safe UTF-8-boundary-truncated raw body text when JSON is absent or malformed.
Integrations Client Implementation
src/openhuman/integrations/client.rs
Update post and get methods to read response body text on non-2xx status, compute enriched detail via extract_error_detail, include detail in observability, and bail with {status} for {method} {url}: {detail} instead of status-only messages. Add #[cfg(test)] wiring for client tests.
Composio Client Integration
src/openhuman/composio/client.rs
Update raw_delete to extract backend error detail and bail with structured message containing HTTP status, URL, and extracted detail.
Test Helpers & Infrastructure
src/openhuman/integrations/client_tests.rs, src/openhuman/integrations/client.rs
Wire test module, add helper functions (start_mock_backend, client_for) to spin up ephemeral axum servers and construct test clients.
Error Extraction Unit Tests
src/openhuman/integrations/client_tests.rs
Test extract_error_detail behavior: JSON envelope extraction with whitespace trimming, fallback to raw body, empty/missing/blank error field handling, and safe UTF-8 truncation with ellipsis for long non-JSON bodies.
Client Integration Tests
src/openhuman/integrations/client_tests.rs, src/openhuman/composio/client_tests.rs
Verify post/get error messages surface JSON error-envelope details for 400 responses, raw HTML text for 500, and propagated errors include both HTTP status and backend-extracted detail; extend Composio tests to assert delete/disable paths propagate envelope error fields.

Sequence Diagram(s)

sequenceDiagram
  participant Backend
  participant Client
  participant Extractor
  participant Observability
  participant Caller
  
  Backend->>Client: HTTP non-2xx + response body
  Client->>Client: read body text
  Client->>Extractor: extract_error_detail(body, max_len)
  alt JSON with error field
    Extractor->>Extractor: parse JSON, trim whitespace
    Extractor-->>Client: error field value
  else Non-JSON or missing field
    Extractor->>Extractor: truncate at UTF-8 boundary
    Extractor-->>Client: truncated raw body + ellipsis
  end
  Client->>Observability: report with enriched detail
  Client-->>Caller: Err("{status} for {method} {url}: {detail}")
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • tinyhumansai/openhuman#501: Related through shared modifications to HTTP client error-reporting paths and error-body extraction utilities.
  • tinyhumansai/openhuman#1110: Related through enhancements to ComposioClient's raw_delete error handling affecting downstream disable_trigger and delete paths.

Poem

🐰 I nibbled through bytes and found the cry,

Trimmed the whitespace, let the message fly.
If JSON whispered, I kept its song,
Else clipped the noise but never long.
Now errors speak true, clear and spry.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: propagating backend error body details in non-2xx HTTP responses across integration clients.
Linked Issues check ✅ Passed The PR implements Phase A of #1296 by extracting and surfacing backend error details from non-2xx responses (composio execute_tool failures), enabling diagnostics visibility that was previously missing.
Out of Scope Changes check ✅ Passed All changes are scoped to error handling in integrations and composio clients; no unrelated refactoring, dependency updates, or out-of-scope modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/openhuman/composio/client.rs (1)

319-329: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid byte-slicing the DELETE error body in the debug log.

&body_text[..body_text.len().min(300)] can panic when the 300-byte cutoff lands inside a multibyte UTF-8 character, so some non-ASCII backend errors will crash this failure path instead of returning the enriched Err. Reuse the shared truncation helper here as well.

Proposed fix
         if !status.is_success() {
             let body_text = resp.text().await.unwrap_or_default();
             let detail = crate::openhuman::integrations::client::extract_error_detail(
                 &body_text,
                 crate::openhuman::integrations::client::MAX_ERROR_BODY_LEN,
             );
+            let logged_body = crate::openhuman::integrations::client::extract_error_detail(
+                &body_text,
+                300,
+            );
             tracing::debug!(
                 "[composio] DELETE {} → {} body={}",
                 url,
                 status,
-                &body_text[..body_text.len().min(300)]
+                logged_body
             );
🤖 Prompt for 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.

In `@src/openhuman/composio/client.rs` around lines 319 - 329, The debug log is
slicing body_text with a byte index which can panic on multibyte UTF-8; instead
reuse the already-computed safe representation and truncation: log the safe
`detail` produced by
crate::openhuman::integrations::client::extract_error_detail (or call the
module's safe truncation helper) rather than using
`&body_text[..body_text.len().min(300)]`, e.g. replace that slice in the
tracing::debug call with `detail` so the log uses a UTF-8-safe, truncated
string.
🤖 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 `@src/openhuman/integrations/client.rs`:
- Around line 40-48: The function truncate_at_char_boundary currently slices to
max bytes then appends an ellipsis, which can exceed the advertised cap; update
truncate_at_char_boundary to reserve space for the ellipsis before choosing the
slice point: compute the byte length of the ellipsis (e.g. '…') and set
available = max - ellipsis_len, then find the largest char boundary <= available
(same char-boundary loop currently using end) and slice to that boundary,
finally append the ellipsis; also handle the edge case when max is less than or
equal to the ellipsis byte length by returning a truncated ellipsis or empty
string appropriate for MAX_ERROR_BODY_LEN usage (ensure final result never
exceeds max). Mentioned symbols: truncate_at_char_boundary,
extract_error_detail, MAX_ERROR_BODY_LEN.

---

Outside diff comments:
In `@src/openhuman/composio/client.rs`:
- Around line 319-329: The debug log is slicing body_text with a byte index
which can panic on multibyte UTF-8; instead reuse the already-computed safe
representation and truncation: log the safe `detail` produced by
crate::openhuman::integrations::client::extract_error_detail (or call the
module's safe truncation helper) rather than using
`&body_text[..body_text.len().min(300)]`, e.g. replace that slice in the
tracing::debug call with `detail` so the log uses a UTF-8-safe, truncated
string.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b1d8eb0e-81b6-4b52-913a-2b8376a5095d

📥 Commits

Reviewing files that changed from the base of the PR and between ca8e4f6 and 023635c.

📒 Files selected for processing (4)
  • src/openhuman/composio/client.rs
  • src/openhuman/composio/client_tests.rs
  • src/openhuman/integrations/client.rs
  • src/openhuman/integrations/client_tests.rs

Comment thread src/openhuman/integrations/client.rs
oxoxDev and others added 2 commits May 7, 2026 16:44
…r_detail (tinyhumansai#1296)

CodeRabbit Minor: `truncate_at_char_boundary` sliced to `max` then
appended `…`, so `extract_error_detail(body, 500)` could return up to
503 bytes — leaking past the advertised hard cap that Sentry tag
values + user-facing toasts depend on. Reserve `'…'.len_utf8()` (3
bytes) before choosing the slice end. Edge case: when `max <
ellipsis_len`, return empty string rather than panic.

Tightens the existing UTF-8 truncation test to assert `out.len() <=
50` strictly, and adds `extract_error_detail_with_max_below_ellipsis_returns_empty`
covering the new edge case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…og (tinyhumansai#1296)

CodeRabbit Major: the existing `&body_text[..body_text.len().min(300)]`
in the `[composio] DELETE` debug log panics when the 300-byte cutoff
lands inside a multibyte codepoint, so non-ASCII backend errors crash
the failure path instead of returning the enriched `Err`. Reuse
`extract_error_detail(&body_text, 300)` so the log preview is bounded
by the same safe truncation as the bail message.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@oxoxDev
Copy link
Copy Markdown
Contributor Author

oxoxDev commented May 7, 2026

Addressed both CodeRabbit findings on dcf70d2:

  • Minor (line 48)truncate_at_char_boundary exceeded the 500-byte cap by the ellipsis bytes. Fixed in f3220403: reserve '…'.len_utf8() before slicing; edge case (max < ellipsis_len) returns empty string. Test tightened to assert out.len() <= 50 strictly + new extract_error_detail_with_max_below_ellipsis_returns_empty.

  • Major (composio/client.rs:319-329, outside diff) — debug log byte-sliced body_text and could panic on multibyte UTF-8. Fixed in dcf70d2c: log preview now uses extract_error_detail(&body_text, 300) so it's bounded by the same UTF-8-safe truncation as the bail message.

Targeted tests still green (11/11 integrations + 24/24 composio). Both commits GPG-signed.

@senamakel senamakel merged commit a3f8321 into tinyhumansai:main May 7, 2026
22 of 24 checks passed
AusAgentSmith pushed a commit to AusAgentSmith/openhuman that referenced this pull request May 23, 2026
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.

2 participants