@@ -311,10 +311,13 @@ closeLocalEndpoint quicTransport localEndPoint = do
311311 LocalEndPointStateClosed -> pure (LocalEndPointStateClosed , Nothing )
312312 LocalEndPointStateValid st -> pure (LocalEndPointStateClosed , Just st)
313313
314- forM_ mPreviousState $ \ vst ->
315- forConcurrently_
316- (vst ^. incomingConnections <> vst ^. outgoingConnections)
317- tryCloseRemoteStream
314+ -- Close outgoing remote endpoints before incoming. The peer's handleIncomingMessages
315+ -- reader writes ConnectionClosed in response to our outgoing close; its listenForClose
316+ -- writes ErrorEvent in response to our incoming close. Processing outgoing first gives
317+ -- the peer's event queue the expected ConnectionClosed-before-ErrorEvent ordering.
318+ forM_ mPreviousState $ \ vst -> do
319+ forConcurrently_ (vst ^. outgoingConnections) tryCloseRemoteStream
320+ forConcurrently_ (vst ^. incomingConnections) tryCloseRemoteStream
318321 atomically $ writeTQueue (localEndPoint ^. localQueue) EndPointClosed
319322 where
320323 tryCloseRemoteStream :: RemoteEndPoint -> IO ()
@@ -409,7 +412,7 @@ createConnectionTo creds validateCreds localEndPoint remoteAddress = do
409412 validateCreds
410413 (localEndPoint ^. localAddress)
411414 remoteAddress
412- (\ _ -> closeRemoteEndPoint Outgoing remoteEndPoint)
415+ (onException remoteEndPoint)
413416 onConnectionLost
414417 >>= \ case
415418 Left exc -> pure $ Left exc
@@ -438,6 +441,25 @@ createConnectionTo creds validateCreds localEndPoint remoteAddress = do
438441 Left (exc :: SomeException ) -> pure . Left $ TransportError ConnectFailed (displayException exc)
439442 Right () -> pure $ Right (remoteEndPoint, clientConnId)
440443 where
444+ onException remoteEndPoint _exception = do
445+ -- The handler runs when QUIC.Client.run exits with an exception — i.e. the
446+ -- peer tore down the connection before we did. If the remote endpoint was
447+ -- still Valid when this fires, our side had not initiated the close, so we
448+ -- surface an EventConnectionLost. (If state is already Closed, we initiated
449+ -- the close ourselves and the QUIC exception is just teardown noise.)
450+ do
451+ mAct <- modifyMVar (remoteEndPoint ^. remoteEndPointState) $ \ case
452+ RemoteEndPointInit -> pure (RemoteEndPointClosed , Nothing )
453+ RemoteEndPointClosed -> pure (RemoteEndPointClosed , Nothing )
454+ RemoteEndPointValid (ValidRemoteEndPointState stream isClosed conns _) ->
455+ let cleanup = do
456+ forM_ conns $ \ c -> sendCloseConnection c stream
457+ putMVar isClosed ()
458+ onConnectionLost
459+ in pure (RemoteEndPointClosed , Just cleanup)
460+ case mAct of
461+ Nothing -> pure ()
462+ Just act -> act
441463 onConnectionLost =
442464 atomically
443465 . writeTQueue (localEndPoint ^. localQueue)
0 commit comments