Skip to content

Instantly share code, notes, and snippets.

@jbinto
Last active November 21, 2025 15:58
Show Gist options
  • Select an option

  • Save jbinto/990d29834b1f6aeb43c69de2ac890fe0 to your computer and use it in GitHub Desktop.

Select an option

Save jbinto/990d29834b1f6aeb43c69de2ac890fe0 to your computer and use it in GitHub Desktop.
msw bad socket/EINVAL sequence diagrams

1. Before Fix: The "Happy" Path (Redundant but Safe)

Why it usually works: In a normal request, both the Real Socket and the Mock Socket are alive. The Mock Socket tries to "drive" (call readStart), but the Real Socket is already driving. Libuv handles are generally resilient to being told to "start reading" when they are already reading. It's messy, but not fatal.

sequenceDiagram
    autonumber
    participant Stream as Node Stream Logic
    participant Mock as MockHttpSocket
    participant Real as Real TLSSocket
    participant Handle as Libuv C++ Handle

    Note over Mock, Real: Setup: Mock._handle = Real._handle (Aliased)

    Note over Real, Handle: Real Socket is alive and managing the handle
    Real->>Handle: readStart() (Real drives the bus)
    
    loop Data Flow
        Handle-->>Real: Data incoming...
        Real-->>Mock: push(data)
        Stream->>Mock: _read(size) (Stream wants more?)
        
        rect rgb(240, 255, 240)
        Note right of Mock: The "Happy" Race
        Mock->>Mock: Has _handle? YES.
        Mock->>Handle: readStart()
        Note left of Handle: Handle is ALREADY reading.<br/>Libuv ignores this redundancy.<br/>Status: OK.
        end
    end

    Note over Stream, Handle: Connection ends gracefully for everyone.
Loading

2. Before Fix: The Bug

Why dd-trace triggers the bug: The Real Socket is destroyed (closing the underlying C++ handle) while the MockHttpSocket is still active, creating a state where the mock attempts to read from a handle that has already been freed. It is unclear how we get in this state, but it is possible to write a test that reproduces the state exactly.

sequenceDiagram
    autonumber
    participant Stream as Node Stream Logic
    participant Agent as HTTP Agent / dd-trace
    participant Mock as MockHttpSocket
    participant Real as Real TLSSocket
    participant Handle as Libuv C++ Handle

    Note over Mock, Real: Setup: Mock._handle = Real._handle (Aliased)

    Note right of Agent: Agent decides to recycle/kill connection
    Agent->>Real: destroy()
    Real->>Handle: close()
    Note right of Handle: Handle is now CLOSED (Dead)

    Note over Mock: Mock is still "alive" in JS land<br/>and thinks it needs data.
    
    Stream->>Mock: _read(size)
    
    rect rgb(255, 240, 240)
    Note right of Mock: The Fatal Race
    Mock->>Mock: Has _handle? YES. (The alias persists)
    Mock->>Handle: readStart()
    
    Note left of Handle: Handle is CLOSED.<br/>Operation invalid on closed handle.
    Handle-->>Mock: Error: EINVAL (Crash)
    end
Loading

3. After Fix: Correct Behavior

Why it is safe: We remove the alias. MockHttpSocket no longer has access to the C++ handle. We also override _read so that when Node asks "Get more data," the Mock simply does nothing. It accepts its role as a Passenger, waiting for the Real Socket (or its events) to push data or signal closure.

sequenceDiagram
    autonumber
    participant Stream as Node Stream Logic
    participant Agent as HTTP Agent / dd-trace
    participant Mock as MockHttpSocket
    participant Real as Real TLSSocket
    participant Handle as Libuv C++ Handle

    Note over Mock, Real: Setup: Mock._handle is UNDEFINED
    
    Note right of Agent: Agent decides to kill connection
    Agent->>Real: destroy()
    Real->>Handle: close()
    Note right of Handle: Handle is now CLOSED (Dead)

    Note over Mock: Mock is still "alive" in JS land.
    
    Stream->>Mock: _read(size)
    
    rect rgb(240, 240, 255)
    Note right of Mock: The Correct Logic
    Mock->>Mock: Has _handle? NO.
    Mock->>Mock: Is Passthrough? YES.
    Mock->>Mock: Return / No-Op.
    Note right of Mock: Mock does not touch the handle.<br/>It waits for events from Real.
    end

    Real->>Mock: emit('close')
    Note over Mock: Mock cleans itself up gracefully.
Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment