ring

gtbono 2025-01-10T02:09:05.790409Z

folks, can someone help me figure out how to setup ring + component the right way? I will be giving more details here in the thread, but I want to be able to change my routes, eval the route and then have it applied to ring (only worried about development at the time). Nowadays I am forced to stop and start the component for changes to be visible in the API, ideally, I would only stop and start the component if I change something in the component.

gtbono 2025-01-10T02:09:21.236689Z

my component start function is as follows (big, I know...) but the important part is http-server/routes, which points to my reitit configuration file:

(defrecord Ring [ring service database redis config firebase]
  component/Lifecycle

  (start [component]
    (println "Starting API Server")
    (if service
      component
      (assoc component :service (run-jetty
                                 (wrap-with-request-id
                                  (logger/wrap-with-logger
                                   (wrap-format
                                    (wrap-params
                                     (ring/ring-handler
                                      (ring/router
                                       (http-server/routes)
                                       {:exception pretty/exception
                                        :validate spec/validate
                                        :data {:coercion coercion-schema/coercion
                                               :muuntaja m/instance
                                               :middleware [swagger/swagger-feature
                                                            openapi/openapi-feature
                                                            parameters/parameters-middleware
                                                            muuntaja/format-negotiate-middleware
                                                            muuntaja/format-response-middleware
                                                            exception-middleware
                                                            muuntaja/format-request-middleware
                                                            multipart/multipart-middleware
                                                            (create-inject-dependencies-middleware database redis config firebase)]}})
                                      (ring/create-default-handler))))
                                   {:logger (fn [data] (timbre/info data))
                                    :printer :no-color
                                    :redact-keys #{:authorization :password :cookie :Set-Cookie}})) ring))))
the http-server/routes is a function that returns a map of my routes, basically, they are as follows:
(defn routes
  []
  [["/birthdays"
      {:get {:description "Shows all birthdays of a user"
             :responses {200 {:description "Successfully fetched all birthdays"
                              :body responses.birthday/BirthdaysResponse}}
             :handler #'birthday-all-handler}}]]]])
and the birthday-all-handler is a function, which is irrelevant to this, but I have looked into a lot of examples, and can't figure out a way to setup ring correctly, I want to just eval a change made in the routes, or in the handler, and have it applied to ring without restarting the whole component everytime. Does anyone have a clue on what may be wrong?

2025-01-10T03:21:56.249909Z

It may not be possible when constructing your routes as fully realized data like that

2025-01-10T03:22:32.194259Z

What you need is some mutable indirection, like using var quote for a handler in regular ring

2025-01-10T03:22:51.360819Z

You might try asking in #reitit

gtbono 2025-01-10T03:24:00.271209Z

what do you mean by routes as fully realized data?

2025-01-10T03:36:50.921269Z

Oh, actually I missed it, birthday-all-handler is specified via a var

2025-01-10T03:37:13.204809Z

So I would expect any changes to that handler to be immediately reflected

2025-01-10T03:39:10.276349Z

But the rest of the route structure is specified as an immutable datastructure that reitit then compiles into something, there is no mutable reference to allow for changing it

2025-01-10T03:40:40.142869Z

You could add one by sticking the built ring handler in an atom, then passing run-jetty a function that derefs the atom and calls the fn in there on its args

2025-01-10T03:41:19.261179Z

You would then need to add some kind of rebuild/refresh mechanism that rebuilds the handler and updates the atom

gtbono 2025-01-10T05:10:53.862379Z

(start [component]
    (println "Starting API Server")
    (if service
      component
      (assoc component :service (run-jetty
                                 (wrap-with-request-id
                                  (logger/wrap-with-logger
                                   (wrap-format
                                    (wrap-params
                                     (ring/reloading-ring-handler
                                      (fn [] (ring/ring-handler
                                              (ring/router
                                               (http-server/routes)
                                               {:exception pretty/exception
                                                :validate spec/validate
                                                :data {:coercion coercion-schema/coercion
                                                       :muuntaja m/instance
                                                       :middleware [swagger/swagger-feature
                                                                    openapi/openapi-feature
                                                                    parameters/parameters-middleware
                                                                    muuntaja/format-negotiate-middleware
                                                                    muuntaja/format-response-middleware
                                                                    exception-middleware
                                                                    muuntaja/format-request-middleware
                                                                    multipart/multipart-middleware
                                                                    (create-inject-dependencies-middleware database redis config firebase)]}})
                                              (ring/create-default-handler))))))
                                   {:logger (fn [data] (timbre/info data))
                                    :printer :no-color
                                    :redact-keys #{:authorization :password :cookie :Set-Cookie}})) ring))))

gtbono 2025-01-10T05:11:37.044319Z

@ikitommi do you have any idea if reloading-ring-handler would work in this case?

ikitommi 2025-01-10T05:56:28.307509Z

It rebuilds the routing tree on each request, so great for development/repl flow

ikitommi 2025-01-10T05:56:37.855789Z

so, yes

Eugen 2025-01-10T08:45:55.525899Z

if you are curios, I'm using mount instead of component (with reitit) and it's working fine for reload - see • https://github.com/netdava/efactura-mea/blob/main/src/efactura_mea/main.cljhttps://github.com/netdava/efactura-mea/blob/main/src/efactura_mea/http_server.clj

gtbono 2025-01-11T06:26:21.868779Z

thanks everyone!! I'm still trying to figure out what's wrong, and I have zero idea, everything I do have no effect and I can only see the changes in my reitit routes or handles when I stop and start the component, I have tried to clean up the code a little bit and see if I could figure out what's wrong, also change the order of the handles and see if I get everything right, here's my entire ring component for reference, can anyone figure it out? I believe I'm using reloading-ring-handler the right but there's something here I'm not getting:

(ns birthdayapi.components.ring
  (:require
   [birthdayapi.diplomat.http-server :as http-server]
   [com.stuartsierra.component :as component]
   [muuntaja.core :as m]
   [muuntaja.middleware :refer [wrap-format wrap-params]]
   [reitit.coercion.malli]
   [reitit.coercion.schema :as coercion-schema]
   [reitit.dev.pretty :as pretty]
   [reitit.openapi :as openapi]
   [reitit.ring :as ring]
   [reitit.ring.malli]
   [reitit.ring.middleware.exception :as exception]
   [reitit.ring.middleware.multipart :as multipart]
   [reitit.ring.middleware.muuntaja :as muuntaja]
   [reitit.ring.middleware.parameters :as parameters]
   [reitit.spec :as spec]
   [reitit.swagger :as swagger]
   [ring.adapter.jetty :refer [run-jetty]]
   [ring.logger.timbre :as logger]
   [taoensso.timbre :as timbre]))

(defn exception-handler [data _exception _request]
  (let [{:keys [status body]} data]
    {:status (or status 500)
     :body body}))

(def exception-middleware
  (exception/create-exception-middleware
   (merge
    exception/default-handlers
    {com.google.firebase.auth.FirebaseAuthException (partial exception-handler {:status 403})

     ::exception/default (partial exception-handler {:status 500 :body "Server error"})

     ::exception/wrap (fn [handler e request]
                        (println {:error (pr-str (:uri request))
                                  :exception (pr-str e)})
                        (handler e request))})))

(defn wrap-dependencies
  [database redis config firebase handler]
  (fn [request]
    (handler (-> request
                 (assoc :system/database database)
                 (assoc :system/redis (:connection redis))
                 (assoc :system/firebase (:firebase firebase))
                 (assoc :system/config config)))))

(defn wrap-with-request-id
  [handler]
  (fn [request]
    (let [request-id (or (get-in request [:headers "x-request-id"])
                         (str (random-uuid)))]
      (timbre/with-context {:request-id request-id}
        (handler request)))))

(def ring-middlewares
  [swagger/swagger-feature
   openapi/openapi-feature
   parameters/parameters-middleware
   muuntaja/format-negotiate-middleware
   muuntaja/format-response-middleware
   exception-middleware
   muuntaja/format-request-middleware
   multipart/multipart-middleware])

(def router-options
  {:exception pretty/exception
   :validate spec/validate
   :data {:coercion coercion-schema/coercion
          :muuntaja m/instance
          :middleware ring-middlewares}})

(defn wrap-with-logger
  [handler]
  (logger/wrap-with-logger handler {:logger (fn [data] (timbre/info data))
                                    :printer :no-color
                                    :redact-keys #{:authorization :password :cookie :Set-Cookie}}))

(defn create-router
  []
  (ring/router
   (http-server/routes) router-options))

(defn create-ring-handler
  []
  (ring/ring-handler
   (create-router)
   (ring/create-default-handler)))

(defrecord Ring [ring service database redis config firebase]
  component/Lifecycle

  (start [component]
    (println "Starting API Server")
    (let [wrap-dependencies (partial wrap-dependencies database redis config firebase)]
      (if service
        component
        (assoc component :service
               (run-jetty
                (-> (ring/reloading-ring-handler create-ring-handler)
                    wrap-with-request-id
                    wrap-with-logger
                    wrap-format
                    wrap-params
                    wrap-dependencies)
                ring)))))

  (stop [component]
    (println "Stopping API Server")
    (when service
      (.stop service))
    (assoc component :service nil)))

(defn service
  [ring]
  (map->Ring {:ring ring}))