Repository navigation
Perfect-NIO 0.2.0 puts limits on upgraded WebSocket connections. In 0.1.0 a single client could make the server hold a message of any size, a client that went silent kept its connection open forever, and a message over 16 KiB sent in one frame (as browsers send them) was refused. This release fixes all three. It's a minor release because two of the fixes change behaviour; see "Breaking and behaviour changes".
.package(url: "https://github.com/PerfectlySoft/Perfect-NIO.git", from: "0.2.0")from: "0.1.0" also resolves 0.2.0. Use .upToNextMinor(from: "0.1.0") to stay on 0.1.x.
Requirements
Unchanged from 0.1.0: Swift 6.2 or later, macOS 12+ or Linux (with pkg-config and zlib1g-dev).
Highlights
- WebSocket message size limit (#20).
Server.maxWebSocketMessageSize(defaultServer.defaultMaxWebSocketMessageSize, 1 MiB) caps a message whether it arrives in one frame or in fragments. A route can lower it with the newWebSocketOption.maxMessageSize(_:), but not raise it. A larger message ends the connection with close code 1009 (message too big), andreadMessage()returns.closefrom then on. - WebSocket keepalive (#20).
.pingInterval(_:)and.responseTimeout(_:), reserved in 0.1.0, now work. AfterpingIntervalseconds without a frame from the client (default 30), the server sends a ping. If no frame arrives withinresponseTimeoutseconds (default 30), it closes the connection. Any frame resets the clock, browsers answer pings automatically, and handlers never see the pong that answers the keepalive's own ping..pingInterval(0)turns it off.
Breaking and behaviour changes
- Messages over 1 MiB end the connection. In 0.1.0 fragmented messages had no limit, and single frames were refused above 16 KiB. Upgrade: raise
Server(maxWebSocketMessageSize:)for endpoints that take larger messages. - Single frames up to the limit are accepted. A message over 16 KiB sent in one frame used to get close code 1009; it's now accepted up to
maxWebSocketMessageSize. Upgrade: nothing. - Silent WebSocket clients are disconnected. An upgraded connection whose client sends nothing is pinged after 30 s and closed about 30 s later if it still sends nothing. Frames are only read while the handler calls
readMessage(), so a handler that stops reading for longer thanpingInterval + responseTimeoutcan be closed too. Upgrade: pass.pingInterval(0)to keep the 0.1.0 behaviour, or larger values for clients that are slow to respond. WebSocketOptionhas a new case,maxMessageSize. Upgrade: an exhaustiveswitchoverWebSocketOptionoutside the package needs adefault:.- Additive API:
Server.maxWebSocketMessageSize,Server.defaultMaxWebSocketMessageSize, the defaultedmaxWebSocketMessageSize:argument onServer.init, andWebSocketOption.defaultPingInterval/defaultResponseTimeout. Existing calls compile unchanged.
Fixes
All in #20, against 0.1.0:
- Memory exhaustion through fragmented messages. Continuation frames were appended without any total limit. A probe delivered a 65,536,000-byte message from one connection in 16 KB fragments. Reassembly now stops at the limit, drops what it gathered and closes with 1009.
- Messages over 16 KiB in a single frame were refused. The upgraders used NIO's default
maxFrameSize(16 KiB), so a single 20,000-byte frame was answered with close code 1009. The frame cap now followsmaxWebSocketMessageSize. - Upgraded connections never timed out. The idle timeout is removed on upgrade, and the options meant to replace it were no-ops.
- A huge limit would trap. NIO requires
maxFrameSize <= UInt32.max, so the server clampsmaxWebSocketMessageSizeto between 125 bytes andUInt32.max.
Tests: a new WebSocketLimitTests suite (9 tests) covers:
- option resolution;
- a 100,000-byte single-frame message;
- fragmented messages at and over a route's limit;
- a single frame over a route's limit;
- the server limit, for single frames and for fragments;
- clamping;
- a silent client being pinged and closed;
- an answering client staying open without its handler seeing the keepalive pong;
- the keepalive turned off.
With the reassembly limit and the keepalive removed, the reassembly, server-limit and both keepalive tests fail.
Verified: Pull request CI passed all 16 checks (Linux Swift 6.2 and 6.4, macOS Xcode 26.0.1 and 26.6, debug and release, warnings as errors, live MySQL), and CI on main (f5df3c8) passed all 8 jobs. Locally on macOS, all 118 + 2 Swift Testing tests and the XCTest suites pass, and a release build with -enable-testing compiles. On Linux with swift:6.2-noble and swift:6.4-noble, all 120 Swift Testing tests and the XCTest suites pass.
Known limitations
Everything listed for 0.1.0 still applies, plus:
- Messages are buffered in memory. Each open WebSocket connection can hold up to
maxWebSocketMessageSizewhile a message arrives. Total memory scales with the number of connections. - No close status in the API.
.closecarries no status code or reason in either direction. Only the server's own 1009 sends a code. - Text isn't validated. Invalid UTF-8 in a text message is decoded with replacement characters rather than closing with 1007.
- A stray continuation frame (one with no message in progress) is treated as the start of a binary message rather than as a protocol error.
- The pre-macOS 13 upgrade pipeline has the same changes, but, as in 0.1.0, only unit tests cover it.
Pull requests
- #20: WebSocket message size limit and keepalive.