Compare commits

..

No commits in common. "master" and "0.10.0" have entirely different histories.

13 changed files with 33 additions and 155 deletions

View file

@ -12,12 +12,6 @@ We use [Break Versioning][breakver]. The version numbers follow a `<major>.<mino
[breakver]: https://github.com/ptaoussanis/encore/blob/master/BREAK-VERSIONING.md [breakver]: https://github.com/ptaoussanis/encore/blob/master/BREAK-VERSIONING.md
## UNRELEASED
* **FIX** redirect-trailing-slash-handler won't make external redirects. [#776](https://github.com/metosin/reitit/pull/776)
* Allow colons in bracket parameter syntax. [#770](https://github.com/metosin/reitit/pull/770)
* Add `url-encode?` option to `match-by-name`. [#778](https://github.com/metosin/reitit/pull/778)
## 0.10.0 (2026-01-09) ## 0.10.0 (2026-01-09)
* Improve & document how response schemas get picked in per-content-type coercion. See [docs](./doc/ring/coercion.md#per-content-type-coercion). [#745](https://github.com/metosin/reitit/issues/745). * Improve & document how response schemas get picked in per-content-type coercion. See [docs](./doc/ring/coercion.md#per-content-type-coercion). [#745](https://github.com/metosin/reitit/issues/745).

View file

@ -75,17 +75,6 @@ Path-parameters are automatically coerced into strings, with the help of (curren
; :path-params {:id "1"}} ; :path-params {:id "1"}}
``` ```
In case you want to do something like generate a template path for documentation, you can disable url-encoding:
```clj
(r/match-by-name router ::user {:id "<id goes here>"} {:url-encode? false})
; #reitit.core.Match{:template "/api/user/:id"
; :data {:name :user/user}
; :path "/api/user/<id goes here>"
; :result nil
; :path-params {:id "<id goes here>"}}
```
There is also an exception throwing version: There is also an exception throwing version:
```clj ```clj

View file

@ -63,6 +63,8 @@ Handlers can access the coerced parameters via the `:parameters` key in the requ
{:status 200 {:status 200
:body {:total total}}))}) :body {:total total}}))})
``` ```
### Nested parameter definitions ### Nested parameter definitions
Parameters are accumulated recursively along the route tree, just like Parameters are accumulated recursively along the route tree, just like
@ -92,26 +94,6 @@ handling for merging eg. malli `:map` schemas.
; [:task-id :int]]} ; [:task-id :int]]}
``` ```
### Differences in behaviour for different parameters
All parameter coercions *except* `:body`:
1. Allow keys outside the schema (by opening up the schema using eg. `malli.util/open-schema`)
2. Keywordize the keys (ie. header & query parameter names) of the input before coercing
In contrast, the `:body` coercion:
1. Uses the specified schema
* depending on the coercion, it can be configured as open or closed, see specific coercion docs for details
2. Does not keywordize the keys of the input before coercion
* however, coercions like malli might do the keywordization when coercing json bodies, depending on configuration
This admittedly confusing behaviour is retained currently due to
backwards compatibility reasons. It can be configured by passing
option `:reitit.coercion/parameter-coercion` to `reitit.ring/router`
or `reitit.coercion/compile-request-coercers`. See also:
`reitit.coercion/default-parameter-coercion`.
## Coercion Middleware ## Coercion Middleware
Defining a coercion for a route data doesn't do anything, as it's just data. We have to attach some code to apply the actual coercion. We can use the middleware from `reitit.ring.coercion`: Defining a coercion for a route data doesn't do anything, as it's just data. We have to attach some code to apply the actual coercion. We can use the middleware from `reitit.ring.coercion`:

View file

@ -4,7 +4,6 @@
[metosin/jsonista "0.3.8"] [metosin/jsonista "0.3.8"]
[ring/ring-jetty-adapter "1.12.1"] [ring/ring-jetty-adapter "1.12.1"]
[metosin/reitit "0.10.0"] [metosin/reitit "0.10.0"]
[metosin/ring-swagger-ui "5.9.0"] [metosin/ring-swagger-ui "5.9.0"]]
[org.slf4j/slf4j-simple "2.0.9"]]
:repl-options {:init-ns example.server} :repl-options {:init-ns example.server}
:profiles {:dev {:dependencies [[ring/ring-mock "0.4.0"]]}}) :profiles {:dev {:dependencies [[ring/ring-mock "0.4.0"]]}})

View file

@ -156,22 +156,22 @@
[:regex [:re "[0-9]+"]] [:regex [:re "[0-9]+"]]
[:enum [:enum 1 3 5 42]] [:enum [:enum 1 3 5 42]]
[:multi [:multi {:dispatch :type} [:multi [:multi {:dispatch :type}
["literal" [:map [:literal [:map
[:type [:= "literal"]] [:type [:= :literal]]
[:value [:or :int :string]]]] [:value [:or :int :string]]]]
["reference" [:map [:reference [:map
[:type [:= "reference"]] [:type [:= :reference]]
[:description :string] [:description :string]
[:ref :uuid]]]]]] [:ref :uuid]]]]]]
:example {:vector-of-tuples [["a" 1] ["b" 2]] :example {:vector-of-tuples [["a" 1] ["b" 2]]
:regex "01234" :regex "01234"
:enum 5 :enum 5
:multi {:type "literal" :multi {:type :literal
:value "x"}}}}} :value "x"}}}}}
:responses {200 {:content {:default {:schema [:map-of :keyword :any]}}}} :responses {200 {:content {:default {:schema [:map]}}}}
:handler (fn [request] :handler (fn [request]
{:status 200 {:status 200
:body (get-in request [:parameters :request])})}}] :body (:body request)})}}]
["/secure" ["/secure"
{:tags #{"secure"} {:tags #{"secure"}

View file

@ -46,7 +46,7 @@
(options [this]) (options [this])
(route-names [this]) (route-names [this])
(match-by-path [this path]) (match-by-path [this path])
(match-by-name [this name] [this name path-params] [this name path-params opts])) (match-by-name [this name] [this name path-params]))
(defn router? [x] (defn router? [x]
(satisfies? Router x)) (satisfies? Router x))
@ -122,11 +122,9 @@
(match-by-name [_ name] (match-by-name [_ name]
(if-let [match (impl/fast-get lookup name)] (if-let [match (impl/fast-get lookup name)]
(match nil))) (match nil)))
(match-by-name [r name path-params] (match-by-name [_ name path-params]
(match-by-name r name path-params nil))
(match-by-name [_ name path-params opts]
(if-let [match (impl/fast-get lookup name)] (if-let [match (impl/fast-get lookup name)]
(match (impl/path-params path-params opts)))))))) (match (impl/path-params path-params))))))))
(defn lookup-router (defn lookup-router
"Creates a lookup-router from resolved routes and optional "Creates a lookup-router from resolved routes and optional
@ -163,11 +161,9 @@
(match-by-name [_ name] (match-by-name [_ name]
(if-let [match (impl/fast-get lookup name)] (if-let [match (impl/fast-get lookup name)]
(match nil))) (match nil)))
(match-by-name [r name path-params] (match-by-name [_ name path-params]
(match-by-name r name path-params nil))
(match-by-name [_ name path-params opts]
(if-let [match (impl/fast-get lookup name)] (if-let [match (impl/fast-get lookup name)]
(match (impl/path-params path-params opts)))))))) (match (impl/path-params path-params))))))))
(defn trie-router (defn trie-router
"Creates a special prefix-tree router from resolved routes and optional "Creates a special prefix-tree router from resolved routes and optional
@ -212,11 +208,9 @@
(match-by-name [_ name] (match-by-name [_ name]
(if-let [match (impl/fast-get lookup name)] (if-let [match (impl/fast-get lookup name)]
(match nil))) (match nil)))
(match-by-name [r name path-params] (match-by-name [_ name path-params]
(match-by-name r name path-params nil))
(match-by-name [_ name path-params opts]
(if-let [match (impl/fast-get lookup name)] (if-let [match (impl/fast-get lookup name)]
(match (impl/path-params path-params opts)))))))) (match (impl/path-params path-params))))))))
(defn single-static-path-router (defn single-static-path-router
"Creates a fast router of 1 static route(s) and optional "Creates a fast router of 1 static route(s) and optional
@ -244,10 +238,8 @@
(if (#?(:clj .equals :cljs =) p path) match)) (if (#?(:clj .equals :cljs =) p path) match))
(match-by-name [_ name] (match-by-name [_ name]
(if (= n name) match)) (if (= n name) match))
(match-by-name [r name path-params] (match-by-name [_ name path-params]
(match-by-name r name path-params nil)) (if (= n name) (impl/fast-assoc match :path-params (impl/path-params path-params))))))))
(match-by-name [_ name path-params opts]
(if (= n name) (impl/fast-assoc match :path-params (impl/path-params path-params opts))))))))
(defn mixed-router (defn mixed-router
"Creates two routers: [[lookup-router]] or [[single-static-path-router]] for "Creates two routers: [[lookup-router]] or [[single-static-path-router]] for
@ -276,11 +268,9 @@
(match-by-name [_ name] (match-by-name [_ name]
(or (match-by-name static-router name) (or (match-by-name static-router name)
(match-by-name wildcard-router name))) (match-by-name wildcard-router name)))
(match-by-name [r name path-params] (match-by-name [_ name path-params]
(match-by-name r name path-params nil)) (or (match-by-name static-router name path-params)
(match-by-name [_ name path-params opts] (match-by-name wildcard-router name path-params)))))))
(or (match-by-name static-router name path-params opts)
(match-by-name wildcard-router name path-params opts)))))))
(defn quarantine-router (defn quarantine-router
"Creates two routers: [[mixed-router]] for non-conflicting routes "Creates two routers: [[mixed-router]] for non-conflicting routes
@ -309,11 +299,9 @@
(match-by-name [_ name] (match-by-name [_ name]
(or (match-by-name mixed-router name) (or (match-by-name mixed-router name)
(match-by-name linear-router name))) (match-by-name linear-router name)))
(match-by-name [r name path-params] (match-by-name [_ name path-params]
(match-by-name r name path-params nil)) (or (match-by-name mixed-router name path-params)
(match-by-name [_ name path-params opts] (match-by-name linear-router name path-params)))))))
(or (match-by-name mixed-router name path-params opts)
(match-by-name linear-router name path-params opts)))))))
;; ;;
;; Creating Routers ;; Creating Routers

View file

@ -297,11 +297,8 @@
(defn path-params (defn path-params
"Convert parameters' values into URL-encoded strings, suitable for URL paths" "Convert parameters' values into URL-encoded strings, suitable for URL paths"
([params] (path-params params nil)) [params]
([params {:keys [url-encode?] :or {url-encode? true}}] (maybe-map-values #(url-encode (into-string %)) params))
(if url-encode?
(maybe-map-values #(url-encode (into-string %)) params)
(maybe-map-values #(into-string %) params))))
(defn- query-parameter [k v] (defn- query-parameter [k v]
(str (form-encode (into-string k)) (str (form-encode (into-string k))

View file

@ -71,17 +71,11 @@
(and bracket? (= \{ c)) (and bracket? (= \{ c))
(let [^long to' (or (str/index-of s "}" to) (ex/fail! ::unclosed-brackets {:path s}))] (let [^long to' (or (str/index-of s "}" to) (ex/fail! ::unclosed-brackets {:path s}))]
(cond (if (= \* (get s (inc to)))
(= \* (get s (inc to)))
(recur (concat ss (-static from to) (-catch-all (inc to) to')) (long (inc to')) (long (inc to'))) (recur (concat ss (-static from to) (-catch-all (inc to) to')) (long (inc to')) (long (inc to')))
(= \: (get s (inc to)))
(recur (concat ss (-static from to) (-wild (inc to) to')) (long (inc to')) (long (inc to')))
:else
(recur (concat ss (-static from to) (-wild to to')) (long (inc to')) (long (inc to'))))) (recur (concat ss (-static from to) (-wild to to')) (long (inc to')) (long (inc to')))))
(and colon? (= \: c) (not= \{ (get s (dec to)))) (and colon? (= \: c))
(let [^long to' (or (str/index-of s "/" to) (count s))] (let [^long to' (or (str/index-of s "/" to) (count s))]
(if (= 1 (- to' to)) (if (= 1 (- to' to))
(recur ss from (inc to)) (recur ss from (inc to))

View file

@ -173,8 +173,7 @@
(letfn [(maybe-redirect [{:keys [query-string] :as request} path] (letfn [(maybe-redirect [{:keys [query-string] :as request} path]
(if (and (seq path) (r/match-by-path (::r/router request) path)) (if (and (seq path) (r/match-by-path (::r/router request) path))
{:status (if (= (:request-method request) :get) 301 308) {:status (if (= (:request-method request) :get) 301 308)
:headers {"Location" (let [path (str/replace-first path #"^/+" "/")] ; Locations starting with // redirect to another hostname. Avoid these due to security implications. :headers {"Location" (if query-string (str path "?" query-string) path)}
(if query-string (str path "?" query-string) path))}
:body ""})) :body ""}))
(redirect-handler [request] (redirect-handler [request]
(let [uri (:uri request)] (let [uri (:uri request)]

View file

@ -33,12 +33,6 @@
:path "/api/ipa/large" :path "/api/ipa/large"
:path-params {:size "large"}}) :path-params {:size "large"}})
(r/match-by-name router ::beer {:size "large"}))) (r/match-by-name router ::beer {:size "large"})))
(is (= (r/map->Match
{:template "/api/ipa/:size"
:data {:name ::beer}
:path "/api/ipa/:large"
:path-params {:size ":large"}})
(r/match-by-name router ::beer {:size ":large"} {:url-encode? false})))
(is (= (r/map->Match (is (= (r/map->Match
{:template "/api/ipa/:size" {:template "/api/ipa/:size"
:data {:name ::beer} :data {:name ::beer}

View file

@ -41,33 +41,7 @@
:u #uuid "c2541900-17a7-4353-9024-db8ac258ba4e" :u #uuid "c2541900-17a7-4353-9024-db8ac258ba4e"
:k :kikka :k :kikka
:qk ::kikka :qk ::kikka
:nil nil}))) :nil nil}))))
(is (= {:n "1"
:n1 "-1"
:n2 "1"
:n3 "1"
:n4 "1"
:n5 "1"
:d "2.2"
:b "true"
:s "kikka"
:u "c2541900-17a7-4353-9024-db8ac258ba4e"
:k "kikka"
:qk "reitit.impl-test/kikka"
:nil nil}
(impl/path-params {:n 1
:n1 -1
:n2 (long 1)
:n3 (int 1)
:n4 (short 1)
:n5 (byte 1)
:d 2.2
:b true
:s "kikka"
:u #uuid "c2541900-17a7-4353-9024-db8ac258ba4e"
:k :kikka
:qk ::kikka
:nil nil} {:url-encode? false}))))
(deftest query-params-test (deftest query-params-test
(are [x y] (are [x y]

View file

@ -416,31 +416,7 @@
:get "/slash-less//" "/slash-less?kikka=kukka" :get "/slash-less//" "/slash-less?kikka=kukka"
:post "/with-slash" "/with-slash/?kikka=kukka" :post "/with-slash" "/with-slash/?kikka=kukka"
:post "/slash-less/" "/slash-less?kikka=kukka" :post "/slash-less/" "/slash-less?kikka=kukka"
:post "/slash-less//" "/slash-less?kikka=kukka")))) :post "/slash-less//" "/slash-less?kikka=kukka"))))))
;; See issue #337
(testing "Avoid external redirects"
(let [app (ring/ring-handler
(ring/router [["*" {:get (constantly nil)}]])
(ring/redirect-trailing-slash-handler))
resp (fn [uri & [query-string]]
(let [r (app {:request-method :get :uri uri :query-string query-string})]
{:status (:status r)
:Location (get-in r [:headers "Location"])}))]
(testing "without query params"
(is (= {:status 301 :Location "/malicious.com/foo/"} (resp "//malicious.com/foo")))
(is (= {:status 301 :Location "/malicious.com/foo"} (resp "//malicious.com/foo/")))
(is (= {:status 301 :Location "/malicious.com/foo"} (resp "//malicious.com/foo//")))
(is (= {:status 301 :Location "/malicious.com/foo/"} (resp "///malicious.com/foo")))
(is (= {:status 301 :Location "/malicious.com/foo"} (resp "///malicious.com/foo/")))
(is (= {:status 301 :Location "/malicious.com/foo"} (resp "///malicious.com/foo//"))))
(testing "with query params"
(is (= {:status 301 :Location "/malicious.com/foo/?bar=quux"} (resp "//malicious.com/foo" "bar=quux")))
(is (= {:status 301 :Location "/malicious.com/foo?bar=quux"} (resp "//malicious.com/foo/" "bar=quux")))
(is (= {:status 301 :Location "/malicious.com/foo?bar=quux"} (resp "//malicious.com/foo//" "bar=quux")))
(is (= {:status 301 :Location "/malicious.com/foo/?bar=quux"} (resp "///malicious.com/foo" "bar=quux")))
(is (= {:status 301 :Location "/malicious.com/foo?bar=quux"} (resp "///malicious.com/foo/" "bar=quux")))
(is (= {:status 301 :Location "/malicious.com/foo?bar=quux"} (resp "///malicious.com/foo//" "bar=quux"))))))))
(deftest async-ring-test (deftest async-ring-test
(let [promise #(let [value (atom ::nil)] (let [promise #(let [value (atom ::nil)]

View file

@ -41,9 +41,7 @@
"/olipa/:kerran/avaruus", ["/olipa/" (trie/->Wild :kerran) "/avaruus"] "/olipa/:kerran/avaruus", ["/olipa/" (trie/->Wild :kerran) "/avaruus"]
"/olipa/{kerran}/avaruus", ["/olipa/{kerran}/avaruus"] "/olipa/{kerran}/avaruus", ["/olipa/{kerran}/avaruus"]
"/olipa/{:kerran}/avaruus", ["/olipa/{:kerran}/avaruus"]
"/olipa/{a.b/c}/avaruus", ["/olipa/{a.b/c}/avaruus"] "/olipa/{a.b/c}/avaruus", ["/olipa/{a.b/c}/avaruus"]
"/olipa/{:a.b/c}/avaruus", ["/olipa/{:a.b/c}/avaruus"]
"/olipa/kerran/*avaruus", ["/olipa/kerran/" (trie/->CatchAll :avaruus)] "/olipa/kerran/*avaruus", ["/olipa/kerran/" (trie/->CatchAll :avaruus)]
"/olipa/kerran/{*avaruus}", ["/olipa/kerran/{" (trie/->CatchAll (keyword "avaruus}"))] "/olipa/kerran/{*avaruus}", ["/olipa/kerran/{" (trie/->CatchAll (keyword "avaruus}"))]
"/olipa/kerran/{*valtavan.suuri/avaruus}", ["/olipa/kerran/{" (trie/->CatchAll (keyword "valtavan.suuri/avaruus}"))]))) "/olipa/kerran/{*valtavan.suuri/avaruus}", ["/olipa/kerran/{" (trie/->CatchAll (keyword "valtavan.suuri/avaruus}"))])))
@ -55,9 +53,7 @@
"/olipa/:kerran/avaruus", ["/olipa/:kerran/avaruus"] "/olipa/:kerran/avaruus", ["/olipa/:kerran/avaruus"]
"/olipa/{kerran}/avaruus", ["/olipa/" (trie/->Wild :kerran) "/avaruus"] "/olipa/{kerran}/avaruus", ["/olipa/" (trie/->Wild :kerran) "/avaruus"]
"/olipa/{:kerran}/avaruus", ["/olipa/" (trie/->Wild :kerran) "/avaruus"]
"/olipa/{a.b/c}/avaruus", ["/olipa/" (trie/->Wild :a.b/c) "/avaruus"] "/olipa/{a.b/c}/avaruus", ["/olipa/" (trie/->Wild :a.b/c) "/avaruus"]
"/olipa/{:a.b/c}/avaruus", ["/olipa/" (trie/->Wild :a.b/c) "/avaruus"]
"/olipa/kerran/*avaruus", ["/olipa/kerran/*avaruus"] "/olipa/kerran/*avaruus", ["/olipa/kerran/*avaruus"]
"/olipa/kerran/{*avaruus}", ["/olipa/kerran/" (trie/->CatchAll :avaruus)] "/olipa/kerran/{*avaruus}", ["/olipa/kerran/" (trie/->CatchAll :avaruus)]
"/olipa/kerran/{*valtavan.suuri/avaruus}", ["/olipa/kerran/" (trie/->CatchAll :valtavan.suuri/avaruus)]))) "/olipa/kerran/{*valtavan.suuri/avaruus}", ["/olipa/kerran/" (trie/->CatchAll :valtavan.suuri/avaruus)])))
@ -69,9 +65,7 @@
"/olipa/:kerran/avaruus", ["/olipa/" (trie/->Wild :kerran) "/avaruus"] "/olipa/:kerran/avaruus", ["/olipa/" (trie/->Wild :kerran) "/avaruus"]
"/olipa/{kerran}/avaruus", ["/olipa/" (trie/->Wild :kerran) "/avaruus"] "/olipa/{kerran}/avaruus", ["/olipa/" (trie/->Wild :kerran) "/avaruus"]
"/olipa/{:kerran}/avaruus", ["/olipa/" (trie/->Wild :kerran) "/avaruus"]
"/olipa/{a.b/c}/avaruus", ["/olipa/" (trie/->Wild :a.b/c) "/avaruus"] "/olipa/{a.b/c}/avaruus", ["/olipa/" (trie/->Wild :a.b/c) "/avaruus"]
"/olipa/{:a.b/c}/avaruus", ["/olipa/" (trie/->Wild :a.b/c) "/avaruus"]
"/olipa/kerran/*avaruus", ["/olipa/kerran/" (trie/->CatchAll :avaruus)] "/olipa/kerran/*avaruus", ["/olipa/kerran/" (trie/->CatchAll :avaruus)]
"/olipa/kerran/{*avaruus}", ["/olipa/kerran/" (trie/->CatchAll :avaruus)] "/olipa/kerran/{*avaruus}", ["/olipa/kerran/" (trie/->CatchAll :avaruus)]
"/olipa/kerran/{*valtavan.suuri/avaruus}", ["/olipa/kerran/" (trie/->CatchAll :valtavan.suuri/avaruus)]))) "/olipa/kerran/{*valtavan.suuri/avaruus}", ["/olipa/kerran/" (trie/->CatchAll :valtavan.suuri/avaruus)])))
@ -83,9 +77,7 @@
"/olipa/:kerran/avaruus", ["/olipa/:kerran/avaruus"] "/olipa/:kerran/avaruus", ["/olipa/:kerran/avaruus"]
"/olipa/{kerran}/avaruus", ["/olipa/{kerran}/avaruus"] "/olipa/{kerran}/avaruus", ["/olipa/{kerran}/avaruus"]
"/olipa/{:kerran}/avaruus", ["/olipa/{:kerran}/avaruus"]
"/olipa/{a.b/c}/avaruus", ["/olipa/{a.b/c}/avaruus"] "/olipa/{a.b/c}/avaruus", ["/olipa/{a.b/c}/avaruus"]
"/olipa/{:a.b/c}/avaruus", ["/olipa/{:a.b/c}/avaruus"]
"/olipa/kerran/*avaruus", ["/olipa/kerran/*avaruus"] "/olipa/kerran/*avaruus", ["/olipa/kerran/*avaruus"]
"/olipa/kerran/{*avaruus}", ["/olipa/kerran/{*avaruus}"] "/olipa/kerran/{*avaruus}", ["/olipa/kerran/{*avaruus}"]
"/olipa/kerran/{*valtavan.suuri/avaruus}", ["/olipa/kerran/{*valtavan.suuri/avaruus}"])))) "/olipa/kerran/{*valtavan.suuri/avaruus}", ["/olipa/kerran/{*valtavan.suuri/avaruus}"]))))