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}))