HTTP is a stateless protocol. This fact has confused and tripped many aspiring web developers. Mostly stateless. It was stateless until Netscape, back in the misty days of 1994, baked cookies and released them for public consumption. Later the Internet power mind, IETF, published the standard baking recipes in their RFC 2109, followed later by RFC 2965, titled "HTTP state management mechanism".
The standard is clear and straight forward. Not surprisingly, it is a translation to network protocol language of one of the most widely used approaches of pure functional programming to managing state - when you have to care about state, thread it. In this post I'm trying to transcribe it. For my amusement, public confusion as well as for brevity I will use haskell like notation when trying to present the modelling of the utility of the http cookies as a carrier of the http state.
Basic HTTP
Let's start with the plain basic http. It is an anarchist, stateless protocol, which has a number of benefits worth exploring. One of them is functional treatment of GET method requests and the closely related RESTful service interfaces.
You could model the client-server interaction with the composition of two functions - request and respond.
request::Resp rs ~> Req rq
respond::Req rq -> Resp rsSimilarly - respond is a function from request to response (from Req rq to Resp rs)
Alternatively, it can be read as - Let's name respond the statement: the request Req rq implies a response Resp rs, where rq and rs are some parameters.
In C++ something similar, not the same as the twist and turns are just too much, can be expressed as:
template< typename Req, typename Res >
Res respond( Req request) { ... }The C++ and related notation in C#, Java or family is too verbose, and I'm too lazy to add sequent rendering to the website.
I've used ~> to indicate that this is an interactive, possibly interruptible computation, which can go wrong, or indeed never come back. It is an IO function (-> IO). For our purposes it does not matter that much, but it is good to at least indicate. Using IO in the type signatures will be excessive for the purposes of this write-up.
The behaviour of a http session is then a composition of these functions:
browse:: Req -> Req
--browse = ...request . respond . request . respond
browse = fix(request . respond)
--alternatively
browse r = fix(request.response) rIn the above fix stands for the recursion (tying the knot, looping) combinator. Of course there could be errors or other misadventures, which can be expressed by wrapping a Maybe the result of respond.
data Maybe a = Just a | Nothing
respond::Req rq -> Maybe Resp rsThe real response type in HTTP is richer, with a number of options available, but for the purposes of this blog Maybe is sufficient.
This is nearly the end of modelling http on the type level. I should probably add, that the semantics of the GET method (rfc2616) are functional. That is GET is not supposed to modify the state of the server, so the above actually models fairly faithfully the http protocol.
The GET method means retrieve whatever information (in the form of an entity) is identified by the Request-URI.
In effect GET replaces an identifier (URI) with the identified entity - a simple rewriting rule. On another hand POST, PUT, DELETE are imperative by nature, but they are happen several times less often and this post is not about them.
State affairs or adding HTTP cookies
Let's add cookies. They are part of the HTTP protocol, together with other administrative information. If we Bundle together everything else and just project the out the cookie parts and bundle the result in a pair, we end up with something like
request::(Resp rs, State s) ~> (Req rq, State s)
respond::(Req rq, State s) -> (Resp rs, State s)Thus the state got threaded through the response and request functions. This has a lot of advantages, like scalability, easier analysis, etc. But let's concentrate on the drawbacks. According to wikipedia the major cons are
- inaccurate identification
- cookie hijacking
- cookie theft
The first is dealt with easily - just don't forget that cookies don't identify a person but the product of a user account, a computer, and a Web browser.
The second and third are trickier. We could sidestep them by requesting them to be truly opaque to the client, thus the client could not change or forge them. This together with other forms of id like for example IP address and https should make them safe.
Don't touch, it's magic.
What would opaque look like?
request::forall s. (Resp rs, s) ~> (Req rq, s)
respond::(Req rq, State s) -> (Resp rs, State s)In this case, from the server standpoint, the opaqueness is just a pretence. Cookies are visible to the client and over the wire. The server should not trust the client with critical information, such as its own state. One simple solution is to encrypt the state. Adding two more functions to the pipeline, enc and dec to encrypt and decrypt the state effectively makes the state unreadable.
enc::forall r.(r, State s)->(r, EnState s)
dec::forall r.(r, EnState s)->(r, State s)
request::forall s .(Resp rs, s) ~> (Req rq, s)
respond::(Req rq, State s)-> (Resp rs, State s)
--and the browsing session is roughly equivalent to
browse = fix(request . enc . respond . dec)This is not a security measure on its own, but can be used as part of a security bundle. Now there is a mechanism for hiding the information in the state. If the encoding or the state itself carries some kind of verification code or hash, it can be used to detect tampering. Comparing an expected (encoded) and the request source IP, can help with identifying cookie theft. The sad fact is that such measures are not secure enough - they can't prevent man in the middle style attacks, including ones originating from trojans and virii. HTTPS and measures outside http should be deployed.
4K contest
The biggest drawback for using cookies as state carrier is their limited size. The standard says that user agents should support at least 20 4K sized cookies. Internet Explorer, as usual, has it's own interpretation of the standard - it supports 20 cookies with a total size of 4K, not sure about the newer versions, but in IE6 and at least the early IE7. This does mean that there are some hard, fairly strict, limits to the size of the usable state. Which should, in theory, lead to demo like thriftiness in encoded size. It shouldn't be that much of a problem. There is a myriad of examples and algorithms for compact data representation. They are just not used much in web apps.
Even if the lazy approach is taken - compress the data before encryption, it should increase the amount of space available. Probably not by much and devising custom thrifty encoding would have a higher pay off.
A significant problem remains - the name part of the cookie name : value pair. Applications usually use descriptive names, which eat a relatively large chunk of the precious 4K. Some form of name calculus is required. The simplest form is just to use a dictionary from strings to numbers, which will compress the space used for a name to a couple of bytes. More complex encodings are possible, but they are application specific. Generally it pays off to pay attention to this.
enc::forall r.(r, CState s)->(r, EnState s)
dec::forall r.(r, EnState s)->(r, CState s)
comp:: forall r.(r,State s) -> (r, CState cs)
decomp:: forall r.(r, CState s) -> (r, State cs)
request::forall s .(Resp rs, s) ~> (Req rq, s)
respond::(Req rq, State s)-> (Resp rs, State s)
--and the browsing session is roughly equivalent to
browse = fix(request . enc . comp. respond . decomp. dec)Final remarks and pragmatic considerations of using cookies and databases as carriers of http state
Using a session id and retrieving the state from persistent storage like mysql, memcache or some "nosql" storage is the most used development practice with cookies. It is a result of the double argument of cookie size - max 4K and page download speed. Cookies go in both directions of the request, so their size is effectively doubled in a request. Add to that the asymmetric connections like ADSL, which have slower up-links, and you can see how this practice makes sense. Unfortunately, it makes the development and deployment of stateful applications more complicated. You need to take care of transactions, deadlocks, livelocks, consistency, etc... With the threaded state approach, i.e. pass a large portion of the state to the client, the points where synchronisation needs to take place could be limited to where it is needed to the long term state of the world.
If the web application controller is viewed as a state transformer, as opposed to an object with encapsulated or ambient state, it can be safely deployed in the cloud. It can't do wrong, it doesn't need synchronisation. Outsource all long-term state holding parts to other elements - the model in MVC. Try to encode the transient state in cookies.