duct

weavejester 2025-10-03T00:10:43.803369Z

I've added an introduction page to https://duct-framework.org/. The docs are now at https://duct-framework.org/docs and of course there's a big button on the new index page linking to them.

2
❤️ 8
🎉 1
2025-10-03T08:26:18.275859Z

I was following the latest version of the documentation and I can reproduce an error consistently. Under the 3. Web Applications section, the third example of middleware configuration fails. My understanding is that the code (update response :headers merge headers) attempts to update the :headers key and it doesn't exist in response. It seems that when the middleware is configured at the route level, the other middlewares are not applied (you don't get a ring response back from the (handler ,,,) call).

2025-10-03T08:26:54.727289Z

java.lang.IllegalArgumentException: Key must be integer
                 APersistentVector.java:410 clojure.lang.APersistentVector.assoc
                  APersistentVector.java:19 clojure.lang.APersistentVector.assoc
                                RT.java:847 clojure.lang.RT.assoc
                               core.clj:193 clojure.core/assoc
                              core.clj:6261 clojure.core/update
                              core.clj:6251 clojure.core/update
                           middleware.clj:8 todo.middleware/wrap-headers[fn]
                                 web.clj:91 duct.middleware.web/wrap-hiccup[fn]
[...]

2025-10-03T09:12:07.595839Z

The call to (handler ,,,) with :middleware key defined at route level returns:

[:html {:lang "en"} [:head [:title "Hello world"]] [:body [:h1 "Hello world"]]]
Moving the :middleware to the :duct.module/web config
{:status 200, :headers {"Content-Type" "text/html;charset=UTF-8", "Set-Cookie" ("ring-session=259915dc-4275-4b2a-bae2-952f51ea10d6; Path=/; HttpOnly; SameSite=Strict"), "Content-Length" "111", "X-Frame-Options" "SAMEORIGIN", "X-Content-Type-Options" "nosniff"}, :body "\nHello world

Hello world

"}

weavejester 2025-10-03T12:45:39.119619Z

Thanks for the report. Let me look into it.

👍🏽 1
weavejester 2025-10-03T12:53:43.601179Z

This is for the :route-middleware example, right? Looks like the module puts the Hiccup middleware after the user middleware, when it should go before.

2025-10-03T12:56:21.917279Z

No, the 3rd example, defining the middleware at next to the :get definition ...

{:system
 {:duct.module/logging {}
  :duct.module/web
  {:features #{:site :hiccup}
   :routes [["/" {:get :todo.routes/index
                  :middleware [#ig/ref :todo.middleware/wrap-headers]}]]}

  :todo.middleware/wrap-headers {"Index-Only" "True"}}}

2025-10-03T12:57:19.584329Z

The :route-middleware works fine

weavejester 2025-10-03T12:59:42.029989Z

Ahh, right. So the ordering was correct for :route-middleware , but of course if we add middleware to the routes directly that comes before the global route middleware... and that sounds like the correct thing to do.

weavejester 2025-10-03T13:01:07.453979Z

Let me give this a little thought.

weavejester 2025-10-03T13:04:19.961719Z

I've been going back and forth on what the best option for the Hiccup middleware is. Returning a raw vector is convenient, but perhaps introduces more problems.

2025-10-03T13:31:11.554689Z

Perhaps it's just a documentation update, stating that when you define the middleware in the route, it becomes the first in the chain? (or something along those lines) ... and the documentation must specify that the middleware code also needs to be updated, from (update response :headers merge headers) to {:headers headers :body response}

weavejester 2025-10-03T13:36:46.584559Z

I think the issue with the documentation is indicative of a more fundamental problem with the Hiccup middleware. It might be that it's overall more predictable if it expects a handler to return {:body [:html ...]}.

👍🏽 1
weavejester 2025-10-03T14:49:44.305159Z

So I've thought about this a little, and I think the solution is to remove the feature that allows you to return a standalone Hiccup vector. Instead, the {:body [:html ...]} syntax will be favoured. The reason for this is that returning a vector breaks any middleware that expects a response map. Now, the {:body [:html ...]} form is a incomplete response, in that it lacks keys for :status and :headers, but middleware can typically deal with missing keys in a map far better than they can deal with an entirely different type. Ultimately I think we need something more flexible than Muuntaja for content negotiation. Ideally I want a way of returning a bunch of content in different formats, and then have the content negotiation pick the correct one. The problem with Muuntaja is that it's based around encoding a data structure in different formats - but it's entirely possible to return HTML and JSON from the same HTTP resource. However, that's a future project, I think.