dream: CORS middleware + preflight handling + 12 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 52s
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:
@@ -31,6 +31,7 @@ MODULES=(
|
|||||||
"lib/dream/websocket.sx"
|
"lib/dream/websocket.sx"
|
||||||
"lib/dream/static.sx"
|
"lib/dream/static.sx"
|
||||||
"lib/dream/error.sx"
|
"lib/dream/error.sx"
|
||||||
|
"lib/dream/cors.sx"
|
||||||
"lib/dream/run.sx"
|
"lib/dream/run.sx"
|
||||||
"lib/dream/demos/hello.sx"
|
"lib/dream/demos/hello.sx"
|
||||||
"lib/dream/demos/counter.sx"
|
"lib/dream/demos/counter.sx"
|
||||||
@@ -49,6 +50,7 @@ SUITES=(
|
|||||||
"websocket dream-ws-tests-run! lib/dream/tests/websocket.sx"
|
"websocket dream-ws-tests-run! lib/dream/tests/websocket.sx"
|
||||||
"static dream-st-tests-run! lib/dream/tests/static.sx"
|
"static dream-st-tests-run! lib/dream/tests/static.sx"
|
||||||
"error dream-er-tests-run! lib/dream/tests/error.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"
|
"run dream-rn-tests-run! lib/dream/tests/run.sx"
|
||||||
"demos dream-dm-tests-run! lib/dream/tests/demos.sx"
|
"demos dream-dm-tests-run! lib/dream/tests/demos.sx"
|
||||||
)
|
)
|
||||||
|
|||||||
51
lib/dream/cors.sx
Normal file
51
lib/dream/cors.sx
Normal 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
93
lib/dream/tests/cors.sx
Normal 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}))
|
||||||
@@ -99,6 +99,11 @@ with extensions + hardening below.
|
|||||||
custom page receiving `(req e)`); normal responses pass through untouched, composes
|
custom page receiving `(req e)`); normal responses pass through untouched, composes
|
||||||
around a router. (`guard` catches explicit `(error …)` raises; `e` stringifies to the
|
around a router. (`guard` catches explicit `(error …)` raises; `e` stringifies to the
|
||||||
message.)
|
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)
|
## 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
|
- [x] **Router HTTP correctness**: 405 Method Not Allowed + `Allow` header; automatic
|
||||||
HEAD (serve the GET handler with an empty body).
|
HEAD (serve the GET handler with an empty body).
|
||||||
- [x] **Status reason phrases** + `dream-status-text` (`lib/dream/error.sx`).
|
- [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).
|
- [x] **Error-handling middleware** (`dream-catch` / custom 500 templates; `guard`-based).
|
||||||
- [ ] **Signed session cookies** (the noted hardening — sign the sid).
|
- [ ] **Signed session cookies** (the noted hardening — sign the sid).
|
||||||
- [ ] **JSON helpers** (build from dict; parse to dict).
|
- [ ] **JSON helpers** (build from dict; parse to dict).
|
||||||
|
|||||||
Reference in New Issue
Block a user