fix(websocket): bound frames and messages, close two RFC 6455 gaps
CI / lint (pull_request) Successful in 2m34s
CI / format (pull_request) Successful in 3m15s
CI / typecheck (pull_request) Canceled after 0s
CI / typetest (pull_request) Canceled after 0s
CI / node-tests (pull_request) Canceled after 0s
CI / browser-tests (pull_request) Canceled after 0s
CI / install (pull_request) Canceled after 7m19s

readFrame buffered whatever payload length a peer announced, and
continuation frames accumulated with no cap on their count or on the
assembled size, so a peer could make a connection allocate without bound.
It was the only protocol package without such a limit. maxFrameSize,
maxMessageSize and maxFragments now bound all three and are configurable
through the client and server options; exceeding one closes the connection
with 1009.

Two conformance gaps sat in the same function. The 64-bit extended length
was accumulated with arithmetic that silently loses precision above 2^53
and never checked that its most significant bit is zero (§5.2). Masking
was parsed but never enforced (§5.1): a server accepted unmasked client
frames and a client accepted masked server frames. Both now fail the
connection with 1002.

Closes #200

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-21 23:55:32 +00:00
co-authored by Claude
parent b534fe21ea
commit 6fba0cdcba
8 changed files with 430 additions and 18 deletions
+12
View File
@@ -45,6 +45,18 @@ const handler: Handler = async (ctx) => {
} }
``` ```
## Limits
A WebSocket peer chooses how much a connection allocates, so every incoming frame and message is bounded. Both `connectWebSocket` and `upgradeWebSocket` accept these alongside their other options; pass `Infinity` for one to opt out of it.
| Option | Default | Applies to |
| ---------------- | ------- | ---------------------------------------------------- |
| `maxFrameSize` | 1 MiB | payload of a single frame, checked before it is read |
| `maxMessageSize` | 8 MiB | assembled size of a message, checked as it arrives |
| `maxFragments` | 1024 | number of frames one message may be fragmented into |
Exceeding one of them fails the connection with close status 1009 (message too big). A frame that breaks the protocol — a client frame that is not masked, a server frame that is, or a 64-bit payload length with its most significant bit set — fails it with 1002 (protocol error). In both cases the iterator ends after the Close frame is sent.
## See also ## See also
- [`@webnet/http`](../http) — the `Handler`/`ClientResponse`/`Server` this package upgrades - [`@webnet/http`](../http) — the `Handler`/`ClientResponse`/`Server` this package upgrades
+11 -5
View File
@@ -1,15 +1,20 @@
import type { ClientResponse } from "@webnet/http/client" import type { ClientResponse } from "@webnet/http/client"
import { ClientConnection, ClientRequestImpl } from "@webnet/http/client" import { ClientConnection, ClientRequestImpl } from "@webnet/http/client"
import type { RawDialer } from "@webnet/transport" import type { RawDialer } from "@webnet/transport"
import type { WebSocketLimits } from "./common.js"
import { computeAccept, generateKey, WebSocketConnection } from "./common.js" import { computeAccept, generateKey, WebSocketConnection } from "./common.js"
export type WebSocketConnectOptions = { export type WebSocketConnectOptions = WebSocketLimits & {
protocols?: string[] protocols?: string[]
headers?: Record<string, string> headers?: Record<string, string>
} }
// Shared: verify Sec-WebSocket-Accept, hijack on success, hijack+close on failure. // Shared: verify Sec-WebSocket-Accept, hijack on success, hijack+close on failure.
async function fromResponse(res: ClientResponse, key: string): Promise<WebSocketConnection> { async function fromResponse(
res: ClientResponse,
key: string,
limits?: WebSocketLimits,
): Promise<WebSocketConnection> {
if (res.status !== 101) { if (res.status !== 101) {
throw new Error(`WebSocket upgrade failed: ${res.status} ${res.statusText}`) throw new Error(`WebSocket upgrade failed: ${res.status} ${res.statusText}`)
} }
@@ -28,7 +33,7 @@ async function fromResponse(res: ClientResponse, key: string): Promise<WebSocket
throw new TypeError("Invalid Sec-WebSocket-Accept header in upgrade response") throw new TypeError("Invalid Sec-WebSocket-Accept header in upgrade response")
} }
return new WebSocketConnection(res.hijack(), true) return new WebSocketConnection(res.hijack(), true, limits)
} }
// Overload 1: open a WebSocket to a ws:// or wss:// (or http:// / https://) URL. // Overload 1: open a WebSocket to a ws:// or wss:// (or http:// / https://) URL.
@@ -44,6 +49,7 @@ export async function connectWebSocket(
export async function connectWebSocket( export async function connectWebSocket(
res: ClientResponse, res: ClientResponse,
key: string, key: string,
limits?: WebSocketLimits,
): Promise<WebSocketConnection> ): Promise<WebSocketConnection>
export async function connectWebSocket( export async function connectWebSocket(
@@ -52,7 +58,7 @@ export async function connectWebSocket(
options?: WebSocketConnectOptions, options?: WebSocketConnectOptions,
): Promise<WebSocketConnection> { ): Promise<WebSocketConnection> {
if ("hijack" in dialerOrRes) { if ("hijack" in dialerOrRes) {
return fromResponse(dialerOrRes, urlOrKey as string) return fromResponse(dialerOrRes, urlOrKey as string, options)
} }
const dialer = dialerOrRes const dialer = dialerOrRes
@@ -117,5 +123,5 @@ export async function connectWebSocket(
throw new Error(`WebSocket upgrade failed: ${res.status} ${res.statusText}`) throw new Error(`WebSocket upgrade failed: ${res.status} ${res.statusText}`)
} }
return fromResponse(res, key) return fromResponse(res, key, options)
} }
+1 -1
View File
@@ -1,4 +1,4 @@
export type { WebSocketMessage } from "../common.js" export type { WebSocketLimits, WebSocketMessage } from "../common.js"
export { WebSocketConnection } from "../common.js" export { WebSocketConnection } from "../common.js"
export type { WebSocketConnectOptions } from "../client.js" export type { WebSocketConnectOptions } from "../client.js"
export { connectWebSocket } from "../client.js" export { connectWebSocket } from "../client.js"
+111 -6
View File
@@ -28,6 +28,48 @@ const OPCODE_CLOSE = 0x8
const OPCODE_PING = 0x9 const OPCODE_PING = 0x9
const OPCODE_PONG = 0xa const OPCODE_PONG = 0xa
const CLOSE_PROTOCOL_ERROR = 1002
const CLOSE_MESSAGE_TOO_BIG = 1009
/**
* Bounds on what a peer can make a connection allocate. Every limit has a
* finite default; pass `Infinity` for one to opt out of it.
*/
export type WebSocketLimits = {
/**
* Maximum payload size in bytes of a single incoming frame. Checked against
* the announced length, before the payload is read.
* @defaultValue 1 MiB
*/
maxFrameSize?: number
/**
* Maximum assembled size in bytes of an incoming message.
* @defaultValue 8 MiB
*/
maxMessageSize?: number
/**
* Maximum number of frames one incoming message may be fragmented into.
* @defaultValue 1024
*/
maxFragments?: number
}
const DEFAULT_LIMITS = {
maxFrameSize: 1024 * 1024,
maxMessageSize: 8 * 1024 * 1024,
maxFragments: 1024,
} satisfies Required<WebSocketLimits>
// A peer violated the protocol. The connection is failed with `code`.
class ProtocolError extends Error {
code: number
constructor(code: number, message: string) {
super(message)
this.code = code
}
}
type Frame = { fin: boolean; opcode: number; data: Uint8Array } type Frame = { fin: boolean; opcode: number; data: Uint8Array }
// Copy slices from ReadBuffer into a single Uint8Array. Always copies so callers can mutate. // Copy slices from ReadBuffer into a single Uint8Array. Always copies so callers can mutate.
@@ -48,7 +90,11 @@ function sliceBytes(buf: ReadBuffer, start: number, len: number): Uint8Array {
return out return out
} }
async function readFrame(buf: ReadBuffer): Promise<Frame> { async function readFrame(
buf: ReadBuffer,
maxFrameSize: number,
expectMasked: boolean,
): Promise<Frame> {
await buf.read(2) await buf.read(2)
if (buf.len < 2) throw new Error("Connection closed before frame header") if (buf.len < 2) throw new Error("Connection closed before frame header")
@@ -60,6 +106,14 @@ async function readFrame(buf: ReadBuffer): Promise<Frame> {
const masked = (b1 & 0x80) !== 0 const masked = (b1 & 0x80) !== 0
let payloadLen = b1 & 0x7f let payloadLen = b1 & 0x7f
// RFC 6455 §5.1: client frames are masked, server frames are not.
if (masked !== expectMasked) {
throw new ProtocolError(
CLOSE_PROTOCOL_ERROR,
expectMasked ? "Client frame is not masked" : "Server frame is masked",
)
}
let extLen = 0 let extLen = 0
if (payloadLen === 126) extLen = 2 if (payloadLen === 126) extLen = 2
else if (payloadLen === 127) extLen = 8 else if (payloadLen === 127) extLen = 8
@@ -71,11 +125,28 @@ async function readFrame(buf: ReadBuffer): Promise<Frame> {
if (extLen === 2) { if (extLen === 2) {
payloadLen = (lenBytes[0] << 8) | lenBytes[1] payloadLen = (lenBytes[0] << 8) | lenBytes[1]
} else { } else {
payloadLen = 0 // RFC 6455 §5.2: the most significant bit of a 64-bit length must be 0.
for (let i = 0; i < 8; i++) payloadLen = payloadLen * 256 + lenBytes[i] if (lenBytes[0] & 0x80) {
throw new ProtocolError(CLOSE_PROTOCOL_ERROR, "64-bit frame length has its high bit set")
}
const hi =
((lenBytes[0] << 24) | (lenBytes[1] << 16) | (lenBytes[2] << 8) | lenBytes[3]) >>> 0
const lo =
((lenBytes[4] << 24) | (lenBytes[5] << 16) | (lenBytes[6] << 8) | lenBytes[7]) >>> 0
payloadLen = hi * 0x1_0000_0000 + lo
if (!Number.isSafeInteger(payloadLen)) {
throw new ProtocolError(CLOSE_MESSAGE_TOO_BIG, "Frame length is not representable")
}
} }
} }
if (payloadLen > maxFrameSize) {
throw new ProtocolError(
CLOSE_MESSAGE_TOO_BIG,
`Frame payload of ${payloadLen} bytes exceeds the ${maxFrameSize} byte limit`,
)
}
const headerSize = 2 + extLen + (masked ? 4 : 0) const headerSize = 2 + extLen + (masked ? 4 : 0)
await buf.read(headerSize + payloadLen) await buf.read(headerSize + payloadLen)
if (buf.len < headerSize + payloadLen) throw new Error("Connection closed in frame payload") if (buf.len < headerSize + payloadLen) throw new Error("Connection closed in frame payload")
@@ -164,8 +235,10 @@ export class WebSocketConnection implements AsyncIterable<WebSocketMessage> {
#closeSent: boolean #closeSent: boolean
#fragmentOpcode: number #fragmentOpcode: number
#fragments: Uint8Array[] #fragments: Uint8Array[]
#fragmentSize: number
#limits: Required<WebSocketLimits>
constructor(transport: RawTransport, masked: boolean) { constructor(transport: RawTransport, masked: boolean, limits: WebSocketLimits = {}) {
this.#transport = transport this.#transport = transport
this.#buf = new ReadBuffer(transport) this.#buf = new ReadBuffer(transport)
this.#masked = masked this.#masked = masked
@@ -175,6 +248,12 @@ export class WebSocketConnection implements AsyncIterable<WebSocketMessage> {
this.#closeSent = false this.#closeSent = false
this.#fragmentOpcode = 0 this.#fragmentOpcode = 0
this.#fragments = [] this.#fragments = []
this.#fragmentSize = 0
this.#limits = {
maxFrameSize: limits.maxFrameSize ?? DEFAULT_LIMITS.maxFrameSize,
maxMessageSize: limits.maxMessageSize ?? DEFAULT_LIMITS.maxMessageSize,
maxFragments: limits.maxFragments ?? DEFAULT_LIMITS.maxFragments,
}
} }
get closed(): boolean { get closed(): boolean {
@@ -212,12 +291,19 @@ export class WebSocketConnection implements AsyncIterable<WebSocketMessage> {
} }
async *[Symbol.asyncIterator](): AsyncGenerator<WebSocketMessage> { async *[Symbol.asyncIterator](): AsyncGenerator<WebSocketMessage> {
// A client masks what it sends and reads unmasked server frames; a server is
// the other way round.
const expectMasked = !this.#masked
try { try {
while (true) { while (true) {
let frame: Frame let frame: Frame
try { try {
frame = await readFrame(this.#buf) frame = await readFrame(this.#buf, this.#limits.maxFrameSize, expectMasked)
} catch { } catch (e) {
// A misbehaving peer gets a Close frame with the violation's status
// code, but we stop reading rather than wait for it to echo it back.
if (e instanceof ProtocolError) await this.close(e.code, e.message)
break break
} }
@@ -254,15 +340,34 @@ export class WebSocketConnection implements AsyncIterable<WebSocketMessage> {
// Data frame (text, binary, or continuation) // Data frame (text, binary, or continuation)
if (frame.opcode === OPCODE_CONTINUATION) { if (frame.opcode === OPCODE_CONTINUATION) {
this.#fragments.push(frame.data) this.#fragments.push(frame.data)
this.#fragmentSize += frame.data.length
} else { } else {
this.#fragmentOpcode = frame.opcode this.#fragmentOpcode = frame.opcode
this.#fragments = [frame.data] this.#fragments = [frame.data]
this.#fragmentSize = frame.data.length
}
// Checked per frame so an oversized message is rejected while it arrives.
if (this.#fragmentSize > this.#limits.maxMessageSize) {
await this.close(
CLOSE_MESSAGE_TOO_BIG,
`Message of at least ${this.#fragmentSize} bytes exceeds the ${this.#limits.maxMessageSize} byte limit`,
)
break
}
if (this.#fragments.length > this.#limits.maxFragments) {
await this.close(
CLOSE_MESSAGE_TOO_BIG,
`Message exceeds the ${this.#limits.maxFragments} fragment limit`,
)
break
} }
if (frame.fin) { if (frame.fin) {
const opcode = this.#fragmentOpcode const opcode = this.#fragmentOpcode
const assembled = concat(this.#fragments) const assembled = concat(this.#fragments)
this.#fragments = [] this.#fragments = []
this.#fragmentSize = 0
this.#fragmentOpcode = 0 this.#fragmentOpcode = 0
if (opcode === OPCODE_TEXT) { if (opcode === OPCODE_TEXT) {
+1 -1
View File
@@ -1,3 +1,3 @@
export { upgradeWebSocket, type WebSocketUpgradeOptions } from "./server.js" export { upgradeWebSocket, type WebSocketUpgradeOptions } from "./server.js"
export { connectWebSocket, type WebSocketConnectOptions } from "./client.js" export { connectWebSocket, type WebSocketConnectOptions } from "./client.js"
export { WebSocketConnection, type WebSocketMessage } from "./common.js" export { WebSocketConnection, type WebSocketLimits, type WebSocketMessage } from "./common.js"
+3 -2
View File
@@ -1,7 +1,8 @@
import type { ServerRequest, ServerResponse } from "@webnet/http/server" import type { ServerRequest, ServerResponse } from "@webnet/http/server"
import type { WebSocketLimits } from "./common.js"
import { computeAccept, WebSocketConnection } from "./common.js" import { computeAccept, WebSocketConnection } from "./common.js"
export type WebSocketUpgradeOptions = { export type WebSocketUpgradeOptions = WebSocketLimits & {
protocols?: string[] protocols?: string[]
} }
@@ -38,5 +39,5 @@ export async function upgradeWebSocket(
// hijack() sends the 101 response and hands us the raw transport // hijack() sends the 101 response and hands us the raw transport
const transport = await res.hijack() const transport = await res.hijack()
return new WebSocketConnection(transport, false) return new WebSocketConnection(transport, false, options)
} }
+1 -1
View File
@@ -1,4 +1,4 @@
export type { WebSocketMessage } from "../common.js" export type { WebSocketLimits, WebSocketMessage } from "../common.js"
export { WebSocketConnection } from "../common.js" export { WebSocketConnection } from "../common.js"
export type { WebSocketUpgradeOptions } from "../server.js" export type { WebSocketUpgradeOptions } from "../server.js"
export { upgradeWebSocket } from "../server.js" export { upgradeWebSocket } from "../server.js"
+290 -2
View File
@@ -11,8 +11,8 @@ import {
} from "@webnet/http/client" } from "@webnet/http/client"
import { ReadBuffer } from "@webnet/transport/buffer" import { ReadBuffer } from "@webnet/transport/buffer"
import type { RawTransport } from "@webnet/transport" import type { RawTransport } from "@webnet/transport"
import { upgradeWebSocket } from "./server.js" import { upgradeWebSocket, type WebSocketUpgradeOptions } from "./server.js"
import { connectWebSocket } from "./client.js" import { connectWebSocket, type WebSocketConnectOptions } from "./client.js"
import { computeAccept, generateKey, WebSocketConnection, type WebSocketMessage } from "./common.js" import { computeAccept, generateKey, WebSocketConnection, type WebSocketMessage } from "./common.js"
const enc = new TextEncoder() const enc = new TextEncoder()
@@ -113,6 +113,158 @@ async function rawServerResponse(transport: RawTransport, response: string): Pro
await transport.close() await transport.close()
} }
// ---------------------------------------------------------------------------
// Frame-level helpers for limit and conformance tests
// ---------------------------------------------------------------------------
const OPCODE_TEXT = 0x1
const OPCODE_BINARY = 0x2
const OPCODE_CONTINUATION = 0x0
const OPCODE_CLOSE = 0x8
const MASK_KEY = new Uint8Array([0x01, 0x02, 0x03, 0x04])
// Frame header announcing `announced` payload bytes, without the payload.
// Takes a bigint so lengths beyond Number.MAX_SAFE_INTEGER can be expressed.
function header(fin: boolean, opcode: number, announced: bigint, masked: boolean): Uint8Array {
let lenField: number
let ext: Uint8Array
if (announced < 126n) {
lenField = Number(announced)
ext = new Uint8Array(0)
} else if (announced <= 0xffffn) {
lenField = 126
ext = new Uint8Array([Number((announced >> 8n) & 0xffn), Number(announced & 0xffn)])
} else {
lenField = 127
ext = new Uint8Array(8)
let n = announced
for (let i = 7; i >= 0; i--) {
ext[i] = Number(n & 0xffn)
n >>= 8n
}
}
const out = new Uint8Array(2 + ext.length + (masked ? 4 : 0))
out[0] = (fin ? 0x80 : 0x00) | opcode
out[1] = (masked ? 0x80 : 0x00) | lenField
out.set(ext, 2)
if (masked) out.set(MASK_KEY, 2 + ext.length)
return out
}
function frame(fin: boolean, opcode: number, payload: Uint8Array, masked: boolean): Uint8Array {
const head = header(fin, opcode, BigInt(payload.length), masked)
const out = new Uint8Array(head.length + payload.length)
out.set(head, 0)
for (let i = 0; i < payload.length; i++) {
out[head.length + i] = masked ? payload[i] ^ MASK_KEY[i % 4] : payload[i]
}
return out
}
async function take(buf: ReadBuffer, n: number): Promise<Uint8Array> {
await buf.read(n)
assert.ok(buf.len >= n, "unexpected end of stream")
const out = new Uint8Array(n)
let off = 0
for (const s of buf.slice(0, n)) {
out.set(s, off)
off += s.length
}
buf.forward(n)
return out
}
// Reads one frame with a short (<126 byte) payload, which is all a control
// frame can carry. Unmasks if the peer masked it.
async function readShortFrame(buf: ReadBuffer): Promise<{ opcode: number; payload: Uint8Array }> {
const head = await take(buf, 2)
const masked = (head[1] & 0x80) !== 0
const len = head[1] & 0x7f
assert.ok(len < 126, "helper only reads short frames")
const maskKey = masked ? await take(buf, 4) : null
const payload = await take(buf, len)
if (maskKey) for (let i = 0; i < payload.length; i++) payload[i] ^= maskKey[i % 4]
return { opcode: head[0] & 0x0f, payload }
}
async function readCloseCode(buf: ReadBuffer): Promise<number> {
const f = await readShortFrame(buf)
assert.strictEqual(f.opcode, OPCODE_CLOSE)
return (f.payload[0] << 8) | f.payload[1]
}
// Performs the client half of the handshake by hand against a real server, then
// hands the raw transport to clientFn so it can write frames the client API
// would never produce.
async function withRawClient(
clientFn: (transport: RawTransport, buf: ReadBuffer) => Promise<void>,
options?: WebSocketUpgradeOptions,
): Promise<WebSocketMessage[]> {
const [clientTransport, serverTransport] = loopbackTransportPair()
const serverConn = new ServerConnection(serverTransport)
const received: WebSocketMessage[] = []
await Promise.all([
serverConn.handle(async ({ req, res }) => {
const ws = await upgradeWebSocket(req, res, options)
for await (const msg of ws) received.push(msg)
}),
(async () => {
await clientTransport.write(
enc.encode(
"GET / HTTP/1.1\r\nHost: localhost\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n" +
`Sec-WebSocket-Key: ${generateKey()}\r\nSec-WebSocket-Version: 13\r\n\r\n`,
),
)
const buf = new ReadBuffer(clientTransport)
while ((await buf.readLine()) !== "") {
/* drain the 101 response headers */
}
await clientFn(clientTransport, buf)
await clientTransport.close()
})(),
])
return received
}
// Serves the server half of the handshake by hand, then hands the raw transport
// to serverFn. The client is a real connectWebSocket connection.
async function withRawServer(
serverFn: (transport: RawTransport, buf: ReadBuffer) => Promise<void>,
clientFn: (ws: WebSocketConnection) => Promise<void>,
options?: WebSocketConnectOptions,
): Promise<void> {
const [listener, dialer] = loopbackListener()
await Promise.all([
(async () => {
const t = await listener.accept()
listener.close()
const buf = new ReadBuffer(t)
let key = ""
let line: string
while ((line = await buf.readLine()) !== "") {
if (line.toLowerCase().startsWith("sec-websocket-key:"))
key = line.slice(line.indexOf(":") + 1).trim()
}
await t.write(
enc.encode(
"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n" +
`Sec-WebSocket-Accept: ${await computeAccept(key)}\r\n\r\n`,
),
)
await serverFn(t, buf)
await t.close()
})(),
(async () => {
const ws = await connectWebSocket(dialer, "ws://localhost/", options)
await clientFn(ws)
})(),
])
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Tests // Tests
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -933,6 +1085,142 @@ suite("WebSocket", () => {
}) })
}) })
suite("limits and conformance", () => {
test("server rejects an oversized frame before reading its payload", async () => {
let code = 0
await withRawClient(async (t, buf) => {
// Announce 4 MiB against the 1 MiB default, and send none of it.
await t.write(header(true, OPCODE_BINARY, 4n * 1024n * 1024n, true))
code = await readCloseCode(buf)
})
assert.strictEqual(code, 1009)
})
test("maxFrameSize is configurable and admits a frame at the limit", async () => {
let code = 0
const received = await withRawClient(
async (t, buf) => {
await t.write(frame(true, OPCODE_TEXT, enc.encode("12345678"), true))
await t.write(frame(true, OPCODE_TEXT, enc.encode("123456789"), true))
code = await readCloseCode(buf)
},
{ maxFrameSize: 8 },
)
assert.deepStrictEqual(received, [{ type: "text", data: "12345678" }])
assert.strictEqual(code, 1009)
})
test("client rejects a frame larger than maxFrameSize", async () => {
let code = 0
await withRawServer(
async (t, buf) => {
await t.write(header(true, OPCODE_BINARY, 64n, false))
code = await readCloseCode(buf)
},
async (ws) => {
for await (const _ of ws) {
/* drain */
}
},
{ maxFrameSize: 32 },
)
assert.strictEqual(code, 1009)
})
test("fragmented message over maxMessageSize is rejected mid-stream", async () => {
let code = 0
const chunk = new Uint8Array(100)
await withRawClient(
async (t, buf) => {
// 100 and 200 bytes are within the limit; the third fragment passes it,
// and is rejected without a final frame ever arriving.
await t.write(frame(false, OPCODE_BINARY, chunk, true))
await t.write(frame(false, OPCODE_CONTINUATION, chunk, true))
await t.write(frame(false, OPCODE_CONTINUATION, chunk, true))
code = await readCloseCode(buf)
},
{ maxMessageSize: 200 },
)
assert.strictEqual(code, 1009)
})
test("message over maxFragments is rejected", async () => {
let code = 0
const chunk = new Uint8Array(1)
await withRawClient(
async (t, buf) => {
await t.write(frame(false, OPCODE_BINARY, chunk, true))
for (let i = 0; i < 3; i++) {
await t.write(frame(false, OPCODE_CONTINUATION, chunk, true))
}
code = await readCloseCode(buf)
},
{ maxFragments: 3 },
)
assert.strictEqual(code, 1009)
})
test("server fails an unmasked client frame", async () => {
let code = 0
await withRawClient(async (t, buf) => {
await t.write(frame(true, OPCODE_TEXT, enc.encode("hello"), false))
code = await readCloseCode(buf)
})
assert.strictEqual(code, 1002)
})
test("client fails a masked server frame", async () => {
let code = 0
const received: WebSocketMessage[] = []
await withRawServer(
async (t, buf) => {
await t.write(frame(true, OPCODE_TEXT, enc.encode("hello"), true))
code = await readCloseCode(buf)
},
async (ws) => {
for await (const msg of ws) received.push(msg)
},
)
assert.strictEqual(code, 1002)
assert.deepStrictEqual(received, [])
})
test("server rejects a 64-bit length with its high bit set", async () => {
let code = 0
await withRawClient(async (t, buf) => {
await t.write(header(true, OPCODE_BINARY, 1n << 63n, true))
code = await readCloseCode(buf)
})
assert.strictEqual(code, 1002)
})
test("a length beyond Number.MAX_SAFE_INTEGER is rejected even without a frame limit", async () => {
let code = 0
await withRawClient(
async (t, buf) => {
await t.write(header(true, OPCODE_BINARY, (1n << 62n) + 1n, true))
code = await readCloseCode(buf)
},
{ maxFrameSize: Infinity },
)
assert.strictEqual(code, 1009)
})
})
suite("send / ping errors after close", () => { suite("send / ping errors after close", () => {
test("send() throws after close() is called", async () => { test("send() throws after close() is called", async () => {
await withWebSocketPair( await withWebSocketPair(