Merge pull request #159 from valerauko/trailing-slash

Handle trailing slashes
This commit is contained in:
Tommi Reiman 2018-10-30 19:43:32 +02:00 committed by GitHub
commit 3e6f1c0548
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 159 additions and 0 deletions

View file

@ -25,6 +25,7 @@
["Ring-router" {:file "doc/ring/ring.md"}]
["Reverse-routing" {:file "doc/ring/reverse_routing.md"}]
["Default handler" {:file "doc/ring/default_handler.md"}]
["Slash handler" {:file "doc/ring/slash_handler.md"}]
["Static Resources" {:file "doc/ring/static.md"}]
["Dynamic Extensions" {:file "doc/ring/dynamic_extensions.md"}]
["Data-driven Middleware" {:file "doc/ring/data_driven_middleware.md"}]

64
doc/ring/slash_handler.md Normal file
View file

@ -0,0 +1,64 @@
# Slash handler
The router works with precise matches. If a route is defined without a trailing slash, for example, it won't match a request with a slash.
```clj
(require '[reitit.ring :as ring])
(def app
(ring/ring-handler
(ring/router
["/ping" (constantly {:status 200, :body ""})])))
(app {:uri "/ping/"})
; nil
```
Sometimes it is desirable that paths with and without a trailing slash are recognized as the same.
Setting the `redirect-trailing-slash-handler` as a second argument to `ring-handler`:
```clj
(def app
(ring/ring-handler
(ring/router
[["/ping" (constantly {:status 200, :body ""})]
["/pong/" (constantly {:status 200, :body ""})]])
(ring/redirect-trailing-slash-handler)))
(app {:uri "/ping/"})
; {:status 308, :headers {"Location" "/ping"}, :body ""}
(app {:uri "/pong"})
; {:status 308, :headers {"Location" "/pong/"}, :body ""}
```
`redirect-trailing-slash-handler` accepts an optional `:method` parameter that allows configuring how (whether) to handle missing/extra slashes. The default is to handle both.
```clj
(def app
(ring/ring-handler
(ring/router
[["/ping" (constantly {:status 200, :body ""})]
["/pong/" (constantly {:status 200, :body ""})]])
; only handle extra trailing slash
(ring/redirect-trailing-slash-handler {:method :strip})))
(app {:uri "/ping/"})
; {:status 308, :headers {"Location" "/ping"}, :body ""}
(app {:uri "/pong"})
; nil
```
```clj
(def app
(ring/ring-handler
(ring/router
[["/ping" (constantly {:status 200, :body ""})]
["/pong/" (constantly {:status 200, :body ""})]])
; only handle missing trailing slash
(ring/redirect-trailing-slash-handler {:method :add})))
(app {:uri "/ping/"})
; nil
(app {:uri "/pong"})
; {:status 308, :headers {"Location" "/pong/"}, :body ""}
```

View file

@ -110,6 +110,37 @@
(respond nil)))]
(f handlers))))))
(defn redirect-trailing-slash-handler
"A ring handler that redirects a missing path if there is an
existing path that only differs in the ending slash.
| key | description |
|---------|-------------|
| :method | :add - redirects slash-less to slashed |
| | :strip - redirects slashed to slash-less |
| | :both - works both ways (default) |
"
([] (redirect-trailing-slash-handler {:method :both}))
([{:keys [method]}]
(let [redirect-handler (fn redirect-handler [request]
(let [uri (:uri request)
status (if (= (:request-method request) :get) 301 308)
maybe-redirect (fn maybe-redirect [path]
(if (r/match-by-path (::r/router request) path)
{:status status
:headers {"Location" path}
:body ""}))]
(if (str/ends-with? uri "/")
(if (not= method :add)
(maybe-redirect (subs uri 0 (-> uri count dec))))
(if (not= method :strip)
(maybe-redirect (str uri "/"))))))]
(fn
([request]
(redirect-handler request))
([request respond _]
(respond (redirect-handler request)))))))
(defn create-default-handler
"A default ring handler that can handle the following cases,
configured via options:

View file

@ -239,6 +239,69 @@
(is (= response (app {:request-method :get, :uri "/any"})))
(is (= response (app {:request-method :options, :uri "/any"}))))))))
(deftest trailing-slash-handler-test
(let [ok {:status 200, :body "ok"}
routes [["/slash-less" {:get (constantly ok),
:post (constantly ok)}]
["/with-slash/" {:get (constantly ok),
:post (constantly ok)}]]]
(testing "using :method :add"
(let [app (ring/ring-handler
(ring/router routes)
(ring/redirect-trailing-slash-handler {:method :add}))]
(testing "exact matches work"
(is (= ok (app {:request-method :get, :uri "/slash-less"})))
(is (= ok (app {:request-method :post, :uri "/slash-less"})))
(is (= ok (app {:request-method :get, :uri "/with-slash/"})))
(is (= ok (app {:request-method :post, :uri "/with-slash/"}))))
(testing "adds slashes"
(is (= 301 (:status (app {:request-method :get, :uri "/with-slash"}))))
(is (= 308 (:status (app {:request-method :post, :uri "/with-slash"})))))
(testing "does not strip slashes"
(is (= nil (app {:request-method :get, :uri "/slash-less/"})))
(is (= nil (app {:request-method :post, :uri "/slash-less/"}))))))
(testing "using :method :strip"
(let [app (ring/ring-handler
(ring/router routes)
(ring/redirect-trailing-slash-handler {:method :strip}))]
(testing "exact matches work"
(is (= ok (app {:request-method :get, :uri "/slash-less"})))
(is (= ok (app {:request-method :post, :uri "/slash-less"})))
(is (= ok (app {:request-method :get, :uri "/with-slash/"})))
(is (= ok (app {:request-method :post, :uri "/with-slash/"}))))
(testing "does not add slashes"
(is (= nil (app {:request-method :get, :uri "/with-slash"})))
(is (= nil (app {:request-method :post, :uri "/with-slash"}))))
(testing "strips slashes"
(is (= 301 (:status (app {:request-method :get, :uri "/slash-less/"}))))
(is (= 308 (:status (app {:request-method :post, :uri "/slash-less/"})))))))
(testing "without option (equivalent to using :method :both)"
(let [app (ring/ring-handler
(ring/router routes)
(ring/redirect-trailing-slash-handler))]
(testing "exact matches work"
(is (= ok (app {:request-method :get, :uri "/slash-less"})))
(is (= ok (app {:request-method :post, :uri "/slash-less"})))
(is (= ok (app {:request-method :get, :uri "/with-slash/"})))
(is (= ok (app {:request-method :post, :uri "/with-slash/"}))))
(testing "adds slashes"
(is (= 301 (:status (app {:request-method :get, :uri "/with-slash"}))))
(is (= 308 (:status (app {:request-method :post, :uri "/with-slash"})))))
(testing "strips slashes"
(is (= 301 (:status (app {:request-method :get, :uri "/slash-less/"}))))
(is (= 308 (:status (app {:request-method :post, :uri "/slash-less/"})))))))))
(deftest async-ring-test
(let [promise #(let [value (atom ::nil)]
(fn