cloudflare / cloudflare/partykit
Bug: Bun C++ Exception Crash with PartyWebSocket (partysocket library)
- Dominant language
- TypeScript
- Stars
- 1.3k
- Forks
- 79
- PR merge metrics
- No merged PRs in 30d
Description
## Summary
Bun v1.3.5 crashes with a "pure virtual method called" C++ exception after running for 3-4 minutes when using `PartyWebSocket` from the `partysocket` library. The crash occurs during JavaScriptCore's parallel garbage collection when marking WebCore/DOM objects.
## Environment
- **Bun version**: v1.3.5 (1e86cebd)
- **OS**: Linux x86_64 (baseline)
- **Kernel**: v6.18.0 | glibc v2.42
- **Library**: `partysocket@1.1.10` (via catalog)
- **Crash time**: Consistently after 3-4 minutes of runtime
## Stack Trace
```
Bun v1.3.5 (1e86cebd) on linux x86_64_baseline [AutoCommand]
**panic**: A C++ exception occurred
- JSGlobalObject.zig:854: `Zig__GlobalObject__onCrash`
- compiler_builtins.6c1f39da0e88e811-cgu.247:0: `__cxxabiv1::__terminate`
- ClassInfo.h:124: `WTF::Ref, WTF::RawPtrTraits >, WTF::DefaultRefDerefTraits > > JSC::Subspace::forEachMarkedCellInParallel(...)::'lambda'(...)::operator(...) const::'lambda'(...)>(...)::'lambda'(...)::operator(...) const::'lambda'(...) const&)::Task::run`
- MarkingConstraint.cpp:96: `JSC::MarkingConstraintSolver::runExecutionThread`
- SlotVisitor.cpp:686: `JSC::SlotVisitor::drainFromShared`
- Heap.cpp:1502: `WTF::SharedTaskFunctor::run`
- ParallelHelperPool.cpp:110: `WTF::ParallelHelperPool::Thread::work`
- AutomaticThread.cpp:225: `WTF::Detail::CallableWrapper::call`
- Function.h:82: `WTF::wtfThreadEntryPoint`
```
## Root Cause
The crash occurs during JavaScriptCore's **parallel garbage collection** when marking `WebCore::DOMGCOutputConstraint` objects. The `PartyWebSocket` class from the `partysocket` library creates WebCore/DOM objects that are destroyed while parallel GC threads are trying to mark them, causing the "pure virtual method called" error.
**Key observation**: The stack trace shows `WebCore::DOMGCOutputConstraint`, indicating this is related to DOM/WebCore objects, not just WebSocket connections. The `partysocket` library likely wraps or extends native WebSocket functionality in a way that creates additional WebCore objects that become invalid during GC.
## Problematic Code
### Before (Causing Crash)
```typescript
// backend/apps/support/src/lib/chat-api-client.ts
import { WebSocket as PartyWebSocket } from 'partysocket'
// Create WebSocket connection using partysocket (with auto-reconnect)
let sharedWebSocket: PartyWebSocket | null = null
let sharedWsClient: ContractRouterClient | null = null
function getSharedWebSocketClient(): ContractRouterClient {
// Return existing client if WebSocket is open
if (
sharedWsClient &&
sharedWebSocket &&
sharedWebSocket.readyState === PartyWebSocket.OPEN
) {
return sharedWsClient
}
// Create new WebSocket connection with partysocket
// partysocket provides automatic reconnection and message buffering
sharedWebSocket = new PartyWebSocket(getWebSocketUrl()) // ❌ CRASHES HERE
// Create oRPC client from WebSocket
const link = new WSLink({
websocket: sharedWebSocket as any,
})
sharedWsClient = createORPCClient(link) as ContractRouterClient<
typeof chatContract
>
sharedWebSocket.addEventListener('close', () => {
sharedWsClient = null
})
return sharedWsClient
}
// Created at module load time - WebSocket created immediately
export const chatApiClient: ContractRouterClient =
getSharedWebSocketClient() // ❌ Creates PartyWebSocket immediately
```
### After (Fixed)
```typescript
// backend/apps/support/src/lib/chat-api-client.ts
// REMOVED: import { WebSocket as PartyWebSocket } from 'partysocket'
// Using native WebSocket instead
// Create WebSocket connection using native WebSocket
let sharedWebSocket: WebSocket | null = null
let sharedWsClient: ContractRouterClient | null = null
function getSharedWebSocketClient(): ContractRouterClient {
// Return existing client if WebSocket is open
if (
sharedWsClient &&
sharedWebSocket &&
sharedWebSocket.readyState === WebSocket.OPEN // ✅ Native WebSocket
) {
return sharedWsClient
}
// Create new WebSocket connection with native WebSocket
// Using native WebSocket instead of PartyWebSocket to avoid GC crashes
sharedWebSocket = new WebSocket(getWebSocketUrl()) // ✅ Native WebSocket
// Create oRPC client from WebSocket
const link = new WSLink({
websocket: sharedWebSocket as any,
})
sharedWsClient = createORPCClient(link) as ContractRouterClient<
typeof chatContract
>
sharedWebSocket.addEventListener('close', () => {
sharedWsClient = null
})
return sharedWsClient
}
// Lazy-load chatApiClient to avoid creating WebSocket at module load time
let _chatApiClient: ContractRouterClient | null = null
export const chatApiClient: ContractRouterClient =
new Proxy({} as ContractRouterClient, {
get(_target, prop) {
if (!_chatApiClient) {
_chatApiClient = getSharedWebSocketClient() // ✅ Lazy-loaded
}
return (_chatApiClient as any)[prop]
},
})
```
## Workaround
Replace `PartyWebSocket` from `partysocket` with native `WebSocket`:
1. **Remove partysocket import**:
```typescript
// REMOVED: import { WebSocket as PartyWebSocket } from 'partysocket'
```
2. **Use native WebSocket**:
```typescript
const websocket = new WebSocket(wsUrl) // Instead of new PartyWebSocket(wsUrl)
```
3. **Lazy-load WebSocket connections** to avoid creating them at module load time:
```typescript
// Use Proxy to lazy-load instead of creating at module load
export const chatApiClient = new Proxy({}, {
get(_target, prop) {
if (!_chatApiClient) {
_chatApiClient = getSharedWebSocketClient()
}
return (_chatApiClient as any)[prop]
},
})
```
## Related Issues
- Similar crash reported with worker threads: https://github.com/oven-sh/bun/issues/14559
- Both involve "pure virtual method called" during parallel GC marking
- Both occur after several minutes of runtime
## Additional Context
The crash is **not** related to:
- Native `WebSocket` usage (works fine)
- Async iterators with native WebSocket
- EventPublisher or mergeIterators
- AbortSignal usage
The crash **is** specifically related to:
- `PartyWebSocket` from `partysocket` library
- Objects created by `partysocket` that reference WebCore/DOM
- These objects being destroyed during parallel GC marking
## Expected Behavior
The application should run indefinitely without crashing. Native `WebSocket` works correctly, but `PartyWebSocket` causes crashes after 3-4 minutes.
## Actual Behavior
Application crashes with C++ exception after 3-4 minutes when using `PartyWebSocket`. Replacing with native `WebSocket` resolves the issue.
## Reproduction
1. Create a WebSocket connection using `PartyWebSocket` from `partysocket`
2. Keep the connection alive for 3-4 minutes
3. Wait for JavaScriptCore's parallel GC to run
4. Crash occurs when GC tries to mark WebCore objects created by `partysocket`
## Impact
- **Severity**: High - causes application crashes
- **Frequency**: 100% reproducible after 3-4 minutes
- **Workaround**: Use native `WebSocket` instead of `PartyWebSocket`
Contributor guide
Assessment
This issue has not been assessed yet.