2
votes

Synopsis

Please note, this question isn't about how to serve a static file — that's working — it's about the special case when wrap-file delivers an index file by default and, for lack of a file extension in the URL, the wrong mime-type is being assigned to the Content-Type header.

How does one get the correct mime type on index files served by default?

Current answers don't address how to do this yet, and the workaround I've come up with doesn't scale.


Working Code

Here's a simplified fragment from a Clojure application using Compojure and Ring middleware:

(def app
   (-> handler
      (wrap-file "public")   ; If the route is a static file in public, serve it instead
      (wrap-content-type)))  ; Deduce and add the proper Content-Type header

The intent is to serve up any routes, but if there's a local file in the public directory serve it instead, finally add a meaningful Content-Type header with the corresponding mime type. All this works perfectly.


The Problem

When I browse to the base URL, it does serve index.html as expected, but it does not get a Content-Type of text/html, but rather application/octet-stream.

This begs the question, how to assign Content-Type by contents of the response's body?

However, it's ill-advised to have middleware reads the :body common problems, because it's a mutable InputStream that can only be read once. So that's obviously not the right way.

Is there a better way to serve the index.html by default?

An Ugly Workaround

The current ugly workaround is to have a special-case route that sets the Content-Type manually. <cringe/>

Worse, this solution doesn't scale, should an index file be served from a subdirectory.

Consequently, I'm looking for a Middleware solution, not a routing hack.

Experiments

Exploring the Execution Order of the Middleware and Its Consequences:

Admittedly, although I understand the thread macro (->) in that (-> x A B) transforms into (B (A x) ), I still get a little jumbled in my head when working out the order that the execution flow resolved through a middle-ware chain to an eventual handler with routes. The reason for this stumbling is that code can mess with the request before calling the the handler it was passed, as well as fiddle with the response before returning. The order things need to be in doesn't feel "obvious" to know when I'm augmenting the request with details going in or twiddling with the response coming out, or the more complicated case of doing a different behavior based on some condition.

e.g., Does wrap-file happen "before" or "after" the handler has constructed a response, as the order matters in the threading? I feel this should be more intuitive to me, without having to run to the source code as much as I'm doing.

As it appears possible to have middleware applied only when a specific route matches, it may be that I'm making more of a distinction between Middleware and Handlers than perhaps I should.

Swapping the order (to test the threading-order assumptions) does not do what you think:

(def app                     ; THIS IS AN EXAMPLE OF BROKEN CODE - DON'T USE IT
   (-> handler
      (wrap-content-type)))  ; WRONG ORDER - DON'T DO THIS   (EXAMPLE ONLY)
      (wrap-file "public")   ; WRONG ORDER - DON'T DO THIS   (EXAMPLE ONLY)

It "works," but for the wrong reason. The index.html file will get delivered and renders "properly," but only because there is no Content-Type added. The browser, for lack of a specified mime type, makes an educated guess and happens to guess correctly.

Since the goal is to have a Content-Type in the header, this suggests the threaded order was correct to start with.

What Middleware Should Be Used To Deliver Index Pages?

So with information in hand of what not to do, what is it I should be doing to deliver the default status index.html file when the URL doesn't specify it by name, since there's no extension to examine?

Is there a better middleware stack, or even a recommended one, that someone could walk me through?


UPDATE 2020-05-24: Submitted Ring Issue 480; turns out this may be a design bug looking for a contributor.

1

1 Answers

1
votes

This gives you a server which will serve index.html if present inside a resources/public/ folder.

(ns core
  (:require [compojure.core :refer [routes GET]]
            [ring.middleware.defaults :refer [wrap-defaults]]
            [org.httpkit.server :as http-kit]))

(def handler
  (routes
    (GET "/foo" [] "Hello Foo")
    (GET "/bar" [] "Hello Bar")))

(def app
  (-> handler
   (wrap-defaults {:static {:resources "public"
                            :files     "resources/public"}})))


(def server (http-kit/run-server app {:port 8889}))

(comment
  ;; To stop the server
  (server))

I'm using wrap-defaults as it provides a nice way to get a server up and running, while still providing a lot of flexibility to drop in customisations as required.

In this case I'm telling it to use public as a resources folder and also handing it resources/public to files so it can correctly wrap the files to be served.

ring.middleware.content-type defaults to application/octet-stream when it has insufficient information to guess the content type of the file it is serving.

If you specifically just want to serve files+provide routing, the answer I have given above is sufficient, if you want to explicitly return a Content-Type text/html for index.html, then you will need to wrap the content type using [ring.util.response :refer [content-type]].

So for example:

(GET "/" [] (content-type (io/resource "index.html") "text/html"))

I've normally done this by detecting the file extension in the request url and then returning the correct content-type, with a special case for things like index.html.

You need at least these deps, this is in deps.edn format, but just change it to [ring/ring-core "1.8.0"] for example if you need it in lein's project.clj form instead:

ring/ring-core                    {:mvn/version "1.8.0"}
ring/ring-defaults                {:mvn/version "0.3.2"}
http-kit                          {:mvn/version "2.3.0"}
compojure                         {:mvn/version "1.6.1"}

Let me know if you have any issues!