Skip to content

Surface coordinator error body from apiCall - #1798

Open
PratimMallick wants to merge 3 commits into
developfrom
fix/api-call-error-message
Open

Surface coordinator error body from apiCall#1798
PratimMallick wants to merge 3 commits into
developfrom
fix/api-call-error-message

Conversation

@PratimMallick

@PratimMallick PratimMallick commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Goal

Fixes AND-1478 — Coordinator rejections from StreamVideoClient.apiCall should expose the server message, not a bare HTTP status.

parseError consumes the response body. The old path used it only for the auth-retry check, then rethrew the raw HttpException, so callers only saw HTTP 400.

Implementation

  • Parse the error body once in apiCall.
  • Use the parsed NetworkError for the auth-retry check.
  • On non-auth failures, log and throw the coordinator message.
  • Add unit tests for the surfaced message and the auth retry.

Testing

  • ./gradlew :stream-video-android-core:testDebugUnitTest --tests "io.getstream.video.android.core.StreamVideoClientTest"
  • ./gradlew spotlessApply
  • Trigger a coordinator 403/400 (for example a user calling UpdateCall) and confirm the log/result includes the server reason.

Made with Cursor

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of network errors by surfacing clearer server-provided error messages.
    • Authentication failures now trigger a single token refresh and retry.
    • Prevented duplicate parsing of error responses for more reliable error handling.

parseError consumes the HTTP body. Checking auth and then rethrowing
the raw HttpException left callers with only "HTTP 400".

Co-authored-by: Cursor <cursoragent@cursor.com>
@PratimMallick PratimMallick added the pr:bug Fixes a bug label Sep 2, 2026
@PratimMallick

Copy link
Copy Markdown
Contributor Author

@CodeRabbit review

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

PR checklist ✅

All required conditions are satisfied:

  • Title length is OK (or ignored by label).
  • At least one pr: label exists.
  • Sections ### Goal, ### Implementation, and ### Testing are filled, or the PR is bot-authored.
  • An issue is linked (Linear ticket or GitHub issue), or the PR is bot-authored.

🎉 Great job! This PR is ready for review.

@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@PratimMallick PratimMallick added pr:improvement Enhances an existing feature or code and removed pr:bug Fixes a bug labels Sep 2, 2026
@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: f3fd0f71-c601-4cf0-8653-e1a1bdf46393

📥 Commits

Reviewing files that changed from the base of the PR and between 354ed25 and 8f755f9.

📒 Files selected for processing (2)
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt
  • stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/StreamVideoClientTest.kt

Included review availability: Your plan provides up to 2 included reviews per hour; 1 remains after this review.


Walkthrough

The client now parses HTTP error bodies once, retries authentication failures with a refreshed token, and exposes coordinator messages for other failures. Tests cover configurable token providers, error responses, message preservation, and successful authentication retry.

Changes

Coordinator error handling

Layer / File(s) Summary
Parse errors and retry authentication
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt
apiCall parses failures once, retries authentication errors with a new token, and throws the parsed server message for other errors.
Validate error messages and token refresh
stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/StreamVideoClientTest.kt
Tests inject a configurable TokenProvider, construct coordinator errors, preserve error messages, and verify one token refresh before a successful retry.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to 8f755

Coordinator failures now expose the server message while authentication retries remain unchanged. No actionable merge-blocking risk remains beyond normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant apiCall
  participant ErrorParser
  participant TokenProvider
  apiCall->>ErrorParser: Parse HttpException once
  ErrorParser-->>apiCall: Return Error.NetworkError
  apiCall->>TokenProvider: Load fresh token for auth error
  TokenProvider-->>apiCall: Return refreshed token
  apiCall->>apiCall: Retry request
Loading

Suggested reviewers: rahul-lohra, aleksandar-apostolov

Poem

A rabbit parses errors once
A token hops through retry
Server words stay clear and bright
Auth failures get one more try
Tests guard the path with care

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.44% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 9 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the main change: exposing the coordinator error body from apiCall.
Description check ✅ Passed The description includes the required Goal, Implementation, and Testing sections and accurately explains the error-body handling, authentication retry, unit tests, and verification steps. UI, checklis…
Full details: Description check

Explanation

The description includes the required Goal, Implementation, and Testing sections and accurately explains the error-body handling, authentication retry, unit tests, and verification steps. UI, checklist, reviewer, and GIF sections are not completed, but they are not critical for this non-UI change.

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/api-call-error-message

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

SDK Size Comparison 📏

SDK Before After Difference Status
stream-video-android-core 12.30 MB 12.30 MB 0.00 MB 🟢
stream-video-android-ui-xml 5.70 MB 5.70 MB 0.00 MB 🟢
stream-video-android-ui-compose 6.23 MB 6.23 MB 0.00 MB 🟢

@PratimMallick
PratimMallick marked this pull request as ready for review September 2, 2026 07:03
@PratimMallick
PratimMallick requested a review from a team as a code owner September 2, 2026 07:03
Avoid wrapping coordinator HTTP errors in a generic Exception so callers can still classify retries, and parse the post-refresh attempt the same way.

Co-authored-by: Cursor <cursoragent@cursor.com>
@sonarqubecloud

sonarqubecloud Bot commented Sep 2, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
78.6% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

): Failure {
val networkError = parsed ?: parseError(exception).value as Error.NetworkError
logger.e { "[apiCall] HTTP ${exception.code()}: ${networkError.message}" }
return Failure(Error.ThrowableError(networkError.message, exception))

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.

Non-JSON error bodies now produce a worse message than before this PR.

coordinatorHttpFailure uses networkError.message unconditionally, but when the body isn't Stream JSON parseError never reaches the branch that reads error.message — it returns one of two fallbacks, both of which say "failed to parse error response from server". (Empty bodies land there too: errorBody()?.bytes() returns a zero-length array, not null, so decodeFromString("") throws into the parse-failure branch.)

So for a 502/504/captive-portal response the caller used to get "HTTP 502 Bad Gateway" and now gets "failed to parse error response from server: Expected start of the object '{' ...". The status code is gone from the message entirely — strictly less useful than what was there before, on the failure mode users hit most.

Suggest falling back to "HTTP ${exception.code()} ${exception.message()}" when the parse didn't produce a real coordinator error. Watch out that the no-body branch sets serverErrorCode = e.code() rather than PARSER_ERROR, so both fallbacks need covering, not just the PARSER_ERROR one.

Worth a test that throws an HttpException with a 502 and an HTML body, and asserts the status still appears in the resulting ThrowableError.message. That case is also most of the gap to the 80% Sonar gate this PR is currently under.

): Result<T> {
val firstError = parseError(first).value as Error.NetworkError
if (firstError.isAuthError()) {
val newToken = tokenProvider.loadToken()

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.

A failing loadToken() swallows the coordinator message.

If the customer's TokenProvider throws here, the exception escapes to the outer catch (e: Exception) in apiCall and the caller ends up with "Safe call failed with <token provider message>". The original 401 reason — the thing this PR exists to surface — is discarded, on the one path where the SDK definitely has it.

Wrapping the refresh and falling back to coordinatorHttpFailure(first, firstError) keeps the coordinator message and still reports the refresh failure via the log.

serverErrorCode = error.code,
statusCode = error.statusCode,
cause = Throwable(error.moreInfo),
cause = e,

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.

Two pieces of the parsed error end up unreachable by callers.

Switching cause to e is right — that's what fixes @rahul-lohra's classification point — but moreInfo isn't kept anywhere else, and the conversion to ThrowableError keeps only the message, so serverErrorCode goes too. That half of rahul's comment is still open, and parseError consumes the body so callers can't re-derive it.

If keeping ThrowableError is the decision — fair, CallCrudTest depends on it — folding both into the message ("[<code>] <message> (<moreInfo>)") at least keeps them recoverable to a human reading logs.

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

Labels

pr:improvement Enhances an existing feature or code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants