Commit2b0f82a1Recorded8 Sep 2026Repositorysigil-websocket
Release sigil-websocket 0.10.13 with complete TLS frame writes
Changed
CHANGELOG.md | 16 ++++++++++++++++
package.sgl | 2 +-
src/sigil/websocket/connection.sgl | 43 +++++++++++++++++++++----------------------
test/integration/short-socket-writes.c | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 93 insertions(+), 23 deletions(-)Diff
CHANGELOG.mdmodified
@@ -7,6 +7,22 @@ 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.13] - 2026-09-08+11
+12
### Fixed+13
+14
- Complete nonblocking TLS WebSocket writes after short writes or backpressure,+15
preserving the encoded frame and retrying only bytes not yet committed.+16
Previously the TLS path could discard the remainder of a large frame, causing+17
remote file and image transfers to stall or fail.+18
- Count native string writes in UTF-8 bytes and stop retrying fatal write errors.+19
+20
### Tests+21
+22
- Add a Linux socket instrument that forces short writes and would-block results+23
for isolated TLS regressions. Slate's browser integration verified complete+24
1.6 MB and 5.8 MB image transfers, including byte hashes and forced backpressure.+25
26
## [0.10.12] - 2026-09-07 27
28
### Fixedpackage.sglmodified
@@ -8,7 +8,7 @@
8
9
(package 10
name: "sigil-websocket"−11
version: "0.10.12"+11
version: "0.10.13" 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
@@ -280,33 +280,32 @@
280
;; Connection Helpers 281
;; ============================================================ 282
−283
;;; Write one complete WebSocket frame. Plain scheduler-owned sockets are−284
;;; nonblocking after the handshake, so a successful socket-write may cover−285
;;; only a prefix. Preserve the already-encoded (and already-masked) frame,−286
;;; wait for writability, and resume at its byte offset.+283
;;; Write one complete WebSocket frame. Both plain and TLS sockets may+284
;;; accept only a prefix when nonblocking. Keep the encoded (and masked)+285
;;; bytes intact and resume at the committed offset after backpressure. 286
(define (conn-write sock data) 287
(cond-expand 288
(wasm 289
(wasm-websocket-send sock data)) 290
(else−292
(if (tls-connection? sock)−293
(tls-write sock data)−294
(let ((len (if (string? data) (string-length data)−295
(bytevector-length data))))−296
(let loop ((offset 0))−297
(if (>= offset len)−298
#t−299
;; Offset arguments are present in the native socket ABI;−300
;; apply tolerates the older stdlib arity annotation.−301
(let ((sent (apply socket-write−302
(list sock data offset len))))−303
(cond−304
((and (integer? sent) (> sent 0))−305
(loop (+ offset sent)))−306
((and (current-scheduler) (not (socket-closed? sock)))−307
(await-writable sock)−308
(loop offset))−309
(else #f))))))))))+291
(let* ((bytes (if (string? data) (string->utf8 data) data))+292
(len (bytevector-length bytes))+293
(secure? (tls-connection? sock))+294
(write (if secure? tls-write socket-write))+295
(closed? (if secure? tls-closed? socket-closed?)))+296
(let loop ((offset 0))+297
(if (>= offset len)+298
#t+299
;; Offset arguments exist in both native ABIs. Apply also+300
;; supports the older two-argument arity annotations.+301
(let ((sent (apply write (list sock bytes offset len))))+302
(cond+303
((and (integer? sent) (> sent 0))+304
(loop (+ offset sent)))+305
((and (integer? sent) (= sent 0) (not (closed? sock)))+306
(await-writable sock)+307
(loop offset))+308
(else #f))))))))) 309
310
;;; Read from socket (handles both TCP and TLS). 311
;;;test/integration/short-socket-writes.cadded
@@ -0,0 +1,55 @@
+1
/* Linux test instrument: force short writes and EAGAIN on nonblocking+2
* stream sockets. Run ONLY in an isolated client via LD_PRELOAD.+3
* cc -shared -fPIC -o short-socket-writes.so short-socket-writes.c -ldl+4
* SIGIL_SHORT_WRITE_LOG optionally records counts, never payload bytes. */+5
#define _GNU_SOURCE+6
#include <dlfcn.h>+7
#include <sys/socket.h>+8
#include <fcntl.h>+9
#include <errno.h>+10
#include <unistd.h>+11
#include <stdio.h>+12
#include <stdlib.h>+13
+14
static unsigned short_writes, would_block;+15
static int constrain(int fd, size_t *length) {+16
int type;+17
socklen_t size = sizeof(type);+18
if (*length <= 4096 || !(fcntl(fd, F_GETFL) & O_NONBLOCK) ||+19
getsockopt(fd, SOL_SOCKET, SO_TYPE, &type, &size) != 0 ||+20
type != SOCK_STREAM) return 0;+21
if (short_writes > would_block) {+22
++would_block;+23
errno = EAGAIN;+24
return -1;+25
}+26
++short_writes;+27
*length = 4096;+28
return 0;+29
}+30
+31
ssize_t send(int fd, const void *bytes, size_t length, int flags) {+32
static ssize_t (*native_send)(int, const void *, size_t, int);+33
if (!native_send) native_send = dlsym(RTLD_NEXT, "send");+34
if (constrain(fd, &length) < 0) return -1;+35
return native_send(fd, bytes, length, flags);+36
}+37
+38
ssize_t write(int fd, const void *bytes, size_t length) {+39
static ssize_t (*native_write)(int, const void *, size_t);+40
if (!native_write) native_write = dlsym(RTLD_NEXT, "write");+41
if (constrain(fd, &length) < 0) return -1;+42
return native_write(fd, bytes, length);+43
}+44
+45
__attribute__((destructor)) static void record_counts(void) {+46
const char *path = getenv("SIGIL_SHORT_WRITE_LOG");+47
if (!path || (!short_writes && !would_block)) return;+48
int fd = open(path, O_WRONLY | O_CREAT | O_APPEND, 0600);+49
if (fd < 0) return;+50
char line[128];+51
int length = snprintf(line, sizeof(line), "%ld %u %u\n",+52
(long)getpid(), short_writes, would_block);+53
if (length > 0 && (size_t)length < sizeof(line)) write(fd, line, length);+54
close(fd);+55
}