Commit5bdbe0c9Recorded3 Sep 2026Repositorysigil-websocket
Bound native WebSocket connection setup
Changed
CHANGELOG.md | 12 ++++++++++++
package.sgl | 2 +-
src/sigil/websocket/connection.sgl | 45 ++++++++++++++++++++++++++++++++-------------
test/integration/native-upgrade-timeout-client.sgl | 19 +++++++++++++++++++
test/integration/test-native-websocket-upgrade-timeout.mjs | 40 ++++++++++++++++++++++++++++++++++++++++
test/integration/test-native-websocket-upgrade-timeout.sh | 3 +++
6 files changed, 107 insertions(+), 14 deletions(-)Diff
CHANGELOG.mdmodified
@@ -7,6 +7,18 @@ All notable changes to this project will be documented in this file.
7
The format is based on [Keep a Changelog](https://keepachangelog.com/), 8
and this project adheres to [Semantic Versioning](https://semver.org/). 9
+10
## [0.10.9] - 2026-09-03+11
+12
### Added+13
+14
- Optional native WebSocket deadlines for TCP/TLS connect, TLS handshake, and+15
the HTTP Upgrade response.+16
+17
### Fixed+18
+19
- Make a bounded HTTP Upgrade read nonblocking and scheduler-cooperative so a+20
peer that accepts TLS and then goes silent cannot stall its caller forever.+21
22
## [0.10.8] - 2026-09-01 23
24
### Fixedpackage.sglmodified
@@ -8,7 +8,7 @@
8
9
(package 10
name: "sigil-websocket"−11
version: "0.10.8"+11
version: "0.10.9" 12
sigil: "0.20" 13
description: "WebSocket protocol library — client + server, with TLS" 14
url: "https://codeberg.org/sigil/sigil-websocket"src/sigil/websocket/connection.sglmodified
@@ -8,6 +8,7 @@
8
(sigil string) 9
(sigil io) 10
(sigil struct)+11
(only (sigil math) max) 12
(sigil async)) 13
(cond-expand 14
(wasm@@ -178,11 +179,13 @@
179
180
;;; Read HTTP response and validate handshake 181
;;; Returns #t on success, #f on failure−181
(define (validate-handshake conn expected-accept)+182
(define (validate-handshake conn expected-accept timeout-ms) 183
(let ((socket (ws-connection-socket conn))) 184
;; Read response (accumulate until we see \r\n\r\n). Keep the 185
;; accumulator binary: a server may coalesce its first WebSocket frame 186
;; with the 101 response, and that tail belongs to ws-receive.+187
(let ((deadline (and timeout-ms+188
(+ (current-milliseconds) timeout-ms)))) 189
(let loop ((data (make-bytevector 0))) 190
(let ((chunk (conn-read socket 4096))) 191
(if (or (not chunk) (eof-object? chunk))@@ -223,11 +226,18 @@
226
;; nonblocking, so an empty read means EAGAIN rather than 227
;; EOF. Yield until the fd is readable instead of spinning 228
;; and starving the server that will finish the upgrade.−226
(begin−227
(when (and (current-scheduler)−228
(= (bytevector-length chunk-bytes) 0))−229
(await-readable socket))−230
(loop response)))))))))+229
(if (and deadline+230
(>= (current-milliseconds) deadline))+231
#f+232
(begin+233
(when (= (bytevector-length chunk-bytes) 0)+234
(if deadline+235
(await-readable-any+236
(list socket)+237
timeout-ms: (max 1+238
(- deadline (current-milliseconds))))+239
(await-readable socket)))+240
(loop response))))))))))) 241
242
;; ============================================================ 243
;; Connection Helpers@@ -305,8 +315,11 @@
315
;;; Connect to a WebSocket server 316
;;; url: WebSocket URL (ws:// or wss://) 317
;;; Returns ws-connection on success, #f on failure−308
(define (ws-connect url)−309
(: string? -> any?)+318
(define (ws-connect url (keys: (connect-timeout-ms #f)+319
(handshake-timeout-ms #f)+320
(upgrade-timeout-ms #f)))+321
(: string? (connect-timeout-ms: any?) (handshake-timeout-ms: any?)+322
(upgrade-timeout-ms: any?) -> any?) 323
(let ((parsed (parse-ws-url url))) 324
(if (not parsed) 325
#f@@ -329,7 +342,11 @@
342
(path (cadddr parsed)) 343
;; Connect using TLS for wss:// 344
(socket (if (string=? scheme "wss")−332
(tls-connect host port)+345
(if (or connect-timeout-ms handshake-timeout-ms)+346
(tls-connect host port+347
(or connect-timeout-ms 10000)+348
(or handshake-timeout-ms 10000))+349
(tls-connect host port)) 350
(tcp-connect host port)))) 351
(if (not socket) 352
#f@@ -343,15 +360,17 @@
360
(key (generate-ws-key)) 361
(expected-accept (calculate-accept-key key)) 362
(request (build-handshake-request host path key)))+363
;; Make the HTTP upgrade read cooperative before it can+364
;; wait for a peer that accepted TLS and then went quiet.+365
(when (or (current-scheduler) upgrade-timeout-ms)+366
(conn-set-non-blocking! socket #t)) 367
;; Send handshake request 368
(conn-write socket request) 369
;; Validate response−349
(if (validate-handshake conn expected-accept)+370
(if (validate-handshake conn expected-accept+371
upgrade-timeout-ms) 372
(begin 373
(set-ws-connection-state! conn 'open)−352
;; Set to non-blocking for async I/O when in scheduler−353
(when (current-scheduler)−354
(conn-set-non-blocking! socket #t)) 374
conn) 375
(begin 376
(conn-close socket)test/integration/native-upgrade-timeout-client.sgladded
@@ -0,0 +1,19 @@
+1
(import (sigil core)+2
(sigil websocket))+3
+4
(define port+5
(or (and (getenv "SIGIL_WS_TEST_PORT")+6
(string->number (getenv "SIGIL_WS_TEST_PORT")))+7
(error "native upgrade-timeout client requires a port")))+8
(define started (current-milliseconds))+9
(define conn+10
(ws-connect (string-append "ws://127.0.0.1:"+11
(number->string port) "/")+12
upgrade-timeout-ms: 250))+13
(define elapsed (- (current-milliseconds) started))+14
+15
(when conn (error "silent HTTP upgrade unexpectedly succeeded"))+16
(unless (and (>= elapsed 150) (< elapsed 2000))+17
(error "HTTP upgrade timeout was not bounded" elapsed))+18
(display "native-websocket-upgrade-timeout-ok")+19
(newline)test/integration/test-native-websocket-upgrade-timeout.mjsadded
@@ -0,0 +1,40 @@
+1
import net from "node:net";+2
import path from "node:path";+3
import { fileURLToPath } from "node:url";+4
import { spawn } from "node:child_process";+5
+6
const here = path.dirname(fileURLToPath(import.meta.url));+7
const repo = path.resolve(here, "../..");+8
const server = net.createServer((socket) => {+9
// Consume the request but deliberately never send the HTTP 101 response.+10
socket.on("data", () => {});+11
});+12
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));+13
const port = server.address().port;+14
+15
const dependencyRoots = [+16
"sigil-stdlib", "sigil-socket", "sigil-tls", "sigil-crypto",+17
].map((name) => path.join(repo, ".sigil", "deps", name, "src"));+18
const args = [+19
"-L", path.join(repo, "build/dev/lib"),+20
...dependencyRoots.flatMap((root) => ["-L", root]),+21
path.join(here, "native-upgrade-timeout-client.sgl"),+22
];+23
const child = spawn("sigil", args, {+24
cwd: "/tmp",+25
env: { ...process.env, SIGIL_WS_TEST_PORT: String(port) },+26
stdio: ["ignore", "pipe", "pipe"],+27
});+28
let stdout = "";+29
let stderr = "";+30
child.stdout.on("data", (chunk) => { stdout += chunk; });+31
child.stderr.on("data", (chunk) => { stderr += chunk; });+32
const watchdog = setTimeout(() => child.kill("SIGKILL"), 3000);+33
const status = await new Promise((resolve) => child.on("close", resolve));+34
clearTimeout(watchdog);+35
server.close();+36
+37
if (status !== 0 || !stdout.includes("native-websocket-upgrade-timeout-ok")) {+38
throw new Error(`native timeout client failed (${status})\nstdout:\n${stdout}\nstderr:\n${stderr}`);+39
}+40
console.log("native-websocket-upgrade-timeout-integration-ok");test/integration/test-native-websocket-upgrade-timeout.shadded
@@ -0,0 +1,3 @@
+1
#!/usr/bin/env bash+2
set -eu+3
exec node test/integration/test-native-websocket-upgrade-timeout.mjs