Commitd8caad28Recorded3 Sep 2026Repositorysigil-websocket
Bound plain WebSocket TCP connects
Changed
CHANGELOG.md | 8 ++++++++
package.sgl | 2 +-
src/sigil/websocket/connection.sgl | 32 +++++++++++++++++++++++++++++++-
test/integration/native-connect-timeout-client.sgl | 17 +++++++++++++++++
test/integration/test-native-websocket-connect-timeout.mjs | 29 +++++++++++++++++++++++++++++
test/integration/test-native-websocket-connect-timeout.sh | 3 +++
6 files changed, 89 insertions(+), 2 deletions(-)Diff
CHANGELOG.mdmodified
@@ -7,6 +7,14 @@ 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.10] - 2026-09-03+11
+12
### Fixed+13
+14
- Apply `connect-timeout-ms:` to plain `ws://` TCP setup as well as TLS+15
connections, so a blackholed peer cannot indefinitely block retry or relay+16
fallback.+17
18
## [0.10.9] - 2026-09-03 19
20
### Addedpackage.sglmodified
@@ -8,7 +8,7 @@
8
9
(package 10
name: "sigil-websocket"−11
version: "0.10.9"+11
version: "0.10.10" 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
@@ -49,6 +49,35 @@
49
50
(begin 51
+52
;; Native sigil-socket exposes the bounded per-address connector as a+53
;; primitive, while `tcp-connect` remains the compatibility surface. Keep+54
;; WebSocket's public connect deadline honest for plain ws:// too: without+55
;; this, a blackholed SYN never returns and neither the HTTP-upgrade timeout+56
;; nor a caller's retry/fallback state machine can run.+57
(cond-expand+58
(wasm)+59
(else+60
(define-native (%tcp-connect-ip-timeout ip port timeout-ms)+61
(: string? integer? integer? -> any?))))+62
+63
(define (tcp-connect/bounded host port timeout-ms)+64
(if (not timeout-ms)+65
(tcp-connect host port)+66
(let* ((resolved (resolve-hostname host))+67
;; getaddrinfo is accumulated with cons by sigil-socket.+68
(addresses (cond ((not resolved) '())+69
((string? resolved) (list resolved))+70
(else (reverse resolved))))+71
(deadline (+ (current-milliseconds) timeout-ms)))+72
(let loop ((xs addresses))+73
(if (null? xs)+74
#f+75
(let ((remaining (- deadline (current-milliseconds))))+76
(and (> remaining 0)+77
(or (guard (e (#t #f))+78
(%tcp-connect-ip-timeout (car xs) port remaining))+79
(loop (cdr xs))))))))))+80
81
;; ============================================================ 82
;; WebSocket GUID (RFC 6455) 83
;; ============================================================@@ -347,7 +376,8 @@
376
(or connect-timeout-ms 10000) 377
(or handshake-timeout-ms 10000)) 378
(tls-connect host port))−350
(tcp-connect host port))))+379
(tcp-connect/bounded host port+380
connect-timeout-ms)))) 381
(if (not socket) 382
#f 383
(let* ((conn (ws-connectiontest/integration/native-connect-timeout-client.sgladded
@@ -0,0 +1,17 @@
+1
(import (sigil core)+2
(sigil websocket))+3
+4
;; TEST-NET-3 is non-routable public documentation space. A host may reject it+5
;; immediately; it must never hold ws-connect beyond the caller's deadline.+6
(define started (current-milliseconds))+7
(define conn+8
(ws-connect "ws://203.0.113.1:32644/"+9
connect-timeout-ms: 250+10
upgrade-timeout-ms: 250))+11
(define elapsed (- (current-milliseconds) started))+12
+13
(when conn (error "documentation endpoint unexpectedly connected"))+14
(unless (< elapsed 2000)+15
(error "plain WebSocket TCP connect was not bounded" elapsed))+16
(display "native-websocket-connect-timeout-ok")+17
(newline)test/integration/test-native-websocket-connect-timeout.mjsadded
@@ -0,0 +1,29 @@
+1
import path from "node:path";+2
import { fileURLToPath } from "node:url";+3
import { spawn } from "node:child_process";+4
+5
const here = path.dirname(fileURLToPath(import.meta.url));+6
const repo = path.resolve(here, "../..");+7
const dependencyRoots = [+8
"sigil-stdlib", "sigil-socket", "sigil-tls", "sigil-crypto",+9
].map((name) => path.join(repo, ".sigil", "deps", name, "src"));+10
const args = [+11
"-L", path.join(repo, "build/dev/lib"),+12
...dependencyRoots.flatMap((root) => ["-L", root]),+13
path.join(here, "native-connect-timeout-client.sgl"),+14
];+15
const child = spawn("sigil", args, {+16
cwd: "/tmp", env: process.env, stdio: ["ignore", "pipe", "pipe"],+17
});+18
let stdout = "";+19
let stderr = "";+20
child.stdout.on("data", (chunk) => { stdout += chunk; });+21
child.stderr.on("data", (chunk) => { stderr += chunk; });+22
const watchdog = setTimeout(() => child.kill("SIGKILL"), 3000);+23
const status = await new Promise((resolve) => child.on("close", resolve));+24
clearTimeout(watchdog);+25
+26
if (status !== 0 || !stdout.includes("native-websocket-connect-timeout-ok")) {+27
throw new Error(`native timeout client failed (${status})\nstdout:\n${stdout}\nstderr:\n${stderr}`);+28
}+29
console.log("native-websocket-connect-timeout-integration-ok");test/integration/test-native-websocket-connect-timeout.shadded
@@ -0,0 +1,3 @@
+1
#!/usr/bin/env bash+2
set -euo pipefail+3
exec node test/integration/test-native-websocket-connect-timeout.mjs