Liking cljdoc? Tell your friends :D

koine.route

Routing, static files and reverse proxying — as pure handler combinators over koine.server.

THE DESIGN CONSTRAINT: this file contains zero reader conditionals, on purpose. Everything here is handler -> handler or data -> handler, composed ABOVE the seam:

routing is map lookup (plain clojure.core) static is koine.fs + slurp (already portable) proxying is koine.http/request (already portable) timing is koine.time/mono-ms (already portable)

So the namespace inherits four-host support for free and adds no host code. If a change here seems to need a reader conditional, the design is wrong — the branch belongs in the seam namespace (koine.server, koine.http, koine.fs), not in a combinator. (There is a test asserting this file has none; it greps the source.)

(def app
  (route/routes->handler
    {[:get "/health"] (fn [_] {:body "ok"})
     "/assets/*"       (route/static "public")
     "/api/*"          (route/proxy "http://127.0.0.1:9000")}
    {:middleware [(fn [h] (route/wrap-log h println))]}))

(server/serve app {:port 8080})

Handlers speak exactly the koine.server request/response maps — request {:method :path :headers :body}, response {:status :headers :body} — so any handler here is a valid serve handler and any serve handler is a valid route target. Nothing new to learn, nothing new to port.

NESTING. A wildcard match adds two keys to the request map:

:path-info the part of the path AFTER the matched prefix ("/a.css") :route-prefix the prefix that matched ("/assets/")

router, static and proxy all read :path-info in preference to :path, which is what makes them nest: a router inside a router matches on the remainder, and static/proxy mounted under /assets/* see /a.css, not /assets/a.css. :path is never rewritten, so logging and the handler's own view of the original URL stay intact.

KNOWN LIMITS, stated rather than faked:

  • Bodies are strings on every host (that is koine.server's contract), so static serves TEXT faithfully and binary files only as far as the host's slurp decoding allows. Content types for binary extensions are still emitted, because getting the header right costs nothing.
  • koine.server hands over the path only, never the query string, so proxy forwards the path and drops any query.
  • koine.fs/exists?/directory? are implemented for the JVM and cljgo only. static therefore takes :exists?/:directory?/:read overrides: the DEFAULTS are the koine.fs fns, and a host where those are unimplemented can supply its own without this file growing a branch. (The fix belongs in koine/fs.cljc, not here.)
Routing, static files and reverse proxying — as pure handler combinators
over `koine.server`.

THE DESIGN CONSTRAINT: this file contains **zero reader conditionals**, on
purpose. Everything here is `handler -> handler` or `data -> handler`,
composed ABOVE the seam:

  routing  is map lookup                (plain clojure.core)
  static   is `koine.fs` + `slurp`      (already portable)
  proxying is `koine.http/request`      (already portable)
  timing   is `koine.time/mono-ms`      (already portable)

So the namespace inherits four-host support for free and adds no host code.
If a change here seems to need a reader conditional, the design is wrong —
the branch belongs in the seam namespace (`koine.server`, `koine.http`,
`koine.fs`), not in a combinator. (There is a test asserting this file has
none; it greps the source.)

    (def app
      (route/routes->handler
        {[:get "/health"] (fn [_] {:body "ok"})
         "/assets/*"       (route/static "public")
         "/api/*"          (route/proxy "http://127.0.0.1:9000")}
        {:middleware [(fn [h] (route/wrap-log h println))]}))

    (server/serve app {:port 8080})

Handlers speak exactly the `koine.server` request/response maps — request
`{:method :path :headers :body}`, response `{:status :headers :body}` — so
any handler here is a valid `serve` handler and any `serve` handler is a
valid route target. Nothing new to learn, nothing new to port.

NESTING. A wildcard match adds two keys to the request map:

  :path-info    the part of the path AFTER the matched prefix ("/a.css")
  :route-prefix the prefix that matched ("/assets/")

`router`, `static` and `proxy` all read `:path-info` in preference to
`:path`, which is what makes them nest: a router inside a router matches on
the remainder, and `static`/`proxy` mounted under `/assets/*` see
`/a.css`, not `/assets/a.css`. `:path` is never rewritten, so logging and
the handler's own view of the original URL stay intact.

KNOWN LIMITS, stated rather than faked:
- Bodies are strings on every host (that is `koine.server`'s contract), so
  `static` serves TEXT faithfully and binary files only as far as the
  host's `slurp` decoding allows. Content types for binary extensions are
  still emitted, because getting the header right costs nothing.
- `koine.server` hands over the path only, never the query string, so
  `proxy` forwards the path and drops any query.
- `koine.fs/exists?`/`directory?` are implemented for the JVM and cljgo
  only. `static` therefore takes `:exists?`/`:directory?`/`:read`
  overrides: the DEFAULTS are the `koine.fs` fns, and a host where those
  are unimplemented can supply its own without this file growing a branch.
  (The fix belongs in `koine/fs.cljc`, not here.)
raw docstring

content-typeclj/s

(content-type path)

Content type guessed from a path's extension. Unknown -> octet-stream.

Content type guessed from a path's extension. Unknown -> octet-stream.
raw docstring

effective-pathclj/s

(effective-path req)

The path a mounted handler should act on: the remainder left by an enclosing wildcard route, or the request path when there is no enclosing route. This one function is the whole nesting story.

The path a mounted handler should act on: the remainder left by an
enclosing wildcard route, or the request path when there is no enclosing
route. This one function is the whole nesting story.
raw docstring

proxyclj/s

(proxy target-base)
(proxy target-base {:keys [timeout-ms headers] :or {timeout-ms 30000}})

Forward the request to target-base and return the upstream response.

Mounted under a wildcard route it forwards the remainder: {"/api/*" (proxy "http://up:9000")} sends /api/v1/x to http://up:9000/v1/x. Hop-by-hop headers are stripped in BOTH directions.

opts: {:timeout-ms 30000 :headers {"x-forwarded-host" "…"}} ; merged in, wins over inbound

Forward the request to `target-base` and return the upstream response.

Mounted under a wildcard route it forwards the remainder:
`{"/api/*" (proxy "http://up:9000")}` sends `/api/v1/x` to
`http://up:9000/v1/x`. Hop-by-hop headers are stripped in BOTH directions.

opts: {:timeout-ms 30000
       :headers {"x-forwarded-host" "…"}}  ; merged in, wins over inbound
raw docstring

routerclj/s

(router routes)
(router routes {:keys [not-found]})

Turn a routes map into a handler.

routes: {[method path] handler path handler} ; path-only key = any method

path may end in * for a prefix route ("/api/*"). Longest prefix wins, an exact path always beats a wildcard, and a method-specific route beats a method-agnostic one at the same path. Nothing matched -> 404.

opts: {:not-found handler}

Turn a routes map into a handler.

routes: {[method path] handler
         path          handler}   ; path-only key = any method

`path` may end in `*` for a prefix route ("/api/*"). Longest prefix wins,
an exact path always beats a wildcard, and a method-specific route beats a
method-agnostic one at the same path. Nothing matched -> 404.

opts: {:not-found handler}
raw docstring

routes->handlerclj/s

(routes->handler routes)
(routes->handler routes {:keys [not-found middleware]})

router plus a middleware stack, so a whole app is one expression.

opts: {:not-found handler :middleware [f g]} ; each f is handler -> handler; FIRST is outermost

(routes->handler routes {:middleware [a b]}) == (a (b (router routes))).

`router` plus a middleware stack, so a whole app is one expression.

opts: {:not-found handler
       :middleware [f g]}   ; each f is handler -> handler; FIRST is outermost

`(routes->handler routes {:middleware [a b]})` == `(a (b (router routes)))`.
raw docstring

safe-path?clj/s

(safe-path? rel)

THE security line of this namespace. True only for a relative, rooted, traversal-free path.

Rejects, after percent-decoding: any .. segment, a backslash (a Windows separator that would slip past a /-only segment test), a leading // (protocol-relative / UNC), a drive letter, an embedded NUL, and anything not starting at /. Deliberately a WHITELIST of shape rather than a blacklist of strings.

THE security line of this namespace. True only for a relative, rooted,
traversal-free path.

Rejects, after percent-decoding: any `..` segment, a backslash (a Windows
separator that would slip past a `/`-only segment test), a leading `//`
(protocol-relative / UNC), a drive letter, an embedded NUL, and anything
not starting at `/`. Deliberately a WHITELIST of shape rather than a
blacklist of strings.
raw docstring

staticclj/s

(static dir)
(static dir {:keys [index exists? directory? read] :or {index "index.html"}})

Serve files under dir as a handler.

Resolves (effective-path req) — so mounting it at "/assets/*" serves dir/a.css for /assets/a.css. Path traversal is rejected with 403, a missing file is 404, a directory serves :index if present.

opts: {:index "index.html" :exists? fn ; default koine.fs/exists? :directory? fn ; default koine.fs/directory? :read fn} ; default slurp

The three fn overrides exist because koine.fs is currently JVM+cljgo only; they let a host without those branches serve files without this file growing a reader conditional. Everything else here is pure string work and is identical on every host.

Serve files under `dir` as a handler.

Resolves `(effective-path req)` — so mounting it at "/assets/*" serves
`dir/a.css` for `/assets/a.css`. Path traversal is rejected with 403, a
missing file is 404, a directory serves `:index` if present.

opts: {:index      "index.html"
       :exists?    fn        ; default koine.fs/exists?
       :directory? fn        ; default koine.fs/directory?
       :read       fn}       ; default slurp

The three fn overrides exist because `koine.fs` is currently JVM+cljgo
only; they let a host without those branches serve files without this
file growing a reader conditional. Everything else here is pure string
work and is identical on every host.
raw docstring

wrap-logclj/s

(wrap-log handler f)

Call (f {:method :path :status :ms}) after every request, then return the response unchanged.

:ms comes from koine.time/mono-ms, never wall clock — a duration measured against a clock the OS can move backwards is a lie. The call is in a finally, so a handler that throws is still logged (as 500) and the exception still propagates; finally needs no catch clause and therefore no reader conditional, which is exactly why it is used here.

Call `(f {:method :path :status :ms})` after every request, then return
the response unchanged.

`:ms` comes from `koine.time/mono-ms`, never wall clock — a duration
measured against a clock the OS can move backwards is a lie. The call is
in a `finally`, so a handler that throws is still logged (as 500) and the
exception still propagates; `finally` needs no catch clause and therefore
no reader conditional, which is exactly why it is used here.
raw docstring

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close