Search This Blog

Wednesday, January 9, 2013

How Cookies Work in Ring

Cookies turn the stateless http protocol into something with memory.

Here's how to use them in Ring:



;;  necessary dependencies 
;; [[org.clojure/clojure "1.4.0"]
;;  [ring/ring "1.1.6"]]
;; -------------

;; Here's an app, built in a way which should surprise no-one who's read the previous posts

(require 'ring.adapter.jetty 
         'ring.middleware.stacktrace 
         'clojure.pprint)

;; Middleware for spying on the doings of other middleware:
(defn html-escape [string] 
  (str "<pre>" (clojure.string/escape string {\< "&lt;", \> "&gt;"}) "</pre>"))

(defn format-request [name request]
  (with-out-str
    (println "-------------------------------")
    (println name)
    (clojure.pprint/pprint request)
    (println "-------------------------------")))

(defn wrap-spy [handler spyname include-body]
  (fn [request]
    (let [incoming (format-request (str spyname ":\n Incoming Request:") request)]
      (println incoming)
      (let [response (handler request)]
        (let [r (if include-body response (assoc response :body "#<?>"))
              outgoing (format-request (str spyname ":\n Outgoing Response Map:") r)]
          (println outgoing)
          (update-in response  [:body] (fn[x] (str (html-escape incoming) x  (html-escape outgoing)))))))))



;; Absolute binding promise to someday get around to writing the app
(declare handler)

;; plumbing
(def app
  (-> #'handler
      (wrap-spy "what the handler sees" true)
      (ring.middleware.stacktrace/wrap-stacktrace)
      (wrap-spy "what the web server sees" false)))  

;; The actual application
(defn handler [request]
  {:status 200
   :headers {"Content-Type" "text/html"}
   :body (str "<h1>Hello World!</h1>" )})

 
;; Start the server if it hasn't already been started
(defonce server (ring.adapter.jetty/run-jetty #'app {:port 8080 :join? false}))


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; Next we'll include the cookies middleware

(require 'ring.middleware.cookies)

;; And re-plumb

(def app
  (-> #'handler
      (ring.middleware.stacktrace/wrap-stacktrace)
      (wrap-spy "what the handler sees" true)
      (ring.middleware.cookies/wrap-cookies)
      (wrap-spy "what the web server sees" false)))


;; Now go and look at http://localhost:8080 again.

;; In the map the handler sees, there is a key :cookies, whose value is {}
;; ( If it's not, you might want to clear cookies for localhost from your browser )

;; Let's make our app set a cookie:
(defn handler [request]
  {:status 200
   :headers {"Content-Type" "text/html"}
   :body (str "<h1>Setting Cookie!</h1>" )
   :cookies {"yo" {:value "hi"}} })


;; What happens now is quite complicated. 

;; Our key 
{ :cookies {"yo" {:value "hi"}}}
;; Gets converted by the middleware, and combined with our header, to make
{ :headers {"Set-Cookie" '("yo=hi"), "Content-Type" "text/html"}}
;; in the map given to the jetty adapter

;; If you look at the page with
;; $ curl -sv http://localhost:8080
;; Then you'll see
;; < Set-Cookie: yo=hi
;; as part of the http transaction

;; Now if we look at http://localhost:8080, the response will contain the Set-Cookie header.

;; Most browsers will react to this by including the cookie whenever they contact the site.
;; You can examine cookies from the browser's point of view by 
;; (In Chrome) looking at chrome://chrome/settings/cookies
;; (In Firefox) following some interminable GUI procedure that life is too short to describe. 


;; If you refresh the page yet again, you should now see:
{:headers {"cookie" "yo=hi"}}
;; in the incoming request from the webserver
;; and a new key:
{:cookies {"yo" {:value "hi"}}} 
;; in the map the eventual handler sees (put there by the middleware of course!)



;; We can use this to count how many times a particular browser has been greeted:
(defn seen-before [request]
  (try (Integer/parseInt (((request :cookies) "yo") :value))
       (catch Exception e :never-before)))

(defn handler [request]
  (let [s (seen-before request)]
    (cond
     (= s :never-before) {:status 200
                          :headers {"Content-Type" "text/html"}
                          :body (str "<h1>Hello Stranger!</h1>" )
                          :cookies {"yo" {:value "1"}}}
     (= s 1) {:status 200
                          :headers {"Content-Type" "text/html"}
                          :body (str "<h1>Hello Again!</h1>" )
                          :cookies {"yo" {:value "2"}}}
     :else {:status 200
                          :headers {"Content-Type" "text/html"}
                          :body (str "<h1>Hi, this is visit "s"</h1>" )
                          :cookies {"yo" {:value (str (inc s))}}})))



;; And now, an exercise for the reader!

;; If I look at my site in Firefox, it works as I expected.

;; If I look at it with Chrome, it double counts

;; If I use curl, like so:
;; curl -sv http://localhost:8080 | grep -i hello

;; Then all I ever see is "Hello Stranger"

;; What is going on?



10 comments:

  1. This comment has been removed by a blog administrator.

    ReplyDelete
    Replies
    1. Thomas, that's the right answer, but I'm deleting it so as not to spoil the fun for others! Sorry.

      Delete
  2. This comment has been removed by a blog administrator.

    ReplyDelete
  3. xeqi, again, the right answer, but I'm going to delete it so that others have to think about it!

    ReplyDelete
  4. I realize these tutorials are focused on Ring, but I am curious about the overall tool and library kits that you are using. For instance, with routing, do you use Moustache or Compojure, or have you written your own? HTML Templates? Hiccup? Enlive?

    ReplyDelete
    Replies
    1. Lawrence, I'm writing these tutorials because I'm new to it. I was/am writing something using Compojure/Hiccup/Enlive, and they're all great, but I kept getting really confused.

      So I figured I'd work out how to do web apps using just ring, and write it up as blog articles, which has a way of making sure that I really understand it, during which process I'm probably going to come up with a fair number of abstractions of my own. Then I can work out exactly what it is that those libraries are doing for me and whether I want to use them or something else to replace my own stuff or not.

      Once I've done that, I'll use libraries rather than doing everything by hand. But I've got nowhere near enough experience of it yet to go making recommendations about the rest of the stack.

      I can say that I love ring to bits. Such a clean simple elegant idea. I wish I'd thought of it.

      Delete
    2. I know what you mean. I thought about writing a small app with nothing but Ring. I've looked at the wiki and I realize that it is very rich in functionality. But I made the mistake of taking on a large commercial while I was still learning, so I had to start using the libraries without fully understanding them. This lead to much confusion. I managed to work out in the end, but I still to learn more about Ring. I am impressed with you elegant it is.

      Delete
  5. This comment has been removed by a blog administrator.

    ReplyDelete
  6. Colin, deleted because I don't want to spoil it for others. But
    actually you're only sort-of right!

    ReplyDelete
  7. is there a link for the answer?

    ReplyDelete

Followers