4: Cookies

In Bee Client, cookies are immutable objects, each comprising a name, a value and various other mostly-optional parameters. Because their identity is a compound structure consisting of the name, domain and path, there is a CookieIdentity trait.

trait CookieIdentity {
  def name: String
  def domain: Domain
  def path: String
  def matches(cookie: CookieIdentity): Boolean = { ... }

CookieIdentity is shared by two case classes: CookieKey implements CookieIdentity directly, whilst Cookie does so and also provides value information. A CookieKey can easily be given a value to become a Cookie.

case class CookieKey (name: String, domain: Domain, path: String = "/") extends CookieIdentity {

case class Cookie (name: String,
                   value: String,
                   domain: Domain = Domain.localhost,
                   path: String = "/",
                   expires: Option[HttpDateTimeInstant] = None,
                   creation: HttpDateTimeInstant = new HttpDateTimeInstant(),
                   persistent: Boolean = false,
                   hostOnly: Boolean = false,
                   secure: Boolean = false,
                   httpOnly: Boolean = false,
                   serverProtocol: String = "http") extends CookieIdentity {

Cookies are held in immutable collections called cookie jars. Not only do cookie jars store collections of cookies, they also provide the interaction point when you make HTTP requests. You don’t have to do much except provide the existing cookie jar to each request, and then use the cookie jar from the resultant response. The response cookie jar is based on the request cookie jar, but potentially with new cookies having been added or expired cookies having been deleted. Here’s an example.

import uk.co.bigbeeconsultants.http._
import header.{Cookie, CookieJar}

object Example4a {
  val cookie = Cookie(name = "XYZ", value = "zzz", domain = "localhost")
  val originalCookieJar = CookieJar(cookie)
  val url = "http://beeclient/test-echo-back.php"
  val httpClient = new HttpClient

  def main(args: Array[String]) {
    val response = httpClient.get(url, Nil, originalCookieJar)
    val augmentedCookieJar = response.cookies.get
    println(augmentedCookieJar.get("XYZ").get.value) // prints "zzz"

We firstly create our original cookie jar containing just one cookie (an empty cookie jar would often be used instead). Then we make a GET request. Because the request is to localhost and the cookie has the same domain, this cookie will be sent as a ‘Cookie’ header with the request.

The response comes back and may include ‘Set-Cookie’ headers for us; these headers may add or alter or delete cookies. Our original cookie jar has these changes applied to it, ending up with an augmented cookie jar that is included in the response.

Finally, we should throw away our original cookie jar and keep the augmented one for the next request. You may find it useful to have a ‘var’ here, although be careful if you share the ‘var’ between multiple threads.

  var cookieJar = CookieJar(...)
  ...  other stuff
  val response = httpClient.get(url, Nil, cookieJar)
  cookieJar = response.cookies.get

One thing to bear in mind is that you only get a cookie jar in the response if there was a cookie jar in the request, which is optional. When you make requests without a cookie jar, the response cookies field will always be None. If you want to make a request and you haven’t got any cookies to send, you need to use CookieJar.Empty in the request so that the ‘Set-cookie’ headers will be processed.

In summary,

  • if request.cookies is None, then response.cookies is also None;
  • if request.cookies is Some(cookieJar), then response.cookies will also be Some(cookieJar), but it might be different from the request’s jar.

Remember, in the second case, you can simply start with Some(CookieJar.Empty) the first time if you want.

What’s in a CookieJar?

A cookie jar is essentially an immutable list of cookies. It looks like this

case class CookieJar (cookies: List[Cookie]) extends Iterable[CookieIdentity] {

You can find a particular cookie using the get, contains, filter, filterNot or find operations, such as

  val cookie: Option[Cookie] = cookieJar.get("X1")
  val foundX1: Option[Cookie] = cookieJar.find (_.name == "X1")
  val filteredX1: Iterable[Cookie] = cookieJar.filter (_.name == "X1")

All the above describes cookie processing in detail, and is needed every time you use HttpClient.

However, to make the cookie round-trip much easier for those cases that make many requests that need cookies, use the HttpBrowser instead of HttpClient. A later section describes this in detail.