dream: router 405 Method Not Allowed + Allow header + automatic HEAD + 9 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 51s

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-07 15:00:29 +00:00
parent b1be3a36ec
commit 078872728e
3 changed files with 133 additions and 26 deletions

View File

@@ -1,7 +1,9 @@
;; lib/dream/router.sx — Dream-on-SX routing.
;; Routes are dicts {:method :path :handler}; a router is a handler that
;; dispatches request -> response by method + path, extracting :name path
;; params and binding a ** catch-all. No match -> 404. Depends on types.sx.
;; params and binding a ** catch-all. No path match -> 404; path matches but
;; method doesn't -> 405 + Allow. HEAD falls back to the GET handler with an
;; empty body. Depends on types.sx.
;; ── route constructors (one per HTTP method) ───────────────────────
(define dream-get (fn (path handler) (dream-route "GET" path handler)))
@@ -53,11 +55,25 @@
(dr/match-segs (rest pat) (rest path) params))
(else nil)))))))
;; path-only match: returns params dict or nil
(define
dr/method-match?
dr/route-params
(fn
(r req)
(dr/match-segs
(dr/segs (dream-route-path r))
(dr/segs (dream-path req))
{})))
;; method acceptance: exact, ANY, or HEAD served by a GET route
(define
dr/method-accepts?
(fn
(route-method req-method)
(or (= route-method "ANY") (= route-method req-method))))
(or
(= route-method "ANY")
(= route-method req-method)
(and (= req-method "HEAD") (= route-method "GET")))))
;; ── middleware pipeline (shared with middleware.sx) ────────────────
;; m1 @@ m2 @@ handler = (m1 (m2 handler)); first in list is outermost.
@@ -95,30 +111,55 @@
(dr/flatten-routes routes))))
;; ── dispatch ───────────────────────────────────────────────────────
(define
dr/try-route
(fn
(r req)
(if
(dr/method-match? (dream-route-method r) (dream-method req))
(let
((params (dr/match-segs (dr/segs (dream-route-path r)) (dr/segs (dream-path req)) {})))
(if
(nil? params)
:no-match (dream-coerce-response
((dream-route-handler r) (dream-with-params req params)))))
:no-match)))
;; allowed = methods of routes whose PATH matched (for 405 + Allow).
(define
dr/dispatch
(fn
(routes req)
(routes req allowed)
(if
(empty? routes)
(dream-not-found)
(if
(empty? allowed)
(dream-not-found)
(dream-method-not-allowed allowed))
(let
((res (dr/try-route (first routes) req)))
(if (= res :no-match) (dr/dispatch (rest routes) req) res)))))
((r (first routes)))
(let
((params (dr/route-params r req)))
(if
(nil? params)
(dr/dispatch (rest routes) req allowed)
(if
(dr/method-accepts? (dream-route-method r) (dream-method req))
(dr/run-route r req params)
(dr/dispatch
(rest routes)
req
(concat allowed (list (dream-route-method r)))))))))))
;; run a matched route; blank the body for an auto-HEAD on a GET route
(define
dr/run-route
(fn
(r req params)
(let
((resp (dream-coerce-response ((dream-route-handler r) (dream-with-params req params)))))
(if
(and
(= (dream-method req) "HEAD")
(not (= (dream-route-method r) "HEAD")))
(dream-response (dream-status resp) (dream-headers resp) "")
resp))))
;; 405 response with an Allow header listing the path's methods
(define
dream-method-not-allowed
(fn
(allowed)
(dream-add-header
(dream-response 405 {:content-type "text/plain; charset=utf-8"} "Method Not Allowed")
"allow"
(join ", " allowed))))
(define
dream-router
@@ -126,4 +167,4 @@
(routes)
(let
((flat (dr/flatten-routes routes)))
(fn (req) (dr/dispatch flat req)))))
(fn (req) (dr/dispatch flat req (list))))))

View File

@@ -1,4 +1,4 @@
;; lib/dream/tests/router.sx — routing dispatch, path params, scopes.
;; lib/dream/tests/router.sx — routing dispatch, path params, scopes, 405/HEAD.
(define dream-rt-pass 0)
(define dream-rt-fail 0)
@@ -45,9 +45,9 @@
(dream-status (dream-rt-app (dream-rt-req "GET" "/nope")))
404)
(dream-rt-test
"wrong method 404"
"wrong method 405"
(dream-status (dream-rt-app (dream-rt-req "GET" "/submit")))
404)
405)
(dream-rt-test
"trailing slash equiv"
(dream-resp-body (dream-rt-app (dream-rt-req "GET" "/about/")))
@@ -229,4 +229,44 @@
"x-inner")
"1")
;; ── 405 Method Not Allowed + Allow ─────────────────────────────────
(define
dream-rt-mapp
(dream-router
(list
(dream-get "/r" (fn (req) (dream-text "get")))
(dream-post "/r" (fn (req) (dream-text "post")))
(dream-get "/only" (fn (req) (dream-html "<p>hi</p>"))))))
(define dream-rt-405 (dream-rt-mapp (dream-rt-req "DELETE" "/r")))
(dream-rt-test "405 status" (dream-status dream-rt-405) 405)
(dream-rt-test
"405 Allow has GET"
(contains? (dream-resp-header dream-rt-405 "allow") "GET")
true)
(dream-rt-test
"405 Allow has POST"
(contains? (dream-resp-header dream-rt-405 "allow") "POST")
true)
(dream-rt-test
"matching method still works"
(dream-resp-body (dream-rt-mapp (dream-rt-req "POST" "/r")))
"post")
(dream-rt-test
"no path is 404 not 405"
(dream-status (dream-rt-mapp (dream-rt-req "DELETE" "/absent")))
404)
;; ── automatic HEAD (serve GET, empty body) ─────────────────────────
(define dream-rt-head (dream-rt-mapp (dream-rt-req "HEAD" "/only")))
(dream-rt-test "HEAD status 200" (dream-status dream-rt-head) 200)
(dream-rt-test "HEAD empty body" (dream-resp-body dream-rt-head) "")
(dream-rt-test
"HEAD keeps content-type"
(dream-resp-header dream-rt-head "content-type")
"text/html; charset=utf-8")
(dream-rt-test
"HEAD on missing path 404"
(dream-status (dream-rt-mapp (dream-rt-req "HEAD" "/none")))
404)
(define dream-rt-tests-run! (fn () {:total (+ dream-rt-pass dream-rt-fail) :passed dream-rt-pass :failed dream-rt-fail :fails dream-rt-fails}))