You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
On Linux, chatsStream(query:) crashes with Signal 6 (SIGABRT) when the returned AsyncThrowingStream is cancelled mid-flight — i.e., before the SSE response has finished streaming — while concurrent URLSession requests are in flight. The crash is:
Object 0x7f33b00de390 of class _MultiHandle deallocated with non-zero retain count 2.
This object's deinit, or something called from it, may have created a strong
reference to self which outlived deinit, resulting in a dangling reference.
*** Signal 6: Program crashed: Aborted at 0x000003e80002a5f0
The crash is in libcurl's _MultiHandle deallocation, reached via CFRunLoopRunSpecific in the stack.
Environment
MacPaw/OpenAI: 0.5.0 (latest release; main has no changes to StreamingClient.swift or the delegate path beyond 0.5.0)
StreamingClient consumes the SSE response via a delegate-based URLSessionDataTask that holds libcurl's _MultiHandle across runloop iterations (CFRunLoopRunSpecific appears in the crash stack). When the AsyncThrowingStream<ChatStreamResult, Error> returned by chatsStream(query:) is cancelled mid-flight while other URLSession requests are in flight, the _MultiHandle is deallocated with a non-zero retain count, aborting the process.
The crash requires both elements:
Concurrent URLSession activity — a second URLSession (e.g. URLSession(configuration:)) issuing requests while the SSE stream is open. Without this, the crash does not reproduce (the cancellation path completes cleanly).
Mid-flight cancellation of the chatsStreamAsyncThrowingStream — letting the stream complete naturally does not crash, even with concurrent URLSession activity.
Reproduction
A minimal repro requires an OpenAI-compatible streaming endpoint, a concurrent URLSession request to a live HTTP server, and cancellation of the stream before it completes:
letopenai=OpenAI(configuration:Configuration(token:"...", host:"..."))letquery=ChatQuery(
messages:[.init(role:.user, content:"List numbers 1 through 50, one per line.")],
model:"your-model",
stream:true)
// Start the SSE stream on URLSession.shared (the default).
lettask=Task{letstream= openai.chatsStream(query: query)fortryawaitchunkin stream { /* drain */ }}
// Give the stream a moment to establish its connection.
tryawaitTask.sleep(nanoseconds:500_000_000)
// Fire concurrent URLSession(configuration:) requests to a live HTTP
// server while the SSE stream is open. Any localhost HTTP server
// works (here we assume one is running on port 8080).
awaitwithTaskGroup(of:Void.self){ group infor_in0..<10{
group.addTask{letsession=URLSession(configuration:URLSessionConfiguration.default)
_ =try?await session.data(from:URL(string:"http://127.0.0.1:8080/")!)}}}
// Cancel the SSE stream mid-flight while the concurrent requests
// are settling. This triggers the _MultiHandle crash.
task.cancel()await task.value // crashes with _MultiHandle deallocated, Signal 6 (~3/5 runs)
The crash reproduces ~3/5 runs on debug builds. Release builds narrow the crash window but do not eliminate it.
Note: A plain chatsStream + cancel without concurrent URLSession activity does not crash — the concurrent activity is load-bearing (it widens the timing window for the _MultiHandle deallocation race).
Stack trace (abbreviated)
Object 0x7f33b00de390 of class _MultiHandle deallocated with non-zero retain count 2.
*** Signal 6: Backtracing from 0x7f33bfa8d02c... done ***
*** Program crashed: Aborted at 0x000003e80002a5f0 ***
0 0x00007f33bfa3ff1a sigsuspend + 74 in libc.so.6
1 [ra] 0x00007f33c1d4abc9 _dispatch_sig_thread + 8 in libdispatch.so
...
0 0x00007f33bfb1017e epoll_wait + 94 in libc.so.6
1 [ra] 0x00007f33c1d49f92 _dispatch_mgr_invoke + 129 in libdispatch.so
2 [ra] 0x00007f33c1d49efd _dispatch_mgr_thread + 108 in libdispatch.so
3 [ra] 0x00007f33c1d4d105 _dispatch_worker_thread + 628 in libdispatch.so
The CFRunLoopRunSpecific frame (from libFoundation.so) appears in the full trace — this is the libcurl runloop integration in FoundationNetworking's URLSession that the delegate-based URLSessionDataTask schedules on.
Workaround
We bypass the StreamingClient delegate path on Linux by wrapping OpenAI in a custom OpenAIAsync conformer that overrides chatsStream(query:) to drain the SSE response via URLSession.shared.bytes(for:) (a raw byte drain with no delegate, no StreamingClient). The raw byte-drain path cancels cleanly — no _MultiHandle corruption. Every other OpenAIAsync method delegates to the wrapped OpenAI unchanged. This is Linux-only (behind #if canImport(FoundationNetworking)); on Apple platforms URLSession is native and the delegate path works correctly.
Our red-capable feedback loop confirmed the diagnosis:
Summary
On Linux,
chatsStream(query:)crashes with Signal 6 (SIGABRT) when the returnedAsyncThrowingStreamis cancelled mid-flight — i.e., before the SSE response has finished streaming — while concurrentURLSessionrequests are in flight. The crash is:The crash is in libcurl's
_MultiHandledeallocation, reached viaCFRunLoopRunSpecificin the stack.Environment
mainhas no changes toStreamingClient.swiftor the delegate path beyond 0.5.0)URLSession)Root cause
StreamingClientconsumes the SSE response via a delegate-basedURLSessionDataTaskthat holds libcurl's_MultiHandleacross runloop iterations (CFRunLoopRunSpecificappears in the crash stack). When theAsyncThrowingStream<ChatStreamResult, Error>returned bychatsStream(query:)is cancelled mid-flight while otherURLSessionrequests are in flight, the_MultiHandleis deallocated with a non-zero retain count, aborting the process.The crash requires both elements:
URLSessionactivity — a secondURLSession(e.g.URLSession(configuration:)) issuing requests while the SSE stream is open. Without this, the crash does not reproduce (the cancellation path completes cleanly).chatsStreamAsyncThrowingStream— letting the stream complete naturally does not crash, even with concurrentURLSessionactivity.Reproduction
A minimal repro requires an OpenAI-compatible streaming endpoint, a concurrent
URLSessionrequest to a live HTTP server, and cancellation of the stream before it completes:The crash reproduces ~3/5 runs on debug builds. Release builds narrow the crash window but do not eliminate it.
Note: A plain
chatsStream+ cancel without concurrentURLSessionactivity does not crash — the concurrent activity is load-bearing (it widens the timing window for the_MultiHandledeallocation race).Stack trace (abbreviated)
The
CFRunLoopRunSpecificframe (fromlibFoundation.so) appears in the full trace — this is the libcurl runloop integration inFoundationNetworking'sURLSessionthat the delegate-basedURLSessionDataTaskschedules on.Workaround
We bypass the
StreamingClientdelegate path on Linux by wrappingOpenAIin a customOpenAIAsyncconformer that overrideschatsStream(query:)to drain the SSE response viaURLSession.shared.bytes(for:)(a raw byte drain with no delegate, noStreamingClient). The raw byte-drain path cancels cleanly — no_MultiHandlecorruption. Every otherOpenAIAsyncmethod delegates to the wrappedOpenAIunchanged. This is Linux-only (behind#if canImport(FoundationNetworking)); on Apple platformsURLSessionis native and the delegate path works correctly.Our red-capable feedback loop confirmed the diagnosis:
URLSession(configuration:)+ mid-flight cancel → crashes ~3/5 runs.URLSession.shared.bytes(for:)drain + same concurrentURLSession(configuration:)+ same mid-flight cancel → 5/5 green.Why filing this
main(StreamingClient.swift, the delegate-basedURLSessionDataTaskpath).chatsStreambut was about the API surface, not this crash._MultiHandlecrash or Linux libcurl cancellation.