cloudflare / cloudflare/partykit

Bug: Bun C++ Exception Crash with PartyWebSocket (partysocket library)

Open
#304 1 comment 0 reactions 0 assignees View on GitHub
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.