fix multipart upload crash ClientException Bad file descriptor#7507
fix multipart upload crash ClientException Bad file descriptor#7507krushnarout wants to merge 1 commit into
Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Greptile SummaryThis PR fixes a Crashlytics-reported crash during multipart uploads in
Confidence Score: 4/5The fix is a net improvement that eliminates the reported crash; the one remaining edge case is a theoretical secondary zone error in a rare ordering of timeout + EBADF, which is easy to harden with a try/catch. Both changes target real, well-characterised failure modes. The subscription lifecycle fix correctly cancels the orphaned listener on all three exit paths (normal, timeout, stream error). The one concern is the Only Important Files Changed
Sequence DiagramsequenceDiagram
participant Caller as makeMultipartApiCall
participant Send as _sendMultipartWithProgress
participant Sub as progressStream.listen
participant Sink as streamedRequest.sink
participant Pool as HttpPoolManager
Caller->>Send: call with onProgress
Send->>Sub: subscribe (cancelOnError: true)
Send->>Pool: sendStreaming(streamedRequest)
Pool-->>Send: future
note over Send: Stores subscription<br/>Registers whenComplete(cancel)
alt Normal completion
Sub->>Sink: sink.add(data) [per chunk]
Sub->>Sink: onDone → sink.close()
Pool-->>Caller: StreamedResponse
Send->>Sub: whenComplete → subscription.cancel()
else Timeout / HTTP error
Pool-->>Caller: throws TimeoutException
Send->>Sub: whenComplete → subscription.cancel()
note over Sub: subscription stopped,<br/>no more data pumped
else Stream error (e.g. EBADF)
Sub->>Sink: onError → sink.addError + sink.close()
note over Sub: cancelOnError cancels subscription
Pool-->>Caller: throws error
end
Caller->>Caller: catch → _isTransientNetworkError check
note over Caller: "Bad file descriptor" now<br/>classified as transient → no Crashlytics report
Reviews (1): Last reviewed commit: "fix multipart upload crash ClientExcepti..." | Re-trigger Greptile |
| onError: (Object e, StackTrace st) { | ||
| streamedRequest.sink.addError(e, st); | ||
| streamedRequest.sink.close(); | ||
| }, |
There was a problem hiding this comment.
Guard the sink operations with a try/catch so that a
StateError ("Cannot add event after closing") thrown when the HTTP consumer has already finished does not escape as an unhandled zone error. When the HTTP future times out and completes before the source stream emits EBADF, the HTTP consumer has already disconnected from streamedRequest.sink. If the file stream subsequently raises an error, onError calls sink.addError(e, st) on a sink whose underlying StreamController may have no listeners — the Dart runtime would then re-raise the error as an unhandled zone error, the same class of crash the PR is fixing.
| onError: (Object e, StackTrace st) { | |
| streamedRequest.sink.addError(e, st); | |
| streamedRequest.sink.close(); | |
| }, | |
| onError: (Object e, StackTrace st) { | |
| try { | |
| streamedRequest.sink.addError(e, st); | |
| streamedRequest.sink.close(); | |
| } catch (_) { | |
| // Sink already closed by the HTTP layer; ignore. | |
| } | |
| }, |
Crash
(null).makeMultipartApiCallError:
FlutterError - ClientException: Bad file descriptor, uri=https://api.omi.me/v1/sync-local-filesCrashlytics: https://console.firebase.google.com/u/0/project/based-hardware/crashlytics/app/ios:com.friend-app-with-wearable.ios12/issues/780a8bb89b21adf52eabdac3034cd749?time=7d&types=crash&sessionEventKey=6453a786b0ce4ba0a4c6c96707e1aae3_2222621226552694960
Logs:
Fix
Two changes in
shared.dart:Added
Bad file descriptorto_isTransientNetworkError— when a multipart upload times out, the HTTP layer closes the socket and subsequent reads on the in-flight file stream raiseEBADFwrapped in aClientException. This is an OS-level transient error, not a code bug, so it was being incorrectly reported to Crashlytics. Adding it to the transient list stops the false crash report; the error still rethrows and is caught gracefully by the WAL sync retry loop.Fixed
_sendMultipartWithProgressstream subscription lifecycle — the progress-stream subscription was fire-and-forget with no error handler that closed the sink. On timeout or error, the orphaned subscription would continue pumping data into a closed sink, causing unhandled zone errors. Now the subscription is stored,cancelOnError: truestops it on first error, theonErrorhandler closes the sink immediately, andfuture.whenComplete(subscription.cancel)cleans it up when the send finishes.🤖 Generated with Claude Code