Search code examples
clojurehttp-headersring

Add content-type header to file-response


I have the following endpoint handler (using clj/ring), and it works fine, but it doesn't include a Content Type header in the response, which might be a reason why the returned video does not play in iOS/Safari.

(def stream-partial-media
  {:summary "Stream partial media referenced by file-key"
   :parameters {:path {:file-key uuid?}}
   :handler (fn [{{{:keys [file-key]} :path} :parameters}]
              (let [file-key-res (file-keys/READ-UNEXPIRED file-key)]
                (if (nil? file-key-res)
                  {:status 404
                   :body {:message "file-key not found"}}
                  (let [user-res (users/READ (:user-id file-key-res))]
                    (if (or (:dev env) (:prod env))
                      (log-ut/log-media-access {:file-id (str (:file-id file-key-res))
                                                :username (:username user-res)}))
                    (file-response (utils/file-id-to-path (:file-id file-key-res)))))))})

I want to make the endpoint dynamically return a Content Type, based on the extension of the requested file (mp4/mp3/etc.). I found that ring has a built in wrap-content-type function that uses the file's extension to add a Content Type header, but I do not know where to implement it. I tried putting it in a few different places, but still have no Content Type header.

I would like to know either how to correct implement this wrapper, or how to check the file extension and manually add a header on that basis: mp4=>video/mp4, mp3=>audio/mp3.


Solution

  • From the request you have to get the filename, from which you can get the extension. Then based on the extension you can add the "Content-Disposition" to the headers map. Note that Ring request is just a map and the response as well. Something like this might work:

    (require '[ring.util.response :refer [header]])
    
    (defn extension [s]
      (second (re-find #"\.([a-zA-Z0-9]+)$" s)))
    
    (extension "foo.mp3")
    ;; => "mp3"
    
    (extension "foo.mp4")
    ;; => "mp4"
    
    (extension "foo")
    ;; => nil
    
    (def stream-partial-media
      {:summary "Stream partial media referenced by file-key"
       :parameters {:path {:file-key uuid?}}
       :handler (fn [{{{:keys [file-key]} :path} :parameters :as request}]
                  (let [file-key-res (file-keys/READ-UNEXPIRED file-key)]
                    (if (nil? file-key-res)
                      {:status 404
                       :body {:message "file-key not found"}}
                      (let [user-res (users/READ (:user-id file-key-res))]
                        (when (or (:dev env) (:prod env))
                          (log-ut/log-media-access {:file-id (str (:file-id file-key-res))
                                                    :username (:username user-res)}))
                        (-> (file-response (utils/file-id-to-path (:file-id file-key-res)))
                            (header "Content-Type"
                                    (case (extension (:filename request))
                                      :mp4 "video/mp4"
                                      :mp3 "audio/mp3")))))))})
    
    

    Above Ring's (header "Content-Type" <value>) just adds {:header {"Content-Type" <value>} to the response. The assumption is the :filename key is available in the request.

    I would also add some extra check to see if the filename is correct, and otherwise return an error.