Fork me on GitHub
#pedestal
<
2024-02-16
>
agorgl13:02:03

Hello! I've tried replacing the resource interceptor with fast-resource interceptor to use the :index? functionality like this:

(ns com.example.demoapp.service
  (:require
   [io.pedestal.http :as http]
   [io.pedestal.http.route :refer [routes-from]]
   [io.pedestal.http.ring-middlewares :as middlewares]
   [io.pedestal.environment :refer [dev-mode?]]
   [com.example.demoapp.routes :as routes]))

(defn static-content-interceptors [service-map]
  (let [resource-path (::http/resource-path service-map)
        fast-resource (middlewares/fast-resource resource-path)]
    (-> service-map
        (update
         ::http/interceptors
         #(->> %
               (mapcat
                (fn [item]
                  (if (= (:name item) ::middlewares/resource)
                    [fast-resource]
                    [item])))
               (into []))))))

(defn service-map [opts]
  (let [{:keys [dev-mode]
         :or {dev-mode dev-mode?}} opts]
    (-> {::http/port 8080
         ::http/type :jetty
         ::http/routes (routes-from (routes/routes))
         ::http/resource-path "public"
         ::http/secure-headers {:content-security-policy-settings
                                {:script-src "'self' 'unsafe-inline' 'unsafe-eval' https: http:"
                                 :object-src "'none'"}}
         ::http/join? false}
        http/default-interceptors
        static-content-interceptors
        (cond-> dev-mode http/dev-interceptors))))

(comment
  (-> (service-map nil)
      ::http/interceptors))
While it fetches static content from the ::http/resource-path correctly, it responds with Content-Type: application/octet-stream instead of Content-Type: text/html (or the respective content type of the fetched item) Is this a problem with fast-resource interceptor or content-type interceptor? Is there a better way to achieve the :index? functionality that the fast-resource interceptor uses?

agorgl16:02:52

To answer my own question, it seems that even though fast-resource interceptor supports the :index? functionality it has some major drawbacks: • For uri s that end in "/", even when it picks a matching index file it does not update the request map and the content-type interceptor checks and assigns a content type according to the request :uri property • Due to hardcoded File usage does not work from within an uberjar

agorgl16:02:13

So in the end I wrote this simple dir-index interceptor to match my needs:

(ns com.example.demoapp.service
  (:require
   [clojure.string :as str]
   [io.pedestal.http :as http]
   [io.pedestal.http.route :refer [routes-from]]
   [io.pedestal.http.ring-middlewares :as middlewares]
   [io.pedestal.interceptor :refer [interceptor]]
   [io.pedestal.environment :refer [dev-mode?]]
   [ring.middleware.resource :as resource]
   [ring.util.request :as ring-request]
   [com.example.demoapp.routes :as routes]))

(defn dir-index [root-path]
  (interceptor
   {:name ::dir-index
    :enter (fn [context]
             (let [{:keys [request]} context
                   path (ring-request/path-info request)]
               (if (str/ends-with? path "/")
                 (let [index-path (str path "index.html")
                       index-request (-> request
                                         (assoc :path-info index-path)
                                         (assoc :uri index-path))
                       response (resource/resource-request index-request root-path)]
                   (if response
                     (-> context
                         (assoc :request index-request)
                         (assoc :response response))
                     context))
                 context)))}))

(defn static-content-interceptors [service-map]
  (let [resource-path (::http/resource-path service-map)
        dir-index (dir-index resource-path)]
    (-> service-map
        (update
         ::http/interceptors
         #(->> %
               (mapcat
                (fn [item]
                  (if (= (:name item) ::middlewares/resource)
                    [item dir-index]
                    [item])))
               (into []))))))

(defn service-map [opts]
  (let [{:keys [dev-mode]
         :or {dev-mode dev-mode?}} opts]
    (-> {::http/port 8080
         ::http/type :jetty
         ::http/routes (routes-from (routes/routes))
         ::http/resource-path "public"
         ::http/secure-headers {:content-security-policy-settings
                                {:script-src "'self' 'unsafe-inline' 'unsafe-eval' https: http:"
                                 :object-src "'none'"}}
         ::http/join? false}
        http/default-interceptors
        static-content-interceptors
        (cond-> dev-mode http/dev-interceptors))))

(comment
  (-> (service-map nil)
      ::http/interceptors))

phill17:02:36

