How to set the status code in Compojure? - clojure

I am writing a small website in Clojure and Compojure. I would like to set the HTTP response status for each request based on the data found or not found.
The last call is the html5 macro that returns to the handler the html that needs to be sent back to the browser. Is it possible to set the HTTP response status somehow here?
(ns myapp.views.layout
(:require
[hiccup.page :refer (html5 include-css include-js)]))
(defn layout [title & content]
(html5
(head title)
(body content)))

If you only return text that text will be the body of the response. If you return a map, the map can describe other aspects of the response.
(defn layout [title & content]
{:status 200
:body (html5 (head title) (body content))})

If you return a map containing
{:status NNN
:body (my-code-here)}
then the contents of the :status key will be the http response code.

Just to add some details that might be helpful or interesting to others, each of the return values of your Compojure route handlers "... is treated intelligently" and that intelligence is encapsulated in the "compojure.response/render multimethod".
Based on a cursory examination of the render code, the reason why returning a map works is that the map you return is merge-d with the Ring response map that Compojure implicitly creates.
You might also want to include :headers {"Content-Type" "text/html"} (or whatever's appropriate) in the map for your handler return values. A Unicode character in the page title in my responses wasn't being rendered correctly because the content type header was missing.

Related

Lacinia and re-graph incompatible headers

I'm using the last available lacinia version: "0.36.0-alpha-3" with Luminus (Ring+reitit), but this version asks for a specific header:
$ curl 'http://localhost:3000/api/graphql' -X POST --data "{test_by_id(id: 5) { title } }" -H 'Content-Type: application/graphql'
that request works fine, but without "'Content-Type: application/graphql'" the request wouldn't work. So I need to define my re-graph init vector like:
[::re-graph/init
{:ws-url nil
:http-url "http://localhost:3000/api/graphql"
:http-parameters {:with-credentials? false
:headers {"Content-Type" "application/graphql"}
}
:ws-reconnect-timeout nil
:resume-subscriptions? false
:connection-init-payload {}}]
but putting that header makes re-graph unable to work properly:
{"errors":[{"message":"Failed to parse GraphQL query.","extensions":{"errors":[{"locations":[{"line":1,"column":null}],"message":"mismatched input '\"query\"' expecting {'query', 'mutation', 'subscription',
it looks like re-graph sends and receives data using "application/json" header, so lacinia asks for some type of header but re-graph can't work with that option.
I had the same problem, and I think I got a solution for it. re-frame requests follows the Apollo Specification, as stated by #aarkerio. Here is the code to keep the original endpoint working with the origina specification, and allow it to respond to re-frame requests. This will make the endpoint respond to Graphiql request (from your http://localhost:3000/graphiql route), and re-graph ones. Any comments or corrections are welcomed.
Replace the original function set on the /graphql route on src/clj/mem_learning/routes/services.clj:
["/graphql" {:post graphql-call}
Add the graphql-call function on that same file:
(defn graphql-call [req]
(let [body (:body-params req)
content-type (keyword (get-in req [:headers "content-type"]))]
(case content-type
:application/json (ok (graphql/execute-request-re-graph body))
:application/graphql (ok (graphql/execute-request (-> req :body slurp))))))
add the execute-request-re-graph to the src/clj/mem_learning/routes/services/graphql.clj file:
(defn execute-request-re-graph
"execute request with re-graph/apollo format"
[{:keys [variables query context]}]
(lacinia/execute compiled-schema query variables context)))
ANSWER:
It looks that Luminus creates a middleware configuration:
(defn service-routes []
["/api"
{:coercion spec-coercion/coercion
:muuntaja formats/instance
:swagger {:id ::api}
:middleware [;; query-params & form-params
parameters/parameters-middleware
;; content-negotiation
muuntaja/format-negotiate-middleware
;; encoding response body
muuntaja/format-response-middleware
;; exception handling
exception/exception-middleware
;; decoding request body
muuntaja/format-request-middleware
;; coercing response bodys
coercion/coerce-response-middleware
;; coercing request parameters
coercion/coerce-request-middleware
;; multipart
multipart/multipart-middleware
]}
commenting the line "muuntaja/format-negotiate-middleware" makes the "application/json" call possible.
SECOND UPDATE (four hours later)
Ok, that muuntaja middleware thing was not the problem at all, the real problem is that curl send the data with the format:
{ test_by_id(id: 7, archived: false) { title } }
meanwhile re-graph uses:
{"query":"query { test_by_id(id: 7, archived: false) { title } }","variables":null}
this is a normal java string btw not a data structure, so we need to do some changes, first a new function:
(defn graphql-call [req]
(let [body (-> req :body slurp)
full-query (json/read-str body :key-fn keyword)
_ (log/info (str ">>> **** full-query >>>>> " full-query))]
(ok (graphql/execute-request full-query))))
we set the function:
["/graphql" {:post graphql-call}]
and in my_app.routes.services.graphql file:
(defn execute-request [{:keys [variables query context]}]
(json/write-str (lacinia/execute compiled-schema query variables context)))
and now re-graph works!
(also now I can send and use variables in GraphQL)
It's necessary to set:
:http-parameters {:with-credentials? false
:oauth-token "ah4rdSecr3t"
:headers {"Content-Type" "application/graphql"}
btw. Also, maybe it's better:
(lacinia/execute compiled-schema query variables context)
than:
(json/write-str (lacinia/execute compiled-schema query variables context))
because it interferes with re-graph importing the data already as a native ClojureScript map.

Getting the POST body data from a POST request to Pedestal

I have POSTed data to a Pedestal endpoint "/my-post. I have routed that end point as such:
[[["/" {:get landing} ^:interceptors [(body-params/body-params) ...]
["/my-post {:post mypost-handler}
....
So to my mind this means that the body-params interceptor will fire for /my-post too.
In mypost-handler I have:
(defn mypost-handler
[request]
****HOW TO ACCESS THEN FORM DATA HERE ****
)
How do I now access the form data here? I can see from printing the request that I have a #object[org.eclipse.jetty.sever.HttpInputOverHTTP..] which will clearly need further processing before it is useful to me.
(I must say, the documentation for Pedestal is pretty sketchy at best...)
Something like this should work. Note the body-params interceptor on the mypost-handler route
(defn mypost-handler
[{:keys [headers params json-params path-params] :as request}]
;; json-params is the posted json, so
;; (:name json-params) will be the value (i.e. John) of name property of the posted json {"name": "John"}
;; handle request
{:status 200
:body "ok"})
(defroutes routes
[[["/mypost-handler" {:post mypost-handler}
^:interceptors [(body-params/body-params)]
]
]])
The mypost-handler is acting as a Ring handler, i. e. it should accept a Ring request map and return a Ring response map. Thus, you can expect a typical Ring request structure:
(defn mypost-handler
[{:keys [headers params json-params path-params] :as request}]
;; handle request
{:status 200
:body "ok"})
Here's more relevant info on defining such handlers in your route tables.

Filter sensitive parameters from logs in clojure ring app

I'm using wrap-with-logger (from ring.middleware.logger) and wrap-params (from ring.middleware.params) middlewares in my application. Any simple way to filter sensitive parameters (password, credit card number etc.) from logs?
You could also consider migrating to ring-logger which includes a feature to redact sensitive information:
By default, ring-logger will redact an authorization header or any param named password (at any nesting level). If you want ring-logger to redact other params you can configure the redact-keys option:
(wrap-with-logger app {:redact-keys #{:senha :token})
Ring-logger will walk through the params and headers and redact any key whose name is found in that redact-keys set.
There's also ring-logger-onelog that should make it very easy to migrate from ring.middleware.logger to ring-logger
You may implement custom pre-logger that filters request according to your needs.
See the following:
(use 'ring.adapter.jetty)
(require '[ring.middleware.logger :as logger])
(defn handler [request]
{:status 200
:headers {"Content-Type" "text/html"}
:body "Hello World"})
(run-jetty
(logger/wrap-with-logger
handler
:pre-logger
(fn [options req]
;; Filtering goes here
(let [filtered-req (filter-sensitive-data req)]
((:info options) "Filtered requrest is: " filtered-req))))
{:port 8080})
Note, while documentation claims that pre-logger accepts only one argument, truly it is two-arg function.

how to you access :headers inside compojure function

org.clojure/clojure-contrib "1.2.0"
ring "1.1.8"
compojure "1.1.5"
clout "1.1.0"
(defroutes rest-routes
(GET "/" [] "<p> Hello </p>")
(POST "/api/v1/:stor/sync" [stor] (start-sync stor))
(POST ["/api/v1/:stor/:txn/data/:file" :file #".*"] [stor txn file] (txn-add stor txn file))
(ANY "*" [] "<p>Page not found. </p>"))
In the second POST, I also want to pass all http-headers to "txn-add" handler. I did lot of google and look through the code, but couldn't find anything useful.
I know, I can use the following to pass headers (but then it doesn't parse url request),
(POST "/api/v1"
{headers :headers} (txn-add "dummy stor" "dummy txn" headers))
Also, how do I pass the content (i.e. :body) of POST request to "txn-add" ?
If the second argument to GET, POST etc is not a vector, it's a destructuring binding form for request. That means you can do things like:
(GET "/my/path"
{:keys [headers params body] :as request}
(my-fn headers body request))
To pick out the parts of request you want. See the Ring SPEC and Clojure's docs on binding & destructuring
The whole request map can be specified in the bindings using :as keyword in bindings and then used to read headers or body :
(POST ["/api/v1/:stor/:txn/data/:file" :file #".*"]
[stor txn file :as req]
(my-handler stor txn file req))

compojure defroutes - route sometimes not recognized

I have a clojure / compojure webapp with the following routes
(defroutes my-routes
(GET "/app/preview" request (my-preview-function request))
(ANY "*" request (str "ANY page <br>" (request :params))))
The preview GET request is made with a couple of parameters. I find this works most of the time but sometimes the /ebook/preview is not found and processing drops to the ANY route, in which case the output is similar to this,
ANY page
{:* "/app/preview", :section "50", :id "48"}
Can anyone suggest what might cause the /ebook/preview request to be skipped? It is definitely a GET request being made; the HTML does not have a POST for the /app/preview URL and to be doubly sure I added a POST route for /app/preview and that was not being hit.
JAR versions:
Clojure 1.2
compojure-0.6.2
ring-core-0.3.7
jetty-6.1.14
ring-jetty-adapter-0.3.1
ring-servlet-0.3.1jar
servlet-api-2.5-6.1.14
Routes are wrapped as follows
(require '[compojure.handler :as handler])
(defn wrap-charset [handler charset]
(fn [request]
(if-let [response (handler request)]
(if-let [content-type (get-in response [:headers "Content-Type"])]
(if (.contains content-type "charset")
response
(assoc-in response
[:headers "Content-Type"]
(str content-type "; charset=" charset)))
response))))
(def app (-> my-routes
handler/site
wrap-stateful-session
(wrap-charset "utf-8")
(wrap-file "public")))
(defn run []
(run-jetty (var app) {:join? false :port 8080}))
If you're trying to figure out what request is causing the problems, stop throwing away the request map with (request :params) and just have a look at request. That will give you a map with all the information Compojure has; you can inspect it, and pass it back into your routes later to observe what happens (after you make some changes, say).
If
(my-preview-function request)
returns nil, then the routing will try the next route. Take a look at (source GET) and see how it matches (or doesn't) your route.