dream: error-handling middleware (dream-catch) + status reason phrases + 15 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 48s

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-07 15:03:17 +00:00
parent 078872728e
commit 17ef5f50b3
4 changed files with 142 additions and 2 deletions

View File

@@ -30,6 +30,7 @@ MODULES=(
"lib/dream/form.sx"
"lib/dream/websocket.sx"
"lib/dream/static.sx"
"lib/dream/error.sx"
"lib/dream/run.sx"
"lib/dream/demos/hello.sx"
"lib/dream/demos/counter.sx"
@@ -47,6 +48,7 @@ SUITES=(
"form dream-fo-tests-run! lib/dream/tests/form.sx"
"websocket dream-ws-tests-run! lib/dream/tests/websocket.sx"
"static dream-st-tests-run! lib/dream/tests/static.sx"
"error dream-er-tests-run! lib/dream/tests/error.sx"
"run dream-rn-tests-run! lib/dream/tests/run.sx"
"demos dream-dm-tests-run! lib/dream/tests/demos.sx"
)

41
lib/dream/error.sx Normal file
View File

@@ -0,0 +1,41 @@
;; lib/dream/error.sx — Dream-on-SX status phrases + error-handling middleware.
;; dream-catch wraps a handler and turns a raised error into a 500 response (or a
;; custom page). Depends on types.sx.
;; ── status reason phrases ──────────────────────────────────────────
(define dr/status-texts {:206 "Partial Content" :202 "Accepted" :422 "Unprocessable Entity" :400 "Bad Request" :302 "Found" :204 "No Content" :502 "Bad Gateway" :429 "Too Many Requests" :301 "Moved Permanently" :415 "Unsupported Media Type" :405 "Method Not Allowed" :303 "See Other" :401 "Unauthorized" :304 "Not Modified" :503 "Service Unavailable" :404 "Not Found" :308 "Permanent Redirect" :504 "Gateway Timeout" :416 "Range Not Satisfiable" :500 "Internal Server Error" :307 "Temporary Redirect" :201 "Created" :501 "Not Implemented" :409 "Conflict" :200 "OK" :410 "Gone" :403 "Forbidden"})
(define
dream-status-text
(fn (status) (or (get dr/status-texts (str status)) "Unknown")))
(define
dream-status-line
(fn (status) (str status " " (dream-status-text status))))
;; ── error-handling middleware ──────────────────────────────────────
(define
dream-default-error-page
(fn
(req e)
(dream-html-status
500
(str "<h1>" (dream-status-line 500) "</h1>"))))
(define
dream-catch-with
(fn
(on-error)
(fn
(next)
(fn (req) (guard (e (true (on-error req e))) (next req))))))
(define dream-catch (dream-catch-with dream-default-error-page))
;; a fallback handler that renders a status page for any code
(define
dream-status-page
(fn
(status)
(dream-html-status
status
(str "<h1>" (dream-status-line status) "</h1>"))))

90
lib/dream/tests/error.sx Normal file
View File

@@ -0,0 +1,90 @@
;; lib/dream/tests/error.sx — status phrases + dream-catch.
(define dream-er-pass 0)
(define dream-er-fail 0)
(define dream-er-fails (list))
(define
dream-er-test
(fn
(name actual expected)
(if
(= actual expected)
(set! dream-er-pass (+ dream-er-pass 1))
(begin
(set! dream-er-fail (+ dream-er-fail 1))
(append! dream-er-fails {:name name :actual actual :expected expected})))))
;; ── status phrases ─────────────────────────────────────────────────
(dream-er-test "200 OK" (dream-status-text 200) "OK")
(dream-er-test "404 Not Found" (dream-status-text 404) "Not Found")
(dream-er-test
"405 phrase"
(dream-status-text 405)
"Method Not Allowed")
(dream-er-test
"500 phrase"
(dream-status-text 500)
"Internal Server Error")
(dream-er-test "unknown phrase" (dream-status-text 599) "Unknown")
(dream-er-test "status line" (dream-status-line 404) "404 Not Found")
(dream-er-test
"status page status"
(dream-status (dream-status-page 403))
403)
(dream-er-test
"status page body"
(dream-resp-body (dream-status-page 403))
"<h1>403 Forbidden</h1>")
;; ── dream-catch ────────────────────────────────────────────────────
(define dream-er-boom (fn (req) (error "kaboom")))
(define dream-er-ok (fn (req) (dream-text "fine")))
(dream-er-test
"catch normal passes through"
(dream-resp-body
((dream-catch dream-er-ok) (dream-request "GET" "/" {} "")))
"fine")
(dream-er-test
"catch error -> 500"
(dream-status
((dream-catch dream-er-boom) (dream-request "GET" "/" {} "")))
500)
(dream-er-test
"catch 500 body"
(dream-resp-body
((dream-catch dream-er-boom) (dream-request "GET" "/" {} "")))
"<h1>500 Internal Server Error</h1>")
;; custom error page receives the error
(define
dream-er-custom
(dream-catch-with (fn (req e) (dream-text (str "ERR:" e)))))
(dream-er-test
"custom error page"
(dream-resp-body
((dream-er-custom dream-er-boom) (dream-request "GET" "/" {} "")))
"ERR:kaboom")
(dream-er-test
"custom passes normal through"
(dream-resp-body
((dream-er-custom dream-er-ok) (dream-request "GET" "/" {} "")))
"fine")
;; catch composes around a router
(define
dream-er-app
(dream-catch
(dream-router
(list (dream-get "/boom" dream-er-boom) (dream-get "/ok" dream-er-ok)))))
(dream-er-test
"router error caught"
(dream-status (dream-er-app (dream-request "GET" "/boom" {} "")))
500)
(dream-er-test
"router ok intact"
(dream-resp-body (dream-er-app (dream-request "GET" "/ok" {} "")))
"fine")
(define dream-er-tests-run! (fn () {:total (+ dream-er-pass dream-er-fail) :passed dream-er-pass :failed dream-er-fail :fails dream-er-fails}))

View File

@@ -92,6 +92,13 @@ with extensions + hardening below.
in this worktree every `sx-tree` *edit* tool (`sx_replace_node`,
`sx_replace_by_pattern`, `sx_insert_near`) raises a yojson `Expected string, got
null` error — only `sx_write_file` works, so edits rewrite the whole file.
- **2026-06-07 — Ext: error handling + status phrases** (`lib/dream/error.sx`, 15
tests, 282 total). `dream-status-text` / `dream-status-line` reason-phrase map (string
keys); `dream-status-page` renders a status page. `dream-catch` is a `guard`-based
middleware that turns a raised error into a 500 (`dream-catch-with on-error` for a
custom page receiving `(req e)`); normal responses pass through untouched, composes
around a router. (`guard` catches explicit `(error …)` raises; `e` stringifies to the
message.)
## Extensions (post-roadmap)
@@ -99,9 +106,9 @@ The five-types core is complete; these harden it toward a production HTTP front
- [x] **Router HTTP correctness**: 405 Method Not Allowed + `Allow` header; automatic
HEAD (serve the GET handler with an empty body).
- [ ] **Status reason phrases** + `dream-status-text`.
- [x] **Status reason phrases** + `dream-status-text` (`lib/dream/error.sx`).
- [ ] **CORS middleware** (`dream-cors`).
- [ ] **Error-handling middleware** (`dream-catch` / custom 404 + 500 templates).
- [x] **Error-handling middleware** (`dream-catch` / custom 500 templates; `guard`-based).
- [ ] **Signed session cookies** (the noted hardening — sign the sid).
- [ ] **JSON helpers** (build from dict; parse to dict).
- [ ] **Query/header convenience** (`dream-queries`, defaults).