Repository: vapor/websocket-kit
Pull request #158: Deal with Sendable and clean up package in general
Author: login: gwynne
Labels: name: semver-minor
description: Contains new APIs
Created: 2025-04-26T06:38:44Z | Merged: 2025-04-27T03:00:49Z

**These changes are now available in [2.16.0](https://github.com/vapor/websocket-kit/releases/tag/2.16.0)**


Covers fixing a bunch of `Sendable` things and does the usual round of package cleanup - bump the Swift minimum to 5.10, update the CI and the README, and so forth. Also fixes up some issues in the tests.


Diff:
diff --git a/.api-breakage/allowlist-branch-sendable-take-2.txt b/.api-breakage/allowlist-branch-sendable-take-2.txt
deleted file mode 100644
index a2985625..00000000
--- a/.api-breakage/allowlist-branch-sendable-take-2.txt
+++ /dev/null
@@ -1,14 +0,0 @@
-API breakage: func WebSocket.onText(_:) is now with @preconcurrency
-API breakage: func WebSocket.onBinary(_:) is now with @preconcurrency
-API breakage: func WebSocket.onPong(_:) is now with @preconcurrency
-API breakage: func WebSocket.onPing(_:) is now with @preconcurrency
-API breakage: func WebSocket.connect(to:headers:configuration:on:onUpgrade:) is now with @preconcurrency
-API breakage: func WebSocket.connect(scheme:host:port:path:query:headers:configuration:on:onUpgrade:) is now with @preconcurrency
-API breakage: func WebSocket.connect(scheme:host:port:path:query:headers:proxy:proxyPort:proxyHeaders:proxyConnectDeadline:configuration:on:onUpgrade:) is now with @preconcurrency
-API breakage: func WebSocket.connect(to:headers:proxy:proxyPort:proxyHeaders:proxyConnectDeadline:configuration:on:onUpgrade:) is now with @preconcurrency
-API breakage: func WebSocket.client(on:onUpgrade:) is now with @preconcurrency
-API breakage: func WebSocket.client(on:config:onUpgrade:) is now with @preconcurrency
-API breakage: func WebSocket.server(on:onUpgrade:) is now with @preconcurrency
-API breakage: func WebSocket.server(on:config:onUpgrade:) is now with @preconcurrency
-API breakage: func WebSocketClient.connect(scheme:host:port:path:query:headers:onUpgrade:) is now with @preconcurrency
-API breakage: func WebSocketClient.connect(scheme:host:port:path:query:headers:proxy:proxyPort:proxyHeaders:proxyConnectDeadline:onUpgrade:) is now with @preconcurrency
diff --git a/.api-breakage/allowlist-branch-update-for-new-niossl.txt b/.api-breakage/allowlist-branch-update-for-new-niossl.txt
deleted file mode 100644
index a32a2389..00000000
--- a/.api-breakage/allowlist-branch-update-for-new-niossl.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-API breakage: import NIO has been renamed to import NIOCore
-API breakage: import NIO has been renamed to import NIOPosix
-
diff --git a/.github/workflows/api-docs.yml b/.github/workflows/api-docs.yml
index 60d54030..460458fd 100644
--- a/.github/workflows/api-docs.yml
+++ b/.github/workflows/api-docs.yml
@@ -11,4 +11,4 @@ jobs:
      with:
        package_name: websocket-kit
        modules: WebSocketKit
-       pathsToInvalidate: /websocket-kit
+       pathsToInvalidate: /websocketkit/*
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 586f6846..0bdb7b48 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -5,11 +5,12 @@ concurrency:
 on:
   pull_request: { types: [opened, reopened, synchronize, ready_for_review] }
   push: { branches: [ main ] }
+
 jobs:
   vapor-integration:
     if: ${{ !(github.event.pull_request.draft || false) }}
     runs-on: ubuntu-latest
-    container: swift:5.10-noble
+    container: swift:6.1-noble
     steps:
       - name: Check out package
         uses: actions/checkout@v4
@@ -25,3 +26,5 @@ jobs:
   unit-tests:
     uses: vapor/ci/.github/workflows/run-unit-tests.yml@main
     secrets: inherit
+    with:
+      with_musl: true
diff --git a/Package.swift b/Package.swift
index 0c2cb084..4695ffaa 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,4 +1,4 @@
-// swift-tools-version:5.7
+// swift-tools-version:5.10
 import PackageDescription
 
 let package = Package(
@@ -20,19 +20,36 @@ let package = Package(
         .package(url: "https://github.com/apple/swift-atomics.git", from: "1.1.0"),
     ],
     targets: [
-        .target(name: "WebSocketKit", dependencies: [
-            .product(name: "NIOCore", package: "swift-nio"),
-            .product(name: "NIOConcurrencyHelpers", package: "swift-nio"),
-            .product(name: "NIOExtras", package: "swift-nio-extras"),
-            .product(name: "NIOFoundationCompat", package: "swift-nio"),
-            .product(name: "NIOHTTP1", package: "swift-nio"),
-            .product(name: "NIOSSL", package: "swift-nio-ssl"),
-            .product(name: "NIOWebSocket", package: "swift-nio"),
-            .product(name: "NIOTransportServices", package: "swift-nio-transport-services"),
-            .product(name: "Atomics", package: "swift-atomics")
-        ]),
-        .testTarget(name: "WebSocketKitTests", dependencies: [
-            .target(name: "WebSocketKit"),
-        ]),
+        .target(
+            name: "WebSocketKit",
+            dependencies: [
+                .product(name: "NIOCore", package: "swift-nio"),
+                .product(name: "NIOConcurrencyHelpers", package: "swift-nio"),
+                .product(name: "NIOExtras", package: "swift-nio-extras"),
+                .product(name: "NIOFoundationCompat", package: "swift-nio"),
+                .product(name: "NIOHTTP1", package: "swift-nio"),
+                .product(name: "NIOSSL", package: "swift-nio-ssl"),
+                .product(name: "NIOWebSocket", package: "swift-nio"),
+                .product(name: "NIOTransportServices", package: "swift-nio-transport-services"),
+                .product(name: "Atomics", package: "swift-atomics"),
+            ],
+            swiftSettings: swiftSettings
+        ),
+        .testTarget(
+            name: "WebSocketKitTests",
+            dependencies: [
+                .target(name: "WebSocketKit"),
+            ],
+            swiftSettings: swiftSettings
+        ),
     ]
 )
+
+var swiftSettings: [SwiftSetting] { [
+    .enableUpcomingFeature("ExistentialAny"),
+    .enableUpcomingFeature("ConciseMagicFile"),
+    .enableUpcomingFeature("ForwardTrailingClosures"),
+    .enableUpcomingFeature("DisableOutwardActorInference"),
+    .enableUpcomingFeature("MemberImportVisibility"),
+    .enableExperimentalFeature("StrictConcurrency=complete"),
+] }
diff --git a/README.md b/README.md
index 2066c308..1af6ca9a 100644
--- a/README.md
+++ b/README.md
@@ -1,27 +1,15 @@
 <p align="center">
-    <img 
-        src="https://user-images.githubusercontent.com/1342803/75630258-105e9c00-5bb7-11ea-81b8-86afa000e188.png"
-        height="64" 
-        alt="NIOWebSocketClient"
-    >
-    <br>
-    <br>
-    <a href="https://docs.vapor.codes/4.0/">
-        <img src="http://img.shields.io/badge/read_the-docs-2196f3.svg" alt="API Docs">
-    </a>
-    <a href="http://vapor.team">
-        <img src="https://img.shields.io/discord/431917998102675485.svg" alt="Team Chat">
-    </a>
-    <a href="LICENSE">
-        <img src="http://img.shields.io/badge/license-MIT-brightgreen.svg" alt="MIT License">
-    </a>
-    <a href="https://github.com/vapor/websocket-kit/actions/workflows/test.yml">
-        <img src="https://github.com/vapor/websocket-kit/actions/workflows/test.yml/badge.svg?event=push" alt="Continuous Integration">
-    </a>
-    <a href="https://swift.org">
-        <img src="http://img.shields.io/badge/swift-5.6-brightgreen.svg" alt="Swift 5.6">
-    </a>
-    <a href="https://swift.org">
-        <img src="http://img.shields.io/badge/swift-5.8-brightgreen.svg" alt="Swift 5.8">
-    </a>
+<img src="https://design.vapor.codes/images/vapor-websocketkit.svg" height="96" alt="WebSocketKit">
+<br>
+<br>
+<a href="https://docs.vapor.codes/4.0/"><img src="https://design.vapor.codes/images/readthedocs.svg" alt="Documentation"></a>
+<a href="https://discord.gg/vapor"><img src="https://design.vapor.codes/images/discordchat.svg" alt="Team Chat"></a>
+<a href="LICENSE"><img src="https://design.vapor.codes/images/mitlicense.svg" alt="MIT License"></a>
+<a href="https://github.com/vapor/websocket-kit/actions/workflows/test.yml"><img src="https://img.shields.io/github/actions/workflow/status/vapor/websocket-kit/test.yml?event=push&style=plastic&logo=github&label=tests&logoColor=%23ccc" alt="Continuous Integration"></a>
+<a href="https://codecov.io/gh/vapor/websocket-kit"><img src="https://img.shields.io/codecov/c/gh/vapor/websocket-kit?style=plastic&logo=codecov&label=codecov&token=FroD9hgbSC"></a>
+<a href="https://swift.org"><img src="https://design.vapor.codes/images/swift510up.svg" alt="Swift 5.10+"></a>
 </p>
+
+<br>
+
+WebSocketKit provides an abstraction over [SwiftNIO's Websockets](https://github.com/apple/swift-nio.git) to make it easy to build websocket clients and servers in Swift. IT provides functionality to send and receive data, ping/pongs and text, connection handling and upgrade handling. It should be simpler to use than using SwiftNIO directly.
diff --git a/Sources/WebSocketKit/Concurrency/WebSocket+Concurrency.swift b/Sources/WebSocketKit/Concurrency/WebSocket+Concurrency.swift
index a98afdd0..60831c6f 100644
--- a/Sources/WebSocketKit/Concurrency/WebSocket+Concurrency.swift
+++ b/Sources/WebSocketKit/Concurrency/WebSocket+Concurrency.swift
@@ -110,7 +110,7 @@ extension WebSocket {
         to url: String,
         headers: HTTPHeaders = [:],
         configuration: WebSocketClient.Configuration = .init(),
-        on eventLoopGroup: EventLoopGroup,
+        on eventLoopGroup: any EventLoopGroup,
         onUpgrade: @Sendable @escaping (WebSocket) async -> ()
     ) async throws {
         return try await self.connect(
@@ -130,7 +130,7 @@ extension WebSocket {
         to url: URL,
         headers: HTTPHeaders = [:],
         configuration: WebSocketClient.Configuration = .init(),
-        on eventLoopGroup: EventLoopGroup,
+        on eventLoopGroup: any EventLoopGroup,
         onUpgrade: @Sendable @escaping (WebSocket) async -> ()
     ) async throws {
         return try await self.connect(
@@ -154,7 +154,7 @@ extension WebSocket {
         query: String? = nil,
         headers: HTTPHeaders = [:],
         configuration: WebSocketClient.Configuration = .init(),
-        on eventLoopGroup: EventLoopGroup,
+        on eventLoopGroup: any EventLoopGroup,
         onUpgrade: @Sendable @escaping (WebSocket) async -> ()
     ) async throws {
         return try await self.connect(
diff --git a/Sources/WebSocketKit/Docs.docc/Resources/vapor-websocketkit-logo.svg b/Sources/WebSocketKit/Docs.docc/Resources/vapor-websocketkit-logo.svg
new file mode 100644
index 00000000..042a1e16
--- /dev/null
+++ b/Sources/WebSocketKit/Docs.docc/Resources/vapor-websocketkit-logo.svg
@@ -0,0 +1,32 @@
+<svg viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg" id="technology">
+  <defs>
+  <style>
+    @media(prefers-color-scheme: dark) {
+      :root { --color-logo-shape: #000000; --color-logo-base: #ffffff; }
+    }
+    body[data-color-scheme="dark"] {
+      --color-logo-shape: #000000; --color-logo-base: #ffffff;
+    }
+  </style>
+  <path id="surface" d="m 6,47 l 58,-47 58,47 -58,45 z"/>
+  <path id="strut"   d="m 6,47 v 12 l 58,45 v -12 z"/>
+  <path id="shape"   d="
+    m  81.20, 47.83    6.24, -3.60   -8.65,-15.00  -11.10, -2.97   -1.86,  6.96    8.20,  2.20 z
+    m   8.06, -0.50  -21.76, 12.56   -8.20, -2.20    0.93, -3.50    6.80,  1.80    8.84, -5.10
+      -13.74, -3.70    0.94, -3.50   13.73,  3.70   -5.10, -8.84   -6.75, -1.80    0.93, -3.45
+      -17.10, -4.60  -21.73, 12.55    9.80,  2.63   12.90, -7.43    7.20,  1.93   -2.80, 10.50
+       -7.20, -1.93   -2.04, -3.54   -6.24,  3.60    3.53,  6.10   17.03,  4.56   -1.86,  6.93
+       11.10,  2.97   30.60,-17.67 z
+  "/>
+  </defs>
+  <use href="#strut" fill="#9ee0ff"/>
+  <use href="#strut" fill="#6bd0ff" y="12"/>
+  <use href="#strut" fill="#38c0ff" y="24"/>
+  <g transform="matrix(-1 0 0 1 128 0)">
+    <use href="#strut" fill="#f29eff"/>
+    <use href="#strut" fill="#eb6bff" y="12"/>
+    <use href="#strut" fill="#e438ff" y="24"/>
+  </g>
+  <use href="#surface" style="fill: var(--color-logo-base, #000000);"/>
+  <use href="#shape" style="fill: var(--color-logo-shape, #ffffff);"/>
+</svg>
diff --git a/Sources/WebSocketKit/Docs.docc/theme-settings.json b/Sources/WebSocketKit/Docs.docc/theme-settings.json
new file mode 100644
index 00000000..4a9a1b33
--- /dev/null
+++ b/Sources/WebSocketKit/Docs.docc/theme-settings.json
@@ -0,0 +1,24 @@
+{
+  "theme": {
+    "aside": { "border-radius": "16px", "border-style": "double", "border-width": "3px" },
+    "border-radius": "0",
+    "button": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" },
+    "code":   { "border-radius": "16px", "border-width": "1px", "border-style": "solid" },
+    "color": {
+      "websocketkit": { "dark": "hsl(200, 0%, 100%)", "light": "hsl(200, 0%, 75%)" },
+      "documentation-intro-fill": "radial-gradient(circle at top, var(--color-fluentkit) 30%, #000 100%)",
+      "documentation-intro-accent": "var(--color-websocketkit)",
+      "documentation-intro-eyebrow": "white",
+      "documentation-intro-figure": "white",
+      "documentation-intro-title": "white",
+      "logo-base":  { "dark": "#fff", "light": "#000" },
+      "logo-shape": { "dark": "#000", "light": "#fff" },
+      "fill":       { "dark": "#000", "light": "#fff" }
+    },
+    "icons": { "technology": "/websocketkit/images/WebSocketKit/vapor-websocketkit-logo.svg" }
+  },
+  "features": {
+    "quickNavigation": { "enable": true },
+    "i18n": { "enable": true }
+  }
+}
diff --git a/Sources/WebSocketKit/Exports.swift b/Sources/WebSocketKit/Exports.swift
index b853992f..f8e80cee 100644
--- a/Sources/WebSocketKit/Exports.swift
+++ b/Sources/WebSocketKit/Exports.swift
@@ -1,5 +1,3 @@
-#if swift(>=5.8)
-
 @_documentation(visibility: internal) @_exported import struct NIOCore.ByteBuffer
 @_documentation(visibility: internal) @_exported import protocol NIOCore.Channel
 @_documentation(visibility: internal) @_exported import protocol NIOCore.EventLoop
@@ -8,16 +6,3 @@
 @_documentation(visibility: internal) @_exported import class NIOCore.EventLoopFuture
 @_documentation(visibility: internal) @_exported import struct NIOHTTP1.HTTPHeaders
 @_documentation(visibility: internal) @_exported import struct Foundation.URL
-
-#else
-
-@_exported import struct NIOCore.ByteBuffer
-@_exported import protocol NIOCore.Channel
-@_exported import protocol NIOCore.EventLoop
-@_exported import protocol NIOCore.EventLoopGroup
-@_exported import struct NIOCore.EventLoopPromise
-@_exported import class NIOCore.EventLoopFuture
-@_exported import struct NIOHTTP1.HTTPHeaders
-@_exported import struct Foundation.URL
-
-#endif
diff --git a/Sources/WebSocketKit/HTTPUpgradeRequestHandler.swift b/Sources/WebSocketKit/HTTPUpgradeRequestHandler.swift
index 817d67b3..4044cc28 100644
--- a/Sources/WebSocketKit/HTTPUpgradeRequestHandler.swift
+++ b/Sources/WebSocketKit/HTTPUpgradeRequestHandler.swift
@@ -1,3 +1,4 @@
+import NIOConcurrencyHelpers
 import NIOCore
 import NIOHTTP1
 
@@ -81,7 +82,7 @@ final class HTTPUpgradeRequestHandler: ChannelInboundHandler, RemovableChannelHa
         }
     }
 
-    func errorCaught(context: ChannelHandlerContext, error: Error) {
+    func errorCaught(context: ChannelHandlerContext, error: any Error) {
         self.upgradePromise.fail(error)
         context.close(promise: nil)
     }
diff --git a/Sources/WebSocketKit/WebSocket+Connect.swift b/Sources/WebSocketKit/WebSocket+Connect.swift
index a46dd397..5fe0fedd 100644
--- a/Sources/WebSocketKit/WebSocket+Connect.swift
+++ b/Sources/WebSocketKit/WebSocket+Connect.swift
@@ -17,7 +17,7 @@ extension WebSocket {
         to url: String,
         headers: HTTPHeaders = [:],
         configuration: WebSocketClient.Configuration = .init(),
-        on eventLoopGroup: EventLoopGroup,
+        on eventLoopGroup: any EventLoopGroup,
         onUpgrade: @Sendable @escaping (WebSocket) -> ()
     ) -> EventLoopFuture<Void> {
         guard let url = URL(string: url) else {
@@ -46,7 +46,7 @@ extension WebSocket {
         to url: URL,
         headers: HTTPHeaders = [:],
         configuration: WebSocketClient.Configuration = .init(),
-        on eventLoopGroup: EventLoopGroup,
+        on eventLoopGroup: any EventLoopGroup,
         onUpgrade: @Sendable @escaping (WebSocket) -> ()
     ) -> EventLoopFuture<Void> {
         let scheme = url.scheme ?? "ws"
@@ -85,7 +85,7 @@ extension WebSocket {
         query: String? = nil,
         headers: HTTPHeaders = [:],
         configuration: WebSocketClient.Configuration = .init(),
-        on eventLoopGroup: EventLoopGroup,
+        on eventLoopGroup: any EventLoopGroup,
         onUpgrade: @Sendable @escaping (WebSocket) -> ()
     ) -> EventLoopFuture<Void> {
         return WebSocketClient(
@@ -132,7 +132,7 @@ extension WebSocket {
         proxyHeaders: HTTPHeaders = [:],
         proxyConnectDeadline: NIODeadline = NIODeadline.distantFuture,
         configuration: WebSocketClient.Configuration = .init(),
-        on eventLoopGroup: EventLoopGroup,
+        on eventLoopGroup: any EventLoopGroup,
         onUpgrade: @Sendable @escaping (WebSocket) -> ()
     ) -> EventLoopFuture<Void> {
         return WebSocketClient(
@@ -175,7 +175,7 @@ extension WebSocket {
         proxyHeaders: HTTPHeaders = [:],
         proxyConnectDeadline: NIODeadline = NIODeadline.distantFuture,
         configuration: WebSocketClient.Configuration = .init(),
-        on eventLoopGroup: EventLoopGroup,
+        on eventLoopGroup: any EventLoopGroup,
         onUpgrade: @Sendable @escaping (WebSocket) -> ()
     ) -> EventLoopFuture<Void> {
         guard let url = URL(string: url) else {
diff --git a/Sources/WebSocketKit/WebSocket.swift b/Sources/WebSocketKit/WebSocket.swift
index 963e93ed..0fcdc065 100644
--- a/Sources/WebSocketKit/WebSocket.swift
+++ b/Sources/WebSocketKit/WebSocket.swift
@@ -12,7 +12,7 @@ public final class WebSocket: Sendable {
         case client
     }
 
-    public var eventLoop: EventLoop {
+    public var eventLoop: any EventLoop {
         return channel.eventLoop
     }
 
@@ -31,7 +31,7 @@ public final class WebSocket: Sendable {
 
     @usableFromInline
     /* private but @usableFromInline */
-    internal let channel: Channel
+    internal let channel: any Channel
     private let onTextCallback: NIOLoopBoundBox<@Sendable (WebSocket, String) -> ()>
     private let onBinaryCallback: NIOLoopBoundBox<@Sendable (WebSocket, ByteBuffer) -> ()>
     private let onPongCallback: NIOLoopBoundBox<@Sendable (WebSocket, ByteBuffer) -> ()>
@@ -43,7 +43,7 @@ public final class WebSocket: Sendable {
     private let frameSequence: NIOLockedValueBox<WebSocketFrameSequence?>
     private let _pingInterval: NIOLockedValueBox<TimeAmount?>
 
-    init(channel: Channel, type: PeerType) {
+    init(channel: any Channel, type: PeerType) {
         self.channel = channel
         self.type = type
         self.onTextCallback = .init({ _, _ in }, eventLoop: channel.eventLoop)
diff --git a/Sources/WebSocketKit/WebSocketClient.swift b/Sources/WebSocketKit/WebSocketClient.swift
index 0e2cefd1..e952190e 100644
--- a/Sources/WebSocketKit/WebSocketClient.swift
+++ b/Sources/WebSocketKit/WebSocketClient.swift
@@ -48,7 +48,7 @@ public final class WebSocketClient: Sendable {
     }
 
     let eventLoopGroupProvider: EventLoopGroupProvider
-    let group: EventLoopGroup
+    let group: any EventLoopGroup
     let configuration: Configuration
     let isShutdown = ManagedAtomic(false)
 
@@ -110,7 +110,6 @@ public final class WebSocketClient: Sendable {
         let bootstrap = WebSocketClient.makeBootstrap(on: self.group)
             .channelOption(ChannelOptions.socket(SocketOptionLevel(IPPROTO_TCP), TCP_NODELAY), value: 1)
             .channelInitializer { channel -> EventLoopFuture<Void> in
-
                 let uri: String
                 var upgradeRequestHeaders = headers
                 if proxy == nil {
@@ -146,7 +145,7 @@ public final class WebSocketClient: Sendable {
                     upgraders: [websocketUpgrader],
                     completionHandler: { context in
                         upgradePromise.succeed(())
-                        channel.pipeline.removeHandler(httpUpgradeRequestHandlerBox.value, promise: nil)
+                        channel.pipeline.syncOperations.removeHandler(httpUpgradeRequestHandlerBox.value, promise: nil)
                     }
                 )
                 let configBox = NIOLoopBound(config, eventLoop: channel.eventLoop)
@@ -163,11 +162,12 @@ public final class WebSocketClient: Sendable {
                         }
                     }
 
-                    return channel.pipeline.addHTTPClientHandlers(
-                        leftOverBytesStrategy: .forwardBytes,
-                        withClientUpgrade: config
-                    ).flatMap {
-                        channel.pipeline.addHandler(httpUpgradeRequestHandlerBox.value)
+                    return channel.eventLoop.submit {
+                        try channel.pipeline.syncOperations.addHTTPClientHandlers(
+                            leftOverBytesStrategy: .forwardBytes,
+                            withClientUpgrade: configBox.value
+                        )
+                        try channel.pipeline.syncOperations.addHandler(httpUpgradeRequestHandlerBox.value)
                     }
                 }
 
@@ -200,9 +200,9 @@ public final class WebSocketClient: Sendable {
                 }
 
                 proxyEstablishedPromise.futureResult.flatMap {
-                    channel.pipeline.removeHandler(decoder.value)
+                    channel.pipeline.syncOperations.removeHandler(decoder.value)
                 }.flatMap {
-                    channel.pipeline.removeHandler(encoder.value)
+                    channel.pipeline.syncOperations.removeHandler(encoder.value)
                 }.whenComplete { result in
                     switch result {
                     case .success:
@@ -265,7 +265,7 @@ public final class WebSocketClient: Sendable {
         }
     }
     
-    private static func makeBootstrap(on eventLoop: EventLoopGroup) -> NIOClientTCPBootstrapProtocol {
+    private static func makeBootstrap(on eventLoop: any EventLoopGroup) -> any NIOClientTCPBootstrapProtocol {
         #if canImport(Network)
         if let tsBootstrap = NIOTSConnectionBootstrap(validatingGroup: eventLoop) {
             return tsBootstrap
diff --git a/Sources/WebSocketKit/WebSocketHandler.swift b/Sources/WebSocketKit/WebSocketHandler.swift
index f080ce5b..3641c8ac 100644
--- a/Sources/WebSocketKit/WebSocketHandler.swift
+++ b/Sources/WebSocketKit/WebSocketHandler.swift
@@ -35,7 +35,7 @@ extension WebSocket {
     /// - Returns: A future which completes when the WebSocket connection to the server is established.
     @preconcurrency
     public static func client(
-        on channel: Channel,
+        on channel: any Channel,
         onUpgrade: @Sendable @escaping (WebSocket) -> ()
     ) -> EventLoopFuture<Void> {
         return self.configure(on: channel, as: .client, with: Configuration(), onUpgrade: onUpgrade)
@@ -49,7 +49,7 @@ extension WebSocket {
     /// - Returns: A future which completes when the WebSocket connection to the server is established.
     @preconcurrency
     public static func client(
-        on channel: Channel,
+        on channel: any Channel,
         config: Configuration,
         onUpgrade: @Sendable @escaping (WebSocket) -> ()
     ) -> EventLoopFuture<Void> {
@@ -63,7 +63,7 @@ extension WebSocket {
     /// - Returns: A future which completes when the WebSocket connection to the server is established.
     @preconcurrency
     public static func server(
-        on channel: Channel,
+        on channel: any Channel,
         onUpgrade: @Sendable @escaping (WebSocket) -> ()
     ) -> EventLoopFuture<Void> {
         return self.configure(on: channel, as: .server, with: Configuration(), onUpgrade: onUpgrade)
@@ -77,7 +77,7 @@ extension WebSocket {
     /// - Returns: A future which completes when the WebSocket connection to the server is established.
     @preconcurrency
     public static func server(
-        on channel: Channel,
+        on channel: any Channel,
         config: Configuration,
         onUpgrade: @Sendable @escaping (WebSocket) -> ()
     ) -> EventLoopFuture<Void> {
@@ -85,21 +85,23 @@ extension WebSocket {
     }
 
     private static func configure(
-        on channel: Channel,
+        on channel: any Channel,
         as type: PeerType,
         with config: Configuration,
         onUpgrade: @Sendable @escaping (WebSocket) -> ()
     ) -> EventLoopFuture<Void> {
         let webSocket = WebSocket(channel: channel, type: type)
 
-        return channel.pipeline.addHandlers([
-            NIOWebSocketFrameAggregator(
-                minNonFinalFragmentSize: config.minNonFinalFragmentSize,
-                maxAccumulatedFrameCount: config.maxAccumulatedFrameCount,
-                maxAccumulatedFrameSize: config.maxAccumulatedFrameSize
-            ),
-            WebSocketHandler(webSocket: webSocket)
-        ]).map { _ in
+        return channel.eventLoop.submit {
+            try channel.pipeline.syncOperations.addHandlers([
+                NIOWebSocketFrameAggregator(
+                    minNonFinalFragmentSize: config.minNonFinalFragmentSize,
+                    maxAccumulatedFrameCount: config.maxAccumulatedFrameCount,
+                    maxAccumulatedFrameSize: config.maxAccumulatedFrameSize
+                ),
+                WebSocketHandler(webSocket: webSocket)
+            ])
+        }.map {
             onUpgrade(webSocket)
         }
     }
@@ -131,7 +133,7 @@ private final class WebSocketHandler: ChannelInboundHandler {
         self.webSocket.handle(incoming: frame)
     }
 
-    func errorCaught(context: ChannelHandlerContext, error: Error) {
+    func errorCaught(context: ChannelHandlerContext, error: any Error) {
         let errorCode: WebSocketErrorCode
         if let error = error as? NIOWebSocketError {
             errorCode = WebSocketErrorCode(error)
diff --git a/Tests/WebSocketKitTests/AsyncWebSocketKitTests.swift b/Tests/WebSocketKitTests/AsyncWebSocketKitTests.swift
index e662b2c6..95686ecb 100644
--- a/Tests/WebSocketKitTests/AsyncWebSocketKitTests.swift
+++ b/Tests/WebSocketKitTests/AsyncWebSocketKitTests.swift
@@ -48,7 +48,7 @@ final class AsyncWebSocketKitTests: XCTestCase {
     
     func testBadURLInWebsocketConnect() async throws {
         do {
-            try await WebSocket.connect(to: "%w", on: self.elg, onUpgrade: { _ async in })
+            try await WebSocket.connect(to: "%w:", on: self.elg, onUpgrade: { _ async in })
             XCTAssertThrowsError({}())
         } catch {
             XCTAssertThrowsError(try { throw error }()) {
@@ -91,9 +91,9 @@ final class AsyncWebSocketKitTests: XCTestCase {
             return XCTFail("couldn't get port from \(String(reflecting: server.localAddress))")
         }
         try await WebSocket.connect(to: "ws://localhost:\(port)", on: self.elg) { (ws) async in
-            ws.onPong {
+            ws.onPong { s, _ in
                 do {
-                    try await $0.close()
+                    try await s.close()
                 } catch {
                     XCTFail("Failed to close websocket: \(String(reflecting: error))")
                 }
@@ -118,8 +118,8 @@ final class AsyncWebSocketKitTests: XCTestCase {
         }
         try await WebSocket.connect(to: "ws://localhost:\(port)", on: self.elg) { (ws) async in
             ws.pingInterval = .milliseconds(100)
-            ws.onPong {
-                do { try await $0.close() } catch { XCTFail("Failed to close websocket: \(String(reflecting: error))") }
+            ws.onPong { s, _ in
+                do { try await s.close() } catch { XCTFail("Failed to close websocket: \(String(reflecting: error))") }
                 promise.succeed(())
             }
         }
@@ -127,8 +127,8 @@ final class AsyncWebSocketKitTests: XCTestCase {
         try await server.close(mode: .all)
     }
 
-    var elg: EventLoopGroup!
-    
+    var elg: (any EventLoopGroup)!
+
     override func setUp() {
         // needs to be at least two to avoid client / server on same EL timing issues
         self.elg = MultiThreadedEventLoopGroup(numberOfThreads: 2)
diff --git a/Tests/WebSocketKitTests/SSLTestHelpers.swift b/Tests/WebSocketKitTests/SSLTestHelpers.swift
index a6776aab..1b31b877 100644
--- a/Tests/WebSocketKitTests/SSLTestHelpers.swift
+++ b/Tests/WebSocketKitTests/SSLTestHelpers.swift
@@ -14,10 +14,14 @@
 //===----------------------------------------------------------------------===//
 
 import Foundation
-@_implementationOnly import CNIOBoringSSL
 import NIOCore
 @testable import NIOSSL
 
+#if compiler(>=6.1)
+internal import CNIOBoringSSL
+#else
+@_implementationOnly import CNIOBoringSSL
+#endif
 
 // This function generates a random number suitable for use in an X509
 // serial field. This needs to be a positive number less than 2^159
@@ -37,7 +41,7 @@ func randomSerialNumber() -> ASN1_INTEGER {
 
     var readBytes = Array.init(repeating: UInt8(0), count: bytesToRead)
     let readCount = readBytes.withUnsafeMutableBytes {
-        return read(fd, $0.baseAddress, bytesToRead)
+        read(fd, $0.baseAddress, bytesToRead)
     }
     precondition(readCount == bytesToRead)
 
@@ -47,7 +51,7 @@ func randomSerialNumber() -> ASN1_INTEGER {
     defer {
         CNIOBoringSSL_BN_free(bn)
     }
-    
+
     _ = readBytes.withUnsafeBufferPointer {
         CNIOBoringSSL_BN_bin2bn($0.baseAddress, $0.count, bn)
     }
@@ -76,24 +80,41 @@ func generateRSAPrivateKey() -> OpaquePointer {
     precondition(generateRC == 1)
 
     let pkey = CNIOBoringSSL_EVP_PKEY_new()!
-    let assignRC = CNIOBoringSSL_EVP_PKEY_assign(pkey, EVP_PKEY_RSA, rsa)
-    
+    let assignRC = CNIOBoringSSL_EVP_PKEY_assign_RSA(pkey, rsa)
+
     precondition(assignRC == 1)
     return pkey
 }
 
+func generateECPrivateKey(curveNID: CInt = NID_X9_62_prime256v1) -> OpaquePointer {
+    let ctx = CNIOBoringSSL_EVP_PKEY_CTX_new_id(EVP_PKEY_EC, nil)!
+    defer {
+        CNIOBoringSSL_EVP_PKEY_CTX_free(ctx)
+    }
+
+    precondition(CNIOBoringSSL_EVP_PKEY_keygen_init(ctx) == 1)
+    precondition(CNIOBoringSSL_EVP_PKEY_CTX_set_ec_paramgen_curve_nid(ctx, curveNID) == 1)
+
+    var pkey: OpaquePointer? = nil
+    precondition(CNIOBoringSSL_EVP_PKEY_keygen(ctx, &pkey) == 1)
+
+    return pkey!
+}
+
 func addExtension(x509: OpaquePointer, nid: CInt, value: String) {
     var extensionContext = X509V3_CTX()
-    
+
     CNIOBoringSSL_X509V3_set_ctx(&extensionContext, x509, x509, nil, nil, 0)
     let ext = value.withCString { (pointer) in
-        return CNIOBoringSSL_X509V3_EXT_nconf_nid(nil, &extensionContext, nid, UnsafeMutablePointer(mutating: pointer))
+        CNIOBoringSSL_X509V3_EXT_nconf_nid(nil, &extensionContext, nid, UnsafeMutablePointer(mutating: pointer))
     }!
     CNIOBoringSSL_X509_add_ext(x509, ext, -1)
     CNIOBoringSSL_X509_EXTENSION_free(ext)
 }
 
-func generateSelfSignedCert(keygenFunction: () -> OpaquePointer = generateRSAPrivateKey) -> (NIOSSLCertificate, NIOSSLPrivateKey) {
+func generateSelfSignedCert(
+    keygenFunction: () -> OpaquePointer = generateRSAPrivateKey
+) -> (NIOSSLCertificate, NIOSSLPrivateKey) {
     let pkey = keygenFunction()
     let x = CNIOBoringSSL_X509_new()!
     CNIOBoringSSL_X509_set_version(x, 2)
@@ -102,42 +123,48 @@ func generateSelfSignedCert(keygenFunction: () -> OpaquePointer = generateRSAPri
     // safe, there will be no use-after-free.
     var serial = randomSerialNumber()
     CNIOBoringSSL_X509_set_serialNumber(x, &serial)
-    
+
     let notBefore = CNIOBoringSSL_ASN1_TIME_new()!
     var now = time(nil)
     CNIOBoringSSL_ASN1_TIME_set(notBefore, now)
     CNIOBoringSSL_X509_set_notBefore(x, notBefore)
     CNIOBoringSSL_ASN1_TIME_free(notBefore)
-    
+
     now += 60 * 60  // Give ourselves an hour
     let notAfter = CNIOBoringSSL_ASN1_TIME_new()!
     CNIOBoringSSL_ASN1_TIME_set(notAfter, now)
     CNIOBoringSSL_X509_set_notAfter(x, notAfter)
     CNIOBoringSSL_ASN1_TIME_free(notAfter)
-    
+
     CNIOBoringSSL_X509_set_pubkey(x, pkey)
-    
+
     let commonName = "localhost"
     let name = CNIOBoringSSL_X509_get_subject_name(x)
     commonName.withCString { (pointer: UnsafePointer<Int8>) -> Void in
-        pointer.withMemoryRebound(to: UInt8.self, capacity: commonName.lengthOfBytes(using: .utf8)) { (pointer: UnsafePointer<UInt8>) -> Void in
-            CNIOBoringSSL_X509_NAME_add_entry_by_NID(name,
-                                                     NID_commonName,
-                                                     MBSTRING_UTF8,
-                                                     UnsafeMutablePointer(mutating: pointer),
-                                                     CInt(commonName.lengthOfBytes(using: .utf8)),
-                                                     -1,
-                                                     0)
+        pointer.withMemoryRebound(to: UInt8.self, capacity: commonName.lengthOfBytes(using: .utf8)) {
+            (pointer: UnsafePointer<UInt8>) -> Void in
+            CNIOBoringSSL_X509_NAME_add_entry_by_NID(
+                name,
+                NID_commonName,
+                MBSTRING_UTF8,
+                UnsafeMutablePointer(mutating: pointer),
+                ossl_ssize_t(commonName.lengthOfBytes(using: .utf8)),
+                -1,
+                0
+            )
         }
     }
     CNIOBoringSSL_X509_set_issuer_name(x, name)
-    
+
     addExtension(x509: x, nid: NID_basic_constraints, value: "critical,CA:FALSE")
     addExtension(x509: x, nid: NID_subject_key_identifier, value: "hash")
     addExtension(x509: x, nid: NID_subject_alt_name, value: "DNS:localhost")
     addExtension(x509: x, nid: NID_ext_key_usage, value: "critical,serverAuth,clientAuth")
-    
+
     CNIOBoringSSL_X509_sign(x, pkey, CNIOBoringSSL_EVP_sha256())
-    
-    return (NIOSSLCertificate.fromUnsafePointer(takingOwnership: x), NIOSSLPrivateKey.fromUnsafePointer(takingOwnership: pkey))
+
+    return (
+        NIOSSLCertificate.fromUnsafePointer(takingOwnership: x),
+        NIOSSLPrivateKey.fromUnsafePointer(takingOwnership: pkey)
+    )
 }
diff --git a/Tests/WebSocketKitTests/Utilities.swift b/Tests/WebSocketKitTests/Utilities.swift
index 60043ec4..f88c7cb5 100644
--- a/Tests/WebSocketKitTests/Utilities.swift
+++ b/Tests/WebSocketKitTests/Utilities.swift
@@ -2,16 +2,16 @@ import XCTest
 import Atomics
 import NIO
 import NIOExtras
-import NIOHTTP1
-import NIOSSL
+@preconcurrency import NIOHTTP1
+@preconcurrency import NIOSSL
 import NIOWebSocket
 @testable import WebSocketKit
 
 extension ServerBootstrap {
     static func webSocket(
-        on eventLoopGroup: EventLoopGroup,
+        on eventLoopGroup: any EventLoopGroup,
         tls: Bool = false,
-        onUpgrade: @escaping (HTTPRequestHead, WebSocket) -> ()
+        onUpgrade: @escaping @Sendable (HTTPRequestHead, WebSocket) -> ()
     ) -> ServerBootstrap {
         return ServerBootstrap(group: eventLoopGroup).childChannelInitializer { channel in
             if tls {
@@ -46,7 +46,7 @@ extension ServerBootstrap {
     }
 }
 
-internal final class WebsocketBin {
+internal final class WebsocketBin: @unchecked Sendable {
     enum BindTarget {
         case unixDomainSocket(String)
         case localhostIPv4RandomPort
@@ -60,14 +60,14 @@ internal final class WebsocketBin {
         case http1_1(ssl: Bool = false)
     }
 
-    enum Proxy {
+    enum Proxy: Sendable {
         case none
         case simulate(config: ProxyConfig, authorization: String?)
     }
 
-    struct ProxyConfig {
+    struct ProxyConfig: Sendable {
         var tls: Bool
-        let headVerification: (ChannelHandlerContext, HTTPRequestHead) -> Void
+        let headVerification: @Sendable (ChannelHandlerContext, HTTPRequestHead) -> Void
     }
 
     let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
@@ -78,7 +78,7 @@ internal final class WebsocketBin {
 
     private let mode: Mode
     private let sslContext: NIOSSLContext?
-    private var serverChannel: Channel!
+    private var serverChannel: (any Channel)!
     private let isShutdown = ManagedAtomic(false)
 
     init(
@@ -86,7 +86,7 @@ internal final class WebsocketBin {
         proxy: Proxy = .none,
         bindTarget: BindTarget = .localhostIPv4RandomPort,
         sslContext: NIOSSLContext?,
-        onUpgrade: @escaping (HTTPRequestHead, WebSocket) -> ()
+        onUpgrade: @escaping @Sendable (HTTPRequestHead, WebSocket) -> ()
     ) {
         self.mode = mode
         self.sslContext = sslContext
@@ -169,29 +169,29 @@ internal final class WebsocketBin {
     // In the TLS case we must set up the 'proxy' and the 'server' handlers sequentially
     // rather than re-using parts because the requestDecoder stops parsing after a CONNECT request
     private func syncAddTLSHTTPProxyHandlers(
-        to channel: Channel,
+        to channel: any Channel,
         proxyConfig: ProxyConfig,
         expectedAuthorization: String?,
-        upgraders: [HTTPServerProtocolUpgrader]
+        upgraders: [any HTTPServerProtocolUpgrader]
     ) throws {
         let sync = channel.pipeline.syncOperations
         let promise = channel.eventLoop.makePromise(of: Void.self)
 
-        let responseEncoder = HTTPResponseEncoder()
-        let requestDecoder = ByteToMessageHandler(HTTPRequestDecoder(leftOverBytesStrategy: .forwardBytes))
-        let proxySimulator = HTTPProxySimulator(promise: promise, config: proxyConfig, expectedAuthorization: expectedAuthorization)
+        let responseEncoder = NIOLoopBound(HTTPResponseEncoder(), eventLoop: channel.eventLoop)
+        let requestDecoder = NIOLoopBound(ByteToMessageHandler(HTTPRequestDecoder(leftOverBytesStrategy: .forwardBytes)), eventLoop: channel.eventLoop)
+        let proxySimulator = NIOLoopBound(HTTPProxySimulator(promise: promise, config: proxyConfig, expectedAuthorization: expectedAuthorization), eventLoop: channel.eventLoop)
 
-        try sync.addHandler(responseEncoder)
-        try sync.addHandler(requestDecoder)
+        try sync.addHandler(responseEncoder.value)
+        try sync.addHandler(requestDecoder.value)
 
-        try sync.addHandler(proxySimulator)
+        try sync.addHandler(proxySimulator.value)
 
         promise.futureResult.flatMap { _ in
-            channel.pipeline.removeHandler(proxySimulator)
+            channel.pipeline.syncOperations.removeHandler(proxySimulator.value)
         }.flatMap { _ in
-            channel.pipeline.removeHandler(responseEncoder)
+            channel.pipeline.syncOperations.removeHandler(responseEncoder.value)
         }.flatMap { _ in
-            channel.pipeline.removeHandler(requestDecoder)
+            channel.pipeline.syncOperations.removeHandler(requestDecoder.value)
         }.whenComplete { result in
             switch result {
             case .failure:
@@ -207,22 +207,22 @@ internal final class WebsocketBin {
     // In the plain-text case we must set up the 'proxy' and the 'server' handlers simultaneously
     // so that the combined proxy/upgrade request can be processed by the separate proxy and upgrade handlers
     private func syncAddHTTPProxyHandlers(
-        to channel: Channel,
+        to channel: any Channel,
         proxyConfig: ProxyConfig,
         expectedAuthorization: String?,
-        upgraders: [HTTPServerProtocolUpgrader]
+        upgraders: [any HTTPServerProtocolUpgrader]
     ) throws {
         let sync = channel.pipeline.syncOperations
         let promise = channel.eventLoop.makePromise(of: Void.self)
 
         let responseEncoder = HTTPResponseEncoder()
         let requestDecoder = ByteToMessageHandler(HTTPRequestDecoder(leftOverBytesStrategy: .forwardBytes))
-        let proxySimulator = HTTPProxySimulator(promise: promise, config: proxyConfig, expectedAuthorization: expectedAuthorization)
+        let proxySimulator = NIOLoopBound(HTTPProxySimulator(promise: promise, config: proxyConfig, expectedAuthorization: expectedAuthorization), eventLoop: channel.eventLoop)
 
         let serverPipelineHandler = HTTPServerPipelineHandler()
         let serverProtocolErrorHandler = HTTPServerProtocolErrorHandler()
 
-        let extraHTTPHandlers: [RemovableChannelHandler] = [
+        let extraHTTPHandlers: [any RemovableChannelHandler] = [
             requestDecoder,
             serverPipelineHandler,
             serverProtocolErrorHandler
@@ -231,7 +231,7 @@ internal final class WebsocketBin {
         try sync.addHandler(responseEncoder)
         try sync.addHandler(requestDecoder)
 
-        try sync.addHandler(proxySimulator)
+        try sync.addHandler(proxySimulator.value)
 
         try sync.addHandler(serverPipelineHandler)
         try sync.addHandler(serverProtocolErrorHandler)
@@ -248,7 +248,7 @@ internal final class WebsocketBin {
         try sync.addHandler(upgrader)
 
         promise.futureResult.flatMap { () -> EventLoopFuture<Void> in
-            channel.pipeline.removeHandler(proxySimulator)
+            channel.pipeline.syncOperations.removeHandler(proxySimulator.value)
         }.whenComplete { result in
             switch result {
             case .failure:
@@ -259,7 +259,7 @@ internal final class WebsocketBin {
         }
     }
 
-    private func httpProxyEstablished(_ channel: Channel, upgraders: [HTTPServerProtocolUpgrader]) {
+    private func httpProxyEstablished(_ channel: any Channel, upgraders: [any HTTPServerProtocolUpgrader]) {
         do {
             // if a connection has been established, we need to negotiate TLS before
             // anything else. Depending on the negotiation, the HTTPHandlers will be added.
diff --git a/Tests/WebSocketKitTests/WebSocketKitTests.swift b/Tests/WebSocketKitTests/WebSocketKitTests.swift
index 9fa402ba..4be932e0 100644
--- a/Tests/WebSocketKitTests/WebSocketKitTests.swift
+++ b/Tests/WebSocketKitTests/WebSocketKitTests.swift
@@ -347,7 +347,7 @@ final class WebSocketKitTests: XCTestCase {
         let promise = elg.any().makePromise(of: String.self)
 
         let localWebsocketBin: WebsocketBin
-        let verifyProxyHead = { (ctx: ChannelHandlerContext, requestHead: HTTPRequestHead) in
+        let verifyProxyHead = { @Sendable (ctx: ChannelHandlerContext, requestHead: HTTPRequestHead) in
             XCTAssertEqual(requestHead.uri, "ws://apple.com/:\(ctx.localAddress!.port!)")
             XCTAssertEqual(requestHead.headers.first(name: "Host"), "apple.com")
         }
@@ -405,7 +405,7 @@ final class WebSocketKitTests: XCTestCase {
         )
         let sslContext = try! NIOSSLContext(configuration: configuration)
 
-        let verifyProxyHead = { (ctx: ChannelHandlerContext, requestHead: HTTPRequestHead) in
+        let verifyProxyHead = { @Sendable (ctx: ChannelHandlerContext, requestHead: HTTPRequestHead) in
             // CONNECT uses a special form of request target, unique to this method, consisting of
             // only the host and port number of the tunnel destination, separated by a colon.
             // https://httpwg.org/specs/rfc9110.html#CONNECT
@@ -479,7 +479,7 @@ final class WebSocketKitTests: XCTestCase {
     }
     
     func testBadURLInWebsocketConnect() async throws {
-        XCTAssertThrowsError(try WebSocket.connect(to: "%w", on: self.elg, onUpgrade: { _ in }).wait()) {
+        XCTAssertThrowsError(try WebSocket.connect(to: "%w:", on: self.elg, onUpgrade: { _ in }).wait()) {
             guard case .invalidURL = $0 as? WebSocketClient.Error else {
                 return XCTFail("Expected .invalidURL but got \(String(reflecting: $0))")
             }
@@ -516,8 +516,8 @@ final class WebSocketKitTests: XCTestCase {
             return XCTFail("couldn't get port from \(String(reflecting: server.localAddress))")
         }
         WebSocket.connect(to: "ws://localhost:\(port)", on: self.elg) { ws in
-            ws.onPong {
-                $0.close(promise: closePromise)
+            ws.onPong { s, _ in
+                s.close(promise: closePromise)
                 promise.succeed()
             }
             ws.sendPing()
@@ -536,8 +536,8 @@ final class WebSocketKitTests: XCTestCase {
         }
         WebSocket.connect(to: "ws://localhost:\(port)", on: self.elg) { ws in
             ws.pingInterval = .milliseconds(100)
-            ws.onPong {
-                $0.close(promise: closePromise)
+            ws.onPong { s, _ in
+                s.close(promise: closePromise)
                 promise.succeed()
             }
         }.cascadeFailure(to: closePromise)
@@ -545,14 +545,15 @@ final class WebSocketKitTests: XCTestCase {
         XCTAssertNoThrow(try closePromise.futureResult.wait())
         try server.close(mode: .all).wait()
     }
-    
+
+    @available(*, deprecated, message: "Tests deprecated functionality")
     func testCreateNewELGAndShutdown() throws {
         let client = WebSocketClient(eventLoopGroupProvider: .createNew)
         try client.syncShutdown()
     }
 
-    var elg: EventLoopGroup!
-    
+    var elg: (any EventLoopGroup)!
+
     override func setUp() {
         // needs to be at least two to avoid client / server on same EL timing issues
         self.elg = MultiThreadedEventLoopGroup(numberOfThreads: 2)


Reviews:
user: login: MahdiBM
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-26T10:58:52Z
user: login: gwynne
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-26T15:28:43Z
user: login: 0xTim
body: One query around sendability
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-26T17:03:50Z
user: login: gwynne
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-26T17:44:14Z
user: login: gwynne
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-26T17:45:02Z
user: login: 0xTim
body: Nice to see the tests passing again! That should at least unblock other PRs
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-27T02:42:21Z

Review comments:
diff_hunk: @@ -547,12 +547,12 @@ final class WebSocketKitTests: XCTestCase {
     }
     
     func testCreateNewELGAndShutdown() throws {
-        let client = WebSocketClient(eventLoopGroupProvider: .createNew)
+        let client = WebSocketClient(eventLoopGroupProvider: .shared(.singletonMultiThreadedEventLoopGroup))
path: Tests/WebSocketKitTests/WebSocketKitTests.swift
user: login: MahdiBM
body: Did this have a warning or something? because the test name doesn't quite match the change you made.
created_at: 2025-04-26T10:58:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -547,12 +547,12 @@ final class WebSocketKitTests: XCTestCase {
     }
     
     func testCreateNewELGAndShutdown() throws {
-        let client = WebSocketClient(eventLoopGroupProvider: .createNew)
+        let client = WebSocketClient(eventLoopGroupProvider: .shared(.singletonMultiThreadedEventLoopGroup))
path: Tests/WebSocketKitTests/WebSocketKitTests.swift
user: login: gwynne
body: `.createNew` is deprecated and I wasn't paying attention to the test name 🤦‍♀️
created_at: 2025-04-26T15:28:43Z
author_association: MEMBER
diff_hunk: @@ -1,7 +1,8 @@
+import NIOConcurrencyHelpers
 import NIOCore
 import NIOHTTP1
 
-final class HTTPUpgradeRequestHandler: ChannelInboundHandler, RemovableChannelHandler {
+final class HTTPUpgradeRequestHandler: ChannelInboundHandler, RemovableChannelHandler, Sendable {
path: Sources/WebSocketKit/HTTPUpgradeRequestHandler.swift
user: login: 0xTim
body: Why does this need to be `Sendable`?
created_at: 2025-04-26T16:57:25Z
author_association: MEMBER
diff_hunk: @@ -169,29 +169,29 @@ internal final class WebsocketBin {
     // In the TLS case we must set up the 'proxy' and the 'server' handlers sequentially
     // rather than re-using parts because the requestDecoder stops parsing after a CONNECT request
     private func syncAddTLSHTTPProxyHandlers(
-        to channel: Channel,
+        to channel: any Channel,
         proxyConfig: ProxyConfig,
         expectedAuthorization: String?,
-        upgraders: [HTTPServerProtocolUpgrader]
+        upgraders: [any HTTPServerProtocolUpgrader]
     ) throws {
         let sync = channel.pipeline.syncOperations
         let promise = channel.eventLoop.makePromise(of: Void.self)
 
-        let responseEncoder = HTTPResponseEncoder()
-        let requestDecoder = ByteToMessageHandler(HTTPRequestDecoder(leftOverBytesStrategy: .forwardBytes))
-        let proxySimulator = HTTPProxySimulator(promise: promise, config: proxyConfig, expectedAuthorization: expectedAuthorization)
+        let responseEncoder = NIOLoopBound(HTTPResponseEncoder(), eventLoop: channel.eventLoop)
path: Tests/WebSocketKitTests/Utilities.swift
user: login: 0xTim
body: It's tests so doesn't matter as much but why the loop bound dance here? We're `@unchecked Sendable` and `@preconcurrency` importing the types. I would have assumed one of the 3 approaches would be enough
created_at: 2025-04-26T17:03:34Z
author_association: MEMBER
diff_hunk: @@ -1,7 +1,8 @@
+import NIOConcurrencyHelpers
 import NIOCore
 import NIOHTTP1
 
-final class HTTPUpgradeRequestHandler: ChannelInboundHandler, RemovableChannelHandler {
+final class HTTPUpgradeRequestHandler: ChannelInboundHandler, RemovableChannelHandler, Sendable {
path: Sources/WebSocketKit/HTTPUpgradeRequestHandler.swift
user: login: gwynne
body: Because I didn't think of using `NIOLoopBound` first. I'll change it.
created_at: 2025-04-26T17:44:14Z
author_association: MEMBER
diff_hunk: @@ -169,29 +169,29 @@ internal final class WebsocketBin {
     // In the TLS case we must set up the 'proxy' and the 'server' handlers sequentially
     // rather than re-using parts because the requestDecoder stops parsing after a CONNECT request
     private func syncAddTLSHTTPProxyHandlers(
-        to channel: Channel,
+        to channel: any Channel,
         proxyConfig: ProxyConfig,
         expectedAuthorization: String?,
-        upgraders: [HTTPServerProtocolUpgrader]
+        upgraders: [any HTTPServerProtocolUpgrader]
     ) throws {
         let sync = channel.pipeline.syncOperations
         let promise = channel.eventLoop.makePromise(of: Void.self)
 
-        let responseEncoder = HTTPResponseEncoder()
-        let requestDecoder = ByteToMessageHandler(HTTPRequestDecoder(leftOverBytesStrategy: .forwardBytes))
-        let proxySimulator = HTTPProxySimulator(promise: promise, config: proxyConfig, expectedAuthorization: expectedAuthorization)
+        let responseEncoder = NIOLoopBound(HTTPResponseEncoder(), eventLoop: channel.eventLoop)
path: Tests/WebSocketKitTests/Utilities.swift
user: login: gwynne
body: Because futures are now Strict Concurrency-correct and _everything_ you capture in a closure has to be `Sendable` in ways that `@preconcurrency` would only silence if you applied it to the entire `NIOCore` import.
created_at: 2025-04-26T17:45:02Z
author_association: MEMBER

PR comments:
user: login: codecov[bot]
created_at: 2025-04-26T06:40:43Z
body: ## [Codecov](https://app.codecov.io/gh/vapor/websocket-kit/pull/158?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=vapor) Report
Attention: Patch coverage is `91.66667%` with `2 lines` in your changes missing coverage. Please review.
> Project coverage is 82.39%. Comparing base [(`a935b63`)](https://app.codecov.io/gh/vapor/websocket-kit/commit/a935b63b7375f14cb825e828d8b5b4df9e3f1f92?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=vapor) to head [(`3b0e716`)](https://app.codecov.io/gh/vapor/websocket-kit/commit/3b0e71617815c995b6a3d970be15026e1e1f82f1?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=vapor).
> Report is 5 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/vapor/websocket-kit/pull/158?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=vapor) | Patch % | Lines |
|---|---|---|
| [...urces/WebSocketKit/HTTPUpgradeRequestHandler.swift](https://app.codecov.io/gh/vapor/websocket-kit/pull/158?src=pr&el=tree&filepath=Sources%2FWebSocketKit%2FHTTPUpgradeRequestHandler.swift&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=vapor#diff-U291cmNlcy9XZWJTb2NrZXRLaXQvSFRUUFVwZ3JhZGVSZXF1ZXN0SGFuZGxlci5zd2lmdA==) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/vapor/websocket-kit/pull/158?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=vapor) |
| [Sources/WebSocketKit/WebSocketHandler.swift](https://app.codecov.io/gh/vapor/websocket-kit/pull/158?src=pr&el=tree&filepath=Sources%2FWebSocketKit%2FWebSocketHandler.swift&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=vapor#diff-U291cmNlcy9XZWJTb2NrZXRLaXQvV2ViU29ja2V0SGFuZGxlci5zd2lmdA==) | 90.90% | [1 Missing :warning: ](https://app.codecov.io/gh/vapor/websocket-kit/pull/158?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=vapor) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #158      +/-   ##
==========================================
- Coverage   82.85%   82.39%   -0.46%     
==========================================
  Files           6        6              
  Lines         659      659              
==========================================
- Hits          546      543       -3     
- Misses        113      116       +3     
```

| [Files with missing lines](https://app.codecov.io/gh/vapor/websocket-kit/pull/158?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=vapor) | Coverage Δ | |
|---|---|---|
| [...bSocketKit/Concurrency/WebSocket+Concurrency.swift](https://app.codecov.io/gh/vapor/websocket-kit/pull/158?src=pr&el=tree&filepath=Sources%2FWebSocketKit%2FConcurrency%2FWebSocket%2BConcurrency.swift&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=vapor#diff-U291cmNlcy9XZWJTb2NrZXRLaXQvQ29uY3VycmVuY3kvV2ViU29ja2V0K0NvbmN1cnJlbmN5LnN3aWZ0) | `48.80% <ø> (ø)` | |
| [Sources/WebSocketKit/WebSocket+Connect.swift](https://app.codecov.io/gh/vapor/websocket-kit/pull/158?src=pr&el=tree&filepath=Sources%2FWebSocketKit%2FWebSocket%2BConnect.swift&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=vapor#diff-U291cmNlcy9XZWJTb2NrZXRLaXQvV2ViU29ja2V0K0Nvbm5lY3Quc3dpZnQ=) | `98.59% <ø> (ø)` | |
| [Sources/WebSocketKit/WebSocket.swift](https://app.codecov.io/gh/vapor/websocket-kit/pull/158?src=pr&el=tree&filepath=Sources%2FWebSocketKit%2FWebSocket.swift&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=vapor#diff-U291cmNlcy9XZWJTb2NrZXRLaXQvV2ViU29ja2V0LnN3aWZ0) | `88.12% <100.00%> (-0.92%)` | :arrow_down: |
| [Sources/WebSocketKit/WebSocketClient.swift](https://app.codecov.io/gh/vapor/websocket-kit/pull/158?src=pr&el=tree&filepath=Sources%2FWebSocketKit%2FWebSocketClient.swift&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=vapor#diff-U291cmNlcy9XZWJTb2NrZXRLaXQvV2ViU29ja2V0Q2xpZW50LnN3aWZ0) | `94.70% <100.00%> (-0.65%)` | :arrow_down: |
| [...urces/WebSocketKit/HTTPUpgradeRequestHandler.swift](https://app.codecov.io/gh/vapor/websocket-kit/pull/158?src=pr&el=tree&filepath=Sources%2FWebSocketKit%2FHTTPUpgradeRequestHandler.swift&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=vapor#diff-U291cmNlcy9XZWJTb2NrZXRLaXQvSFRUUFVwZ3JhZGVSZXF1ZXN0SGFuZGxlci5zd2lmdA==) | `71.92% <0.00%> (ø)` | |
| [Sources/WebSocketKit/WebSocketHandler.swift](https://app.codecov.io/gh/vapor/websocket-kit/pull/158?src=pr&el=tree&filepath=Sources%2FWebSocketKit%2FWebSocketHandler.swift&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=vapor#diff-U291cmNlcy9XZWJTb2NrZXRLaXQvV2ViU29ja2V0SGFuZGxlci5zd2lmdA==) | `63.79% <90.90%> (+1.29%)` | :arrow_up: |

</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: gwynne
created_at: 2025-04-26T07:13:21Z
body: 5.10 CI failure is caused by NIO, see https://github.com/apple/swift-nio/issues/3223
author_association: MEMBER