Skip to content

fix(http): Align HTTP client error type and value - #3934

Open
buenaflor wants to merge 16 commits into
v10-branchfrom
fix/http-client-error-type-and-value
Open

fix(http): Align HTTP client error type and value#3934
buenaflor wants to merge 16 commits into
v10-branchfrom
fix/http-client-error-type-and-value

Conversation

@buenaflor

@buenaflor buenaflor commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

📜 Description

HTTP client errors from package:http, Dio and Supabase now produce the same short, obfuscation-stable exception the other Sentry SDKs do.

Issue titles

Dio dumped its own toString() into the value — the status code's meaning, an MDN link and generic remediation advice — and its type didn't survive obfuscation:

- Qx: DioException [bad response]: This exception was thrown because the response has a
- status code of 502 and RequestOptions.validateStatus was configured to throw for this
- status code. […MDN link and remediation advice…]
+ DioException: HTTP Client Error with status code: 502

package:http and Supabase were already concise but carried a stray Exception: prefix, and their types didn't survive obfuscation either:

- Qy: Exception: HTTP Client Error with status code: 404
+ SentryHttpClientError: HTTP Client Error with status code: 404

Connection-level failures — timeouts, DNS errors, bad certificates — produced no event at all in Dio, because there's no status code to match against. They're now captured, with the specific cause chained rather than in the title, so grouping stays per failure kind instead of splintering per hostname:

- (no event sent)
+ DioException: HTTP Client Error: connection error
+   └─ SocketException: Failed host lookup: 'example.invalid'

Event payload

Before After
contexts.response (Dio) absent — the response reached the beforeSend hint and stopped there status code, headers, cookies, body size
Response headers sent unmasked under sendDefaultPii Authorization, Cookie and friends filtered out
mechanism.handled unset true
Requests to the DSN captured like any other skipped, on an exact host match
failedRequestTargets (Dio) matched the baseUrl-relative path, so a host target never matched matched against the resolved URL

Important

Issue titles and grouping change for anyone already capturing failed requests, and the newly captured connection failures add event volume. That's why this targets v10-branch.

Two deliberate divergences from sentry-javascript: it never captures connection-level failures at all, where Dio now does, matching our own FailedRequestClient. And it sets handled: false where we set truehandled == false ends the native session as crashed, and captureFailedRequests defaults to on here while js's integration is opt-in, so copying that value would report a crashed session for every captured 5xx.

💡 Motivation and Context

Refs #2914. Obfuscated builds reported these as Qx with a wall of Dio's own remediation text as the value, which is unsearchable and groups badly. The spec and every other SDK use a single short line.

💚 How did you test it?

Unit tests in each affected package. Manually verified on an Android emulator with three new example buttons covering a 404, a connection timeout and a DNS failure.

📝 Checklist

  • I reviewed submitted code
  • I added tests to verify changes
  • No new PII added or SDK only sends newly added PII if sendDefaultPii is enabled
  • I updated the docs if needed
  • All tests passing
  • Public API changes reviewed by another Mobile SDK team member or implemented according to the develop docs spec
  • No breaking changes

🔮 Next steps

  • Cherry-pick the header filtering and the target-matching fix to main — neither affects grouping.
  • Document that Dio now reports connection-level failures.

buenaflor and others added 12 commits July 24, 2026 21:06
Under --obfuscate, DioException.runtimeType resolves to a minified
name such as Qx, which becomes the issue title. Register a
DioExceptionTypeIdentifier so the type stays readable without
requiring the obfuscation map upload.

packages/dart and packages/flutter already ship an identifier; dio
was the only integration without one.

Co-Authored-By: Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
A bad response renders as four lines of boilerplate: the meaning of
the status code, a link to MDN and generic remediation advice. Replace
it with the status code and route, which are what identify the
failure. The request URL and status code are already on the event, and
the untouched DioException stays on SentryException.throwable for
beforeSend.

Gated to the cases that carry no information of their own -
badResponse and a null message - so timeouts keep the duration that
elapsed and connection errors keep the host that failed. Skipped
entirely when the user set DioException.stringBuilder or the global
readableStringBuilder.

Fixes GH-2914

Co-Authored-By: Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
failedRequestTargets was matched against requestOptions.path, which is
relative to baseUrl, so a host-based target like ['myapi.com'] never
matched and the request was silently not captured. Match on the
resolved URL instead, as FailedRequestClient does.

Co-Authored-By: Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
SentryHttpClientError appeared in no ExceptionTypeIdentifier, so the
issue title obfuscated in release builds just like dio's did. Its
toString() also prefixed the message with 'Exception: ', which no
other SDK emits.

The value now reads exactly as sentry-javascript's does:
'HTTP Client Error with status code: 404'.

Co-Authored-By: Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
SentrySupabaseClientError carried the same two problems as
SentryHttpClientError: no ExceptionTypeIdentifier, so the issue title
obfuscated, and an 'Exception: ' prefix on the value that no other SDK
emits.

Co-Authored-By: Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The previous format repeated the exception type, which Sentry already
renders as the issue title prefix, and embedded the request path, making
the value vary per path parameter. Use the same string the HTTP client
spec, sentry-javascript and FailedRequestClient produce, so a Dio 502 and
a package:http 502 read identically. The route still travels on the
request interface.

Co-Authored-By: Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
SentryRequest sanitizes its headers, SentryResponse never did, so a
response's Set-Cookie, Authorization or X-API-Key was sent verbatim
whenever sendDefaultPii was on. Route both HTTP client paths through
HttpSanitizer.

SentryResponse derives its cookies field from the headers map, so the
cookie is now read before sanitizing to keep that field populated, which
matches how sentry-java's OkHttp interceptor treats it.

Co-Authored-By: Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The interceptor required a status-code match, and a DioException without
a response has none, so timeouts, DNS errors and TLS failures produced no
event at all. FailedRequestClient captures the equivalent failures on the
package:http path.

A missing status code now means there is nothing to filter on rather than
an automatic reject. Cancellations stay excluded, being a deliberate user
action, and failedRequestTargets is still honoured.

Co-Authored-By: Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
DioEventProcessor rewrote the exception value only for bad responses,
leaving timeouts and connection errors with Dio's own text. Those
messages restate the timeout the developer configured and append
generic remediation advice, and the embedded duration gave every
timeout setting its own issue title.

sentry-javascript never surfaces the underlying error's message
either: httpClientIntegration uses the error only to recover a stack
trace and always sets the value to its synthetic string. A timeout now
reads "HTTP Client Error: connectionTimeout".

A user-supplied stringBuilder still wins.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Two gaps against the HTTP client spec and sentry-javascript, closed
across the package:http, dio and supabase integrations.

Requests to the DSN were captured like any other, so a failing
transport could try to report the failure over the connection that
just failed. sentry-javascript guards this with isSentryRequestUrl
inside _shouldCaptureResponse; the helper added here matches the DSN
host the same way.

The mechanism also left `handled` unset. It is now true: the SDK
caught the failure and reported it rather than letting it reach the
app. sentry-javascript sets false, which is deliberately not copied.
SentryEnvelope.fromEvent turns handled == false into
containsUnhandledException, which tells the native SDKs to end the
session as crashed, and captureFailedRequests defaults to true here
while httpClientIntegration is opt-in there. Copying that value would
report a crashed session for every captured 5xx.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Three buttons covering the paths this branch touches: a 404 bad
response, a connection timeout and a DNS failure. The latter two were
dropped entirely before the interceptor learned to capture failures
that carry no status code.

Each swallows the DioException so the interceptor's event is the only
one sent, rather than a second one from an implicit capture.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Semver Impact of This PR

🟢 Patch (bug fixes)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


Features

Flutter

  • Extend standalone app start by buenaflor in #3918
  • Add standalone app start tracing by buenaflor in #3896
  • Remove CocoaPods support and update Sentry Cocoa to 9.21.0 by buenaflor in #3879
  • Make native request capture opt-in by buenaflor in #3885
  • Support SwiftPM on Flutter 3.44+ by buenaflor in #3784

Logs

  • Add Hint to beforeSend for logs, metrics, and spans by buenaflor in #3847
  • Enable logs by default by buenaflor in #3846

Other

  • (profiling) Remove SDK profiling by buenaflor in #3891
  • (telemetry) Align span attributes with Sentry Conventions by buenaflor in #3805

Fixes

Http

  • Align HTTP client error type and value by buenaflor in #3934
  • Align HTTP client error type and value by buenaflor in #3934

Other

  • Read normalized rate limit headers by sentry-junior in #3883

Enhancements

  • (flutter) Speed up Android scope sync and replay capture by buenaflor in #3924

Dependencies

Deps

  • chore(deps): update Native SDK to v0.16.0 by github-actions in #3925
  • chore(deps): update Android SDK to v8.50.1 by github-actions in #3921

Flutter

  • Increase jni range and set jnigen to 0.17.0 by buenaflor in #3931
  • Bump jni to 1.0.0 and jnigen to 0.16.0 in v10 by buenaflor in #3765
  • Bump sentry-cocoa to 9.17.1 in v10 by buenaflor in #3764

Internal Changes

Dart

  • Remove deprecated copyWith methods by buenaflor in #3877
  • Breaking: Feature flags are now hub/scope-based and FeatureFlagsIntegration has been removed. Sentry.addFeatureFlag() is unchanged, but events no longer report FeatureFlagsIntegration under sdk.integrations; feature-flag usage is tracked via featureFlags in sdk.features. by buenaflor in #3848
  • Part of Major v10 Task Collection #3487. Fixes [v10] Make FeatureFlagsIntegration hub-based #3837. by buenaflor in #3848

Logs

  • Make traceId required on SentryLog by buenaflor in #3842
  • Make logger methods return void by buenaflor in #3852
  • Remove deprecated SentryLogAttribute by buenaflor in #3843

Other

  • (deps) Pin Flutter development dependencies by buenaflor in #3913
  • (feedback) Remove deprecated SentryFeedbackWidget by buenaflor in #3844
  • (file) Remove file.async attributes from instrumentation by buenaflor in #3841
  • (flutter) Share Darwin plugin sources by buenaflor in #3922
  • (grpc) Move MockHub to _sentry_testing package by lucas-zimerman in #3908
  • Breaking: Removed SentryOptions.log diagnostic callback. Use options.debug and options.diagnosticLevel to control SDK diagnostic output via SentryInternalLogger. by buenaflor in #3932
  • Make clone() internal and remove deprecated protocol clones by buenaflor in #3845
  • Removed the deprecated PerformanceCollector / PerformanceContinuousCollector API and SentryOptions.addPerformanceCollector() / performanceCollectors. These are superseded by the internal trace lifecycle hooks; there is no public replacement. by buenaflor in #3850

🤖 This preview updates automatically when you update the PR.

@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (v10-branch@df583cd). Learn more about missing BASE report.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@              Coverage Diff              @@
##             v10-branch    #3934   +/-   ##
=============================================
  Coverage              ?   88.30%           
=============================================
  Files                 ?      346           
  Lines                 ?    12370           
  Branches              ?        0           
=============================================
  Hits                  ?    10923           
  Misses                ?     1447           
  Partials              ?        0           
Flag Coverage Δ
sentry 88.26% <100.00%> (?)
sentry_dio 97.95% <100.00%> (?)
sentry_drift 93.57% <ø> (?)
sentry_file 64.45% <ø> (?)
sentry_firebase_remote_config 100.00% <ø> (?)
sentry_flutter 92.17% <ø> (?)
sentry_grpc 99.09% <ø> (?)
sentry_hive 77.48% <ø> (?)
sentry_isar 74.37% <ø> (?)
sentry_link 21.50% <ø> (?)
sentry_logging 97.01% <ø> (?)
sentry_sqflite 88.81% <ø> (?)
sentry_supabase 97.47% <100.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@buenaflor
buenaflor marked this pull request as ready for review July 30, 2026 09:48
@buenaflor
buenaflor requested a review from denrase as a code owner July 30, 2026 09:48
Copilot AI review requested due to automatic review settings July 30, 2026 09:48

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment thread packages/dart/lib/src/utils/tracing_utils.dart
buenaflor and others added 2 commits July 30, 2026 12:00
A failure with no status code took its value straight from the enum
name, so issue titles read "HTTP Client Error: connectionTimeout" --
a leaked identifier. They now read "connection timeout", "connection
error", "bad certificate" and so on.

Only the types whose enum name reads badly are mapped; a type Dio adds
in a future minor release falls back to the name rather than breaking
the build, which a switch would.

The detail behind a connection error is not lost by keeping the title
generic: DioErrorExtractor already chains DioException.error as a
cause, so a DNS failure still carries its
"SocketException: Failed host lookup" on the event.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The DSN skip tested whether the URL contained the DSN host, so a
self-hosted DSN on sentry.example.com also suppressed capture for
not-sentry.example.com. Failures on such a host went unreported with
nothing to indicate why.

Parse the URL and compare hosts instead. sentry-javascript's
isSentryRequestUrl has the same substring flaw; the cost of being
stricter is a Uri.tryParse per captured failure, and a relative URL
cannot address the DSN so it yields an empty host and is kept.

Reported by Bugbot on #3934.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 8cd78be. Configure here.

Comment thread packages/dio/lib/src/dio_event_processor.dart Outdated
Putting the response on contexts.response also started shipping the
response body, which had previously been built but only ever reached
the beforeSend hint, and hints are never serialized. With
sendDefaultPii on, every failed request would carry up to 150 KB of
response body -- more than the option asks for, and more than
FailedRequestClient or the other SDKs send.

The event now gets status code, headers, cookies and body size. The
body stays on the hint, so beforeSend can still read it.

Reported by Bugbot on #3934.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread packages/dart/lib/src/http_client/failed_request_client.dart
A request that threw reported mechanism.description as "HTTP Client
Error with status code: null". The reason string was built before
knowing whether it would be used: a thrown exception is kept as-is, so
only the synthesized SentryHttpClientError ever needs the description.

The exception value was never affected -- ??= short-circuits, so the
thrown ClientException and its own message stay in the title.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
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