dream: CORS middleware + preflight handling + 12 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 52s

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

View File

@@ -31,6 +31,7 @@ MODULES=(
"lib/dream/websocket.sx"
"lib/dream/static.sx"
"lib/dream/error.sx"
"lib/dream/cors.sx"
"lib/dream/run.sx"
"lib/dream/demos/hello.sx"
"lib/dream/demos/counter.sx"
@@ -49,6 +50,7 @@ SUITES=(
"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"
"cors dream-co-tests-run! lib/dream/tests/cors.sx"
"run dream-rn-tests-run! lib/dream/tests/run.sx"
"demos dream-dm-tests-run! lib/dream/tests/demos.sx"
)

51
lib/dream/cors.sx Normal file
View File

@@ -0,0 +1,51 @@
;; lib/dream/cors.sx — Dream-on-SX CORS middleware.
;; Decorates responses with Access-Control-Allow-* headers and short-circuits
;; preflight OPTIONS requests with a 204. Depends on types.sx.
(define dream-cors-defaults {:methods "GET, POST, PUT, PATCH, DELETE, OPTIONS" :headers "Content-Type" :max-age 86400 :credentials false :origin "*"})
(define
dr/cors-origin-headers
(fn
(opts resp)
(let
((r1 (dream-add-header resp "access-control-allow-origin" (get opts :origin))))
(if
(get opts :credentials)
(dream-add-header r1 "access-control-allow-credentials" "true")
r1))))
(define
dr/cors-preflight
(fn
(opts)
(dr/cors-origin-headers
opts
(dream-add-header
(dream-add-header
(dream-add-header
(dream-empty 204)
"access-control-allow-methods"
(get opts :methods))
"access-control-allow-headers"
(get opts :headers))
"access-control-max-age"
(str (get opts :max-age))))))
(define
dream-cors-with
(fn
(opts)
(fn
(next)
(fn
(req)
(if
(= (dream-method req) "OPTIONS")
(dr/cors-preflight opts)
(dr/cors-origin-headers opts (next req)))))))
(define dream-cors (dream-cors-with dream-cors-defaults))
(define
dream-cors-origin
(fn (origin) (dream-cors-with (assoc dream-cors-defaults :origin origin))))

93
lib/dream/tests/cors.sx Normal file
View File

@@ -0,0 +1,93 @@
;; lib/dream/tests/cors.sx — CORS decoration + preflight.
(define dream-co-pass 0)
(define dream-co-fail 0)
(define dream-co-fails (list))
(define
dream-co-test
(fn
(name actual expected)
(if
(= actual expected)
(set! dream-co-pass (+ dream-co-pass 1))
(begin
(set! dream-co-fail (+ dream-co-fail 1))
(append! dream-co-fails {:name name :actual actual :expected expected})))))
(define dream-co-h (fn (req) (dream-text "payload")))
(define dream-co-app (dream-cors dream-co-h))
;; ── decoration of normal responses ─────────────────────────────────
(define dream-co-get (dream-co-app (dream-request "GET" "/" {} "")))
(dream-co-test
"allow-origin star"
(dream-resp-header dream-co-get "access-control-allow-origin")
"*")
(dream-co-test "body preserved" (dream-resp-body dream-co-get) "payload")
(dream-co-test "status preserved" (dream-status dream-co-get) 200)
(dream-co-test
"no credentials by default"
(dream-resp-header dream-co-get "access-control-allow-credentials")
nil)
;; ── preflight OPTIONS ──────────────────────────────────────────────
(define
dream-co-pre
(dream-co-app (dream-request "OPTIONS" "/" {} "")))
(dream-co-test "preflight 204" (dream-status dream-co-pre) 204)
(dream-co-test
"preflight origin"
(dream-resp-header dream-co-pre "access-control-allow-origin")
"*")
(dream-co-test
"preflight methods"
(contains?
(dream-resp-header dream-co-pre "access-control-allow-methods")
"POST")
true)
(dream-co-test
"preflight headers"
(dream-resp-header dream-co-pre "access-control-allow-headers")
"Content-Type")
(dream-co-test
"preflight max-age"
(dream-resp-header dream-co-pre "access-control-max-age")
"86400")
;; ── custom origin ──────────────────────────────────────────────────
(define
dream-co-custom
((dream-cors-origin "https://app.example.com") dream-co-h))
(dream-co-test
"custom origin"
(dream-resp-header
(dream-co-custom (dream-request "GET" "/" {} ""))
"access-control-allow-origin")
"https://app.example.com")
;; ── credentials enabled ────────────────────────────────────────────
(define
dream-co-cred
((dream-cors-with (assoc dream-cors-defaults :credentials true))
dream-co-h))
(dream-co-test
"credentials header"
(dream-resp-header
(dream-co-cred (dream-request "GET" "/" {} ""))
"access-control-allow-credentials")
"true")
;; ── composes around a router ───────────────────────────────────────
(define
dream-co-router
(dream-cors
(dream-router (list (dream-get "/api" (fn (req) (dream-json "{}")))))))
(dream-co-test
"router cors origin"
(dream-resp-header
(dream-co-router (dream-request "GET" "/api" {} ""))
"access-control-allow-origin")
"*")
(define dream-co-tests-run! (fn () {:total (+ dream-co-pass dream-co-fail) :passed dream-co-pass :failed dream-co-fail :fails dream-co-fails}))

View File

@@ -99,6 +99,11 @@ with extensions + hardening below.
custom page receiving `(req e)`); normal responses pass through untouched, composes
around a router. (`guard` catches explicit `(error …)` raises; `e` stringifies to the
message.)
- **2026-06-07 — Ext: CORS** (`lib/dream/cors.sx`, 12 tests, 294 total). `dream-cors`
decorates responses with `Access-Control-Allow-Origin` (+ credentials), and
short-circuits preflight `OPTIONS` with a 204 carrying Allow-Methods/Headers/Max-Age.
`dream-cors-origin` for a specific origin, `dream-cors-with opts` for full control
(origin/methods/headers/credentials/max-age). Composes around a router.
## Extensions (post-roadmap)
@@ -107,7 +112,7 @@ 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).
- [x] **Status reason phrases** + `dream-status-text` (`lib/dream/error.sx`).
- [ ] **CORS middleware** (`dream-cors`).
- [x] **CORS middleware** (`dream-cors`).
- [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).