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
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:
@@ -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
41
lib/dream/error.sx
Normal 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
90
lib/dream/tests/error.sx
Normal 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}))
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user