Fix live-stream SSE endpoint 500 by passing an explicit broadcast handler
stream-events-handler called http-response/sse-broadcast while omitting the broadcast handler positional, relying on it defaulting to identity. That default only exists in the . opts form (sigil-http 0.16.7+). Releases 0.16.1-0.16.6 typed the handler as a required positional; omitting it (and jumping straight to the on-connect: keyword) crashed the SSE connect with "car: expected pair" - a 500 on GET /feed, most visible when the server runs interpreted and loads one of those releases.
Pass identity explicitly. It is correct for every release: the hub carries pre-formatted SSE strings so identity is the right handler, and the . opts form accepts a leading procedure too. Bumps to 0.16.1 and adds a regression test for the live-routes -> handler call contract.
CHANGELOG.md | 13 +++++++++++++
package.sgl | 2 +-
src/sigil/web/live.sgl | 9 +++++++++
test/test-live-sse-repro.sgl | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 76 insertions(+), 1 deletion(-)CHANGELOG.mdmodified
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).## [0.16.1] - 2026-07-10### Fixed- Live-stream SSE endpoints (`live-routes`) no longer return `500 Internal Server Error` with `car: expected pair` when a client connects. `stream-events-handler` now passes the broadcast handler (`identity`) explicitly to `http-response/sse-broadcast` instead of relying on its default. The stream hub carries pre-formatted SSE strings, so `identity` is the correct handler; passing it explicitly keeps `GET` on a live-stream endpoint working across every `sigil-http` release, including ones that type the broadcast handler as a required positional argument.## [0.16.0] - 2026-07-10### Addedpackage.sglmodified
(package name: "sigil-web" version: "0.16.0" version: "0.16.1" sigil: "^0.17" description: "Web application framework for Sigil" url: "https://codeberg.org/sigil/sigil-web"src/sigil/web/live.sglmodified
(when (channel-try-send (live-stream-rec-heartbeat-latch stream) 'started) (start-sse-heartbeat! (live-stream-rec-hub stream)))) ;; The stream hub already carries pre-formatted SSE strings (live-push! ;; broadcasts `ui-*` update strings verbatim), so the broadcast handler is ;; `identity`. We pass it EXPLICITLY rather than relying on the default: ;; some `sigil-http` releases (0.16.1 through 0.16.6) typed the handler as a ;; *required* positional, and omitting it there triggers a keyword-dispatch ;; crash ("car: expected pair") instead of using the identity default. ;; Passing it explicitly is correct for every release: the `. opts` form ;; (0.16.7+) also accepts a leading procedure as the handler. (define (stream-events-handler stream) (lambda (request) (ensure-heartbeat! stream) (http-response/sse-broadcast (live-stream-rec-hub stream) identity on-connect: (live-stream-rec-on-connect stream)))) ;;; Return a handler that serves the SSE endpoint for a stream or atest/test-live-sse-repro.sgladded
;; Regression test for the live-stream SSE endpoint 500 ("car: expected pair").;;;; `stream-events-handler` called `http-response/sse-broadcast` while OMITTING;; the broadcast handler positional, relying on it defaulting to the identity;; function. That default only exists in the `. opts` form of;; `http-response/sse-broadcast` (sigil-http 0.16.7+). In releases 0.16.1–0.16.6;; the signature was a `keys:` form with `handler` as a *required* positional;;; omitting it (and jumping to the `on-connect:` keyword) crashed the SSE connect;; with `type-error: car: expected pair` — a 500 on GET /feed. The fix passes the;; identity handler EXPLICITLY, which is valid for every release.;;;; NOTE: fully invoking the handler starts the lazy heartbeat via `(go ...)`,;; which needs a live scheduler and never terminates, so we can't drive it from;; a plain `sigil test`. Instead we pin the two things the fix actually depends;; on: (1) the exact `http-response/sse-broadcast` call SHAPE the handler now;; uses builds a well-formed streaming SSE response, and (2) `live-routes` wires;; a handler for both a stream and a collection. End-to-end proof (GET /feed ->;; 200 text/event-stream on a real server) is recorded in the task note.(import (sigil test) (sigil core) (sigil channels) (sigil web live) (sigil web routes) (sigil http request) (sigil http response))(test-group "live-stream SSE endpoint (regression: /feed 500)" ;; This is the call shape stream-events-handler now emits. Under the keys-form ;; sigil-http releases this exact shape is what avoids the "car: expected pair" ;; crash (the handler positional is supplied); under the `. opts` form it is the ;; identity handler, matching the previous default behaviour. (test "sse-broadcast with an explicit identity handler + on-connect: builds an SSE response" (let* ((hub (make-broadcast)) (resp (http-response/sse-broadcast hub identity on-connect: #f))) (assert-true (http-response? resp)) (assert-equal 200 (http-response-status resp)) ;; streaming responses carry a procedure body (write-chunk close) ... (assert-true (procedure? (http-response-body resp))) ;; ... and advertise the SSE content type. (assert-equal "text/event-stream" (dict-ref (http-response-headers resp) content-type:)))) (test "live-routes wires a handler for a plain stream" (let* ((feed (live-stream path: "/feed")) (app (live-routes feed))) (assert-true (procedure? app)))) (test "live-routes wires a handler for a collection" (let* ((coll (live-collection render: (lambda (item) `(li ,(dict-ref item text:))) container: "#list" path: "/events")) (app (live-routes coll))) (assert-true (procedure? app)))))