Skip to main content
Version: 2.x

Cookies

Cookies are small pieces of data that websites store on a user's browser. They are sent between the client (browser) and server in HTTP requests and responses. Cookies serve various purposes, including session management, user authentication, personalization, and tracking.

When a user visits a website, the server can send one or more cookies to the browser, which stores them locally. The browser then includes these cookies in subsequent requests to the same website, allowing the server to retrieve and utilize the stored information.

In ZIO HTTP, cookies are represented by the Cookie data type, which encompasses both request cookies and response cookies:

We can think of a Cookie as an immutable and type-safe representation of HTTP cookies that contains the name, content:

sealed trait Cookie {
def name: String
def content: String
}

object Cookie {
case class Request(name: String, content: String) extends Cookie { self =>
// Request Cookie methods
}
case class Response(
name: String,
content: String,
domain: Option[String] = None,
path: Option[Path] = None,
isSecure: Boolean = false,
isHttpOnly: Boolean = false,
maxAge: Option[Duration] = None,
sameSite: Option[SameSite] = None,
) extends Cookie { self =>
// Response Cookie methods
}
}

Request cookies (Cookie.Request) are sent by the client to the server, while response cookies (Cookie.Response) are sent by the server to the client.

A Response Cookie can be created with params name, content, expires, domain, path, isSecure, isHttpOnly, maxAge, sameSite and secret according to HTTP Set-Cookie

import zio._
import zio.http._

val responseCookie = Cookie.Response("user_id", "user123", maxAge = Some(5.days))
// responseCookie: Cookie.Response = Response(
// name = "user_id",
// content = "user123",
// domain = None,
// path = None,
// isSecure = false,
// isHttpOnly = false,
// maxAge = Some(value = PT120H),
// sameSite = None
// )

The cookies can be added in Response headers:

val res = Response.ok.addCookie(responseCookie)

It updates the response header Set-Cookie as Set-Cookie: <cookie-name>=<cookie-value>

By adding the above cookie to a Response, it will add a Set-Cookie header with the respective cookie name and value and other optional attributes.

Let's write a simple example to see how it works:

import zio.http._

object ResponseCookieExample extends ZIOAppDefault {
val routes = Routes(
Method.GET / "cookie" -> handler {
Response.ok.addCookie(
Cookie.Response(name = "user_id", content = "user123", maxAge = Some(5.days))
)
},
)

def run = Server.serve(routes).provide(Server.default)
}

When we call the /cookie endpoint, it will return a response with a Set-Cookie header:

~> curl -X GET http://127.0.0.1:8080/cookie -i
HTTP/1.1 200 OK
set-cookie: user_id=user123; Max-Age=432000; Expires=Fri, 08 Mar 2024 10:41:52 GMT
content-length: 0

To convert a request cookie to a response cookie, use the toResponse method:

import zio.http._

val requestCookie = Cookie.Request("id", "abc")
val responseCookie = requestCookie.toResponse

Cookie.Response is a case class, so it can be updated by its copy method:

  • maxAge updates the max-age of the cookie:
responseCookie.copy(maxAge = Some(5.days))
  • domain updates the host to which the cookie will be sent:
responseCookie.copy(domain = Some("example.com"))
  • path updates the path of the cookie:
responseCookie.copy(path = Some(Path.root / "cookie"))
  • isSecure enables cookie only on https server:
responseCookie.copy(isSecure = true)
  • isHttpOnly forbids JavaScript from accessing the cookie:
responseCookie.copy(isHttpOnly = true)
  • sameSite updates whether or not a cookie is sent with cross-origin requests:
responseCookie.copy(sameSite = Some(Cookie.SameSite.Strict))

Signing a cookie involves appending a cryptographic signature to the cookie data before it is transmitted to the client. This signature is generated using a secret key known only to the server. When the client sends the cookie back to the server in subsequent requests, the server can verify the signature to ensure the integrity and authenticity of the cookie data.

The cookies can be signed with a signature:

  • Using Response#sign:
val cookie = Cookie.Response("key", "hello", maxAge = Some(5.days))
val app =
Routes(
Method.GET / "cookie" -> handler {
Response.ok.addCookie(cookie.sign("secret"))
}
)
  • Using signCookies middleware:

To sign all the cookies in your routes, we can use signCookies middleware:

import Middleware.signCookies

val app = Routes(
Method.GET / "cookie" -> handler(Response.ok.addCookie(cookie)),
Method.GET / "secure-cookie" -> handler(Response.ok.addCookie(cookie.copy(isSecure = true)))
)

// Run it like any simple app
def run(args: List[String]): ZIO[Any, Throwable, Nothing] =
Server.serve(app @@ signCookies("secret"))
.provide(Server.default)

A request cookie consists of name and content and can be created with Cookie.Request:

val cookie: Cookie = Cookie.Request("user_id", "user123")
// cookie: Cookie = Request(name = "user_id", content = "user123")

The Cookie#name method updates the name of cookie:

cookie.name("session_id")
// res8: Cookie = Request(name = "session_id", content = "user123")

The Cookie#content method updates the content of the cookie:

cookie.content("abc123xyz789")
// res9: Cookie = Request(name = "user_id", content = "abc123xyz789")

From HTTP requests, a single cookie can be retrieved with Request#cookie:

private val app4 = 
Routes(
Method.GET / "cookie" -> handler { (req: Request) =>
val cookieContent = req.cookie("sessionId").map(_.content)
Response.text(s"cookie content: $cookieContent")
}
)

In HTTP requests, cookies are stored in the Header.cookie header:

private val app3 = 
Routes(
Method.GET / "cookie" -> handler { (req: Request) =>
Response.text(
req.header(Header.Cookie)
.map(_.value.toChunk)
.getOrElse(Chunk.empty)
.mkString("")
)
}
)

Examples

Here are some simple examples of using cookies in a ZIO HTTP application.

Server Side Example

zio-http-example/src/main/scala/example/CookieServerSide.scala
package example

import zio._

import zio.http._

/**
* Example to make app using cookies
*/
object CookieServerSide extends ZIOAppDefault {

// Setting cookies with an expiry of 5 days
private val cookie = Cookie.Response("key", "value", maxAge = Some(5 days))
val res = Response.ok.addCookie(cookie)

private val app = Routes(
Method.GET / "cookie" ->
handler(Response.ok.addCookie(cookie.copy(path = Some(Path.root / "cookie"), isHttpOnly = true))),
Method.GET / "secure-cookie" ->
handler(Response.ok.addCookie(cookie.copy(isSecure = true, path = Some(Path.root / "secure-cookie")))),
Method.GET / "cookie" / "remove" ->
handler(res.addCookie(Cookie.clear("key"))),
)

// Run it like any simple app
val run =
Server.serve(app).provide(Server.default)
}

Signed Cookies

zio-http-example/src/main/scala/example/SignCookies.scala
package example

import zio._

import zio.http._

/**
* Example to make app using signed-cookies
*/
object SignCookies extends ZIOAppDefault {

// Setting cookies with an expiry of 5 days
private val cookie = Cookie.Response("key", "hello", maxAge = Some(5 days))

private val app = Routes(
Method.GET / "cookie" ->
handler(Response.ok.addCookie(cookie.sign("secret"))),
)

// Run it like any simple app
val run = Server.serve(app).provide(Server.default)
}