The interceptor couples 2 things that you could ply apart for greater flexibility. thing 1) translates xyz/ to xyz/index.html, thing 2) responds as if someone asked for xyz/index.html. However, your app can probably already respond (or at least it ought to be able) to the xyz/index.html request. So this interceptor could limit itself to tweaking the request uri, and let downstream interceptors handle the resulting tweaked uri.

agorgl17:02:13

The interceptor is inserted right after the ::middlewares/resource interceptor as a fallback for it

phill17:02:02

Concretely, the job of knowing xyz/ === xyz/index.html is distinct from the job of knowing that index.html will definitely be a resource. Pedestal's scheme of interceptors is ideal for this scenario, because infer-index-html-interceptor can have only an Enter half (which adjusts the uri) and can precede the resource interceptor that knows how to respond to xyz/index.html

agorgl17:02:49

So first the ::middlewares/resource interceptor tries to fetch the requested url from ::http/resource-path and if it fails, dir-index checks if the path is a directory (ends with /) and if so it tries in a similar way to fetch an index.html from the ::http/resource-path. It also updates the :request in context along with the :response so when the content-type leave interceptor runs it assigns the proper Content-Type header

agorgl17:02:54

Yeah I suppose I could also make an interceptor that just checks and modifies the uri instead of actually loading the resource as I do

🎯 1
agorgl17:02:18

Add this before resource interceptor, and let the resource interceptor do the loading

phill17:02:32

Then it would work equally well for resource-served index.html and (theoretically) index.html generated by other means by other interceptors

agorgl17:02:32

Yeah I think the problem with that is that we cannot know beforehand that an uri ending in / has to be translated into an index.html

agorgl17:02:15

Because we want uris like /api/products/ to still work and not be mapped to /api/products/index.html

agorgl17:02:01

The way resource interceptor works is that it just tries to find the :uri in the resource-path first, and if it fails the interceptor chain continues to the router interceptor

agorgl17:02:02

I want dir-index to work this way too

agorgl17:02:28

The problem with that, is that we actually have to do the same type of file/resource lookup that the resource interceptor does in our interceptor (thus duplicating it, only the lookup part)

agorgl17:02:30

It also seems that the current resource/resource-request does not actually load the data, it just constructs a ring response with a resource path to them? So my interceptor kinda does what you describe?

agorgl17:02:13

The only real difference would be now to just assign the new :request to the context, do not assign a :response and add the interceptor before the resource interceptor

agorgl17:02:53

Yeah I updated my interceptor to follow the discussed concept, here it is for future reference:

(ns com.example.demoapp.service
  (:require
   [clojure.string :as str]
   [io.pedestal.http :as http]
   [io.pedestal.http.route :refer [routes-from]]
   [io.pedestal.http.ring-middlewares :as middlewares]
   [io.pedestal.interceptor :refer [interceptor]]
   [io.pedestal.environment :refer [dev-mode?]]
   [ring.middleware.resource :as resource]
   [ring.util.request :as ring-request]
   [com.example.demoapp.routes :as routes]))

(defn dir-index [root-path]
  (interceptor
   {:name ::dir-index
    :enter (fn [context]
             (let [{:keys [request]} context
                   path (ring-request/path-info request)]
               (if (str/ends-with? path "/")
                 (let [index-path (str path "index.html")
                       index-request (-> request
                                         (assoc :path-info index-path)
                                         (assoc :uri index-path))
                       response (resource/resource-request index-request root-path)]
                   (if response
                     (assoc context :request index-request)
                     context))
                 context)))}))

(defn static-content-interceptors [service-map]
  (let [resource-path (::http/resource-path service-map)
        dir-index (dir-index resource-path)]
    (-> service-map
        (update
         ::http/interceptors
         #(->> %
               (mapcat
                (fn [item]
                  (if (= (:name item) ::middlewares/resource)
                    [dir-index item]
                    [item])))
               (into []))))))

(defn service-map [opts]
  (let [{:keys [dev-mode]
         :or {dev-mode dev-mode?}} opts]
    (-> {::http/port 8080
         ::http/type :jetty
         ::http/routes (routes-from (routes/routes))
         ::http/resource-path "public"
         ::http/secure-headers {:content-security-policy-settings
                                {:script-src "'self' 'unsafe-inline' 'unsafe-eval' https: http:"
                                 :object-src "'none'"}}
         ::http/join? false}
        http/default-interceptors
        static-content-interceptors
        (cond-> dev-mode http/dev-interceptors))))