Here's my first attempt of a catcher interceptor on Pedestal:
(definterceptorfn catcher []
(interceptor
:error (fn [context error]
{:status 500
:body (->> error .toString (hash-map :error) json/write-str)
:headers {"Content-type" "application/json"}})))
As I could test, by adding (/ 1 0) to my code, the function does get called, but the client gets an empty response with status 200, instead of the response in the map. I wonder why it is so.
There is nothing fancy in my routes variable:
(defroutes routes
[[["/api"
^:interceptors [(body-params/body-params) (catcher) bootstrap/html-body]
...
As Tim Ewald explained, I was returning a response map when a context was needed.
Fixed with
(definterceptorfn catcher []
(interceptor
:error (fn [context error]
(assoc context :response
{:status 500
:body (->> error .toString (hash-map :error) json/write-str)
:headers {"Content-type" "application/json"}}))))
Related
I have a server hosting my API. My API relies on data requested from a third-party API (Spotify). Here are the relevant parts of my API handler:
(ns myapp.api.handler
(:require
[compojure.api.sweet :refer :all]
[ring.util.http-response :refer [ok forbidden no-content not-found bad-request]]
[clj-spotify.core :as spotify]))
(defroutes api-routes
(api
{:middleware [wrap-api]
:swagger {:ui "/api-docs"
:spec "/swagger.json"
:data {:info {:title "My API"
:description "A description for My API"}
:consumes ["application/json"]
:produces ["application/json"]}}}
(context "/api" []
(context "/me" []
(PUT "/player" []
:query-params [device_id :- String]
(handle-player-put device_id))))))
As you'll be able to tell from my route handler, I'd essentially like to forward the response of the third-party API to my API. Here is the handler function, handle-player-put:
(defn handle-player-put [device-id]
(let [available-devices (-> (spotify/get-current-users-available-devices
{}
(lm/oauth-token :spotify))
:devices)]
(doseq [device available-devices]
(when (= (:id device) device-id)
(if (not (:is_restricted device))
(let [response (spotify/transfer-current-users-playback
{:device_ids [device-id]
:play false}
(lm/oauth-token :spotify))]
(case (-> response :error :status)
nil (no-content)
404 (do
(println "Playback response: 404")
(not-found "Spotify could not find the requested resource."))
{:status (-> response :error :status)
:headers {}
:body (-> response :error :message)})))))))
After a successful (spotify/transfer-current-users-playback) request, response binds to {}. An example of a response after an error looks like {:error {:status 502, :message "Bad gateway."}}
No matter whether transfer-current-users-playback is successful or not, I always get a 404 error (with body text Not Found [404]). What am I doing wrong?
doseq always returns nil so your handler returns nil - which is interpreted by compojure as “this handler won’t handle the request; skip to the next handler” and if no other handler handles the request you get a 404 not found.
You should not use (doseq … (when … expr))) if you need to return expr
I have the following code to define my routes in Compojure:
(ns my-project.my-test
(:gen-class)
(:require
[my-test.template-views :refer :all]
[compojure.core :refer [defroutes GET POST context]]
[compojure.route :as route]
[org.httpkit.server :refer [run-server]]))
(defn wrap-request
[handler]
(fn [request]
(let [{remote-addr :remote-addr uri :uri scheme :scheme request-method :request-method} request]
(println (str "REQUEST: " request)))
(handler request)))
(defroutes app
(wrap-request
(GET "/" request
{:status 200
:headers {"Content-Type" "text/html"}
:body (template-body (:uri request))}))
(wrap-request
(GET "/page1" request
{:status 200
:headers {"Content-Type" "text/html"}
:body (template-body (:uri request))}))
(wrap-request
(GET "/page2" request
{:status 200
:headers {"Content-Type" "text/html"}
:body (template-body (:uri request))}))
(wrap-request
(GET "/page3" request
{:status 200
:headers {"Content-Type" "text/html"}
:body (template-body (:uri request))}))
(route/resources "/")
(route/not-found {:status 404
:headers {"Content-Type" "text/html"}
:body "<h1>Not Found</h1>"}))
That works but it seems like I should be able to simplify it like this:
(ns my-project.my-test
(:gen-class)
(:require
[my-test.template-views :refer :all]
[compojure.core :refer [defroutes GET POST context]]
[compojure.route :as route]
[org.httpkit.server :refer [run-server]]))
(defn wrap-request
[handler]
(fn [request]
(let [{remote-addr :remote-addr uri :uri scheme :scheme request-method :request-method} request]
(println (str "REQUEST: " request)))
(handler request)))
(defn wrap-template
[route]
(wrap-request
(GET route request
{:status 200
:headers {"Content-Type" "text/html"}
:body (template-body (:uri request))})))
(defroutes app
(map wrap-template ["/" "/page1" "/page2" "/page3"])
(route/resources "/")
(route/not-found {:status 404
:headers {"Content-Type" "text/html"}
:body "<h1>Not Found</h1>"}))
However, when I do, I get this error backtrace:
Sat Apr 24 22:38:33 MDT 2021 [worker-2] ERROR - GET /page2
java.lang.ClassCastException: class clojure.lang.LazySeq cannot be cast to class clojure.lang.IFn (clojure.lang.LazySeq and clojure.lang.IFn are in unnamed module of loader 'app')
at compojure.core$routing$fn__368163.invoke(core.clj:185)
at clojure.core$some.invokeStatic(core.clj:2705)
at clojure.core$some.invoke(core.clj:2696)
at compojure.core$routing.invokeStatic(core.clj:185)
at compojure.core$routing.doInvoke(core.clj:182)
at clojure.lang.RestFn.applyTo(RestFn.java:139)
at clojure.core$apply.invokeStatic(core.clj:669)
at clojure.core$apply.invoke(core.clj:662)
at compojure.core$routes$fn__368167.invoke(core.clj:192)
at org.httpkit.server.HttpHandler.run(RingHandler.java:117)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at java.base/java.lang.Thread.run(Thread.java:834)
Is there something about using (map) that is wrong here?
routes (and thus defroutes) expects each argument to be a request handler function. A list of handlers is not a handler function; hence the error. Happily, there is a function to convert a list of handlers to a single handler: routes! Since it wants N separate arguments, rather than a single list, you will need apply as well. So:
(defroutes app
(apply routes (map wrap-template ["/" "/page1" "/page2" "/page3"]))
(route/resources "/")
(route/not-found {:status 404
:headers {"Content-Type" "text/html"}
:body "<h1>Not Found</h1>"}))
As an aside, I generally suggest not using defroutes, simply because it does not compose as easily as separate def + routes, and for beginners it leads to forgetting that anything but defroutes exists, when in fact most interesting servers will want to apply a function to some of their routes.
I'm pretty new in the Clojure webdev ecosystem, I want to send a JSON response with the POST method using the liberator API, I tried this:
(POST "/post/savecomment"
request
(resource
:allowed-methods [:post]
:available-media-types ["application/json"]
:handle-ok (fn [ctx]
(format (str "{body: %s a: 1 b: 4}"), "the body part"))))
All looks fine, there is no error message, I get a "201 Created" response from ring, but the JSON data is not send, in Chrome "response" tab is just empty. Need I to add something? BTW, I'm using compojure, not compojure-api.
I also tried:
(POST "/post/savecomment" request (resource
:allowed-methods [:post]
:available-media-types ["application/json"]
:available-charsets ["utf-8"]
:handle-ok (fn [_] (rep/ring-response {:status 200 :body "\"this is json\""}))
:post! (fn [ctx] (rep/ring-response {:status 666 :body "\"this is json\""}))
))
But no luck.
For 201 Created responses you need to define the handler :handle-created, e.g.
(POST "/post/savecomment"
request
(resource
:allowed-methods [:post]
:available-media-types ["application/json"]
:handle-created (fn [ctx]
(format (str "{body: %s a: 1 b: 4}"), "the body part"))))
The tutorial covers the fundamental concepts of liberator: https://clojure-liberator.github.io/liberator/tutorial/
I can't figure out where I'm going wrong with the following route:
(ns mds.routes.api
(:require [mds.db.core :refer [*db*] :as db]
[compojure.core :refer [defroutes POST]]
[ring.util.http-response :as response]
[clojure.walk :as walk]))
(defroutes api-routes
(POST "/student" request
(let [{body :body} request]
(let [student (walk/keywordize-keys body)]
(try
(db/create-student! student)
{:saved true
:error nil
:student student}
(catch Exception e {:saved false
:error e
:student nil})
)))))
I'm trying to return a response body with a json object that looks something like:
{
"saved":"true",
"error":"nil",
"student": {...}
}
But I'm just getting empty response bodies. The db/create-student! call works fine, and w/o the (try) expression I get either the JSON body or a 500 error, but with the (try) expression I get an empty status 200 response every time.
How do I get the (try) expression to return the map and pass it up to the response handler?
It's hard to guess what could be causing the empty responses with the try in place unless some exception is being thrown due to some other reason. Perhaps there is a difference between what's running and what's in the source file because of some typo or a renamed function. One thing to consider is that exceptions can be thrown further up the middleware stack as well as in the handler.
If you have this wrapped in middleware that converts the responses from clojure datastructures (.edn) to json, that middleware might be throwing an exception trying to serialize an exception. in this case e. try this as a test:
(defroutes api-routes
(POST "/student" request
(let [{body :body} request
student (walk/keywordize-keys body)]
(try
(db/create-student! student)
{:saved true
:error nil
:student student}
(catch Exception e {:saved false
:error (.getMessage e)
:student nil})))))
and check the output/logs/nrepl-buffer for exceptions about exceptions being thrown while generating the response in the catch expression.
if you don't have any json-response forming middleware elsewhere then try something like this:
(defroutes api-routes
(POST "/student" request
(let [{body :body} request
student (walk/keywordize-keys body)]
(try
(db/create-student! student)
{:status 200
:body (str {:saved true
:error nil
:student student})}
(catch Exception e {:status 401
:body (str {:saved false
:error (.getMessage e)
:student nil})})))))
where you set the response code explicitly and see if you can figure out whats going on.
Wrapping the whole code with Exception class it bad practice because it's too wide case. It prevents you from seeing really important errors such as missing files, wrong configuration, invalid arguments and other architectural errors.
But if you really need to catch all the exceptions, at least you may either print or return a stack trace to see what's going on:
(ns foo
(require [clojure.stacktrace :as trace]))
...
;; your handler goes here
(let [trace-string (with-out-str
(trace/print-throwable e))]
{:status 500
:body trace-string})
where e is your exception instance.
I have a small compojure site, with the routes defined as such:
(defroutes example
(GET "/" [] {:status 200
:headers {"Content-Type" "text/html"}
:body (home)})
(GET "/*" (or (serve-file (params :*)) :next))
(GET "/execute/" [] {:status 200
:headers {"Content-Type" "text/html"}
:body (execute-changes)})
(GET "/status/" [] {:status 200
:headers {"Content-Type" "text/html"}
:body (status)})
(route/not-found "Page not found"))
When I try to load the project, I get this error:
java.lang.Exception: Unsupported binding form: (or (serve-file (params :*)) :next)
What am I doing wrong? I took most of this from scattered examples on the internet.
After adding the empty vector, I get this error:
java.lang.Exception: Unable to resolve symbol: serve-file in this context
I think you're missing a binding form:
(GET "/*" {params :params} (or (serve-file (params :*)) :next))
; ^- note the binding form