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
### Fixed
package.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
}