'Best Practices for Processing HTTP Headers and Cookies in a Scala GraphQL API' post illustration

Best Practices for Processing HTTP Headers and Cookies in a Scala GraphQL API

avatar

In this article, you'll become familiar with a way to interact with HTTP headers and cookies when using GraphQL, Play 2 Framework and Sangria in a Scala application.

When we're writing a RESTful API with Play 2, we can easily interact with cookies and HTTP headers:

1
2
3
4
5
def endpoint: Action[JsValue] = Action.async(parse.json) {
  implicit request: Request[JsValue] =>
    val result: ProcessResult = processEndpoint(request.attrs, request.headers)
    Ok(result.body).withHeaders(result.headers: _*).withCookies(result.cookies: _*)
}

processEndpoint() runs some business logic and then adds new headers and cookies into the HTTP response using the functions withHeaders() and withCookies(). Therefore, when developing RESTful applications, we can have multiple endpoints handled by individual controllers thus dividing the functionality and letting us access HTTP data directly.

With GraphQL and Sangria, however, we don't have this luxury of accessing the HTTP request and response objects directly. All we have is a JSON object returned after a GraphQL query is executed. Nevertheless, we can return headers and cookies from the execute() method and change them. How do we achieve that?

All we need to do is create a set of global mutable collections and store additional data in them when executing the GraphQL query and then use them when creating an HTTP response.

Here's our Scala application with the result:

Now, how do we create those global mutable collections? We use the Sangria context.

The Sangria context is all you need

Sangria features a tool called context, which we can use to create the global collections to store HTTP headers and cookies in our Scala and GraphQL API. In simple terms, Sangria can create its own execution context and use it in resolve functions that we define in a GraphQL schema.

What's the context? It's an object passed to the GraphQL query execution and it almost never changes. We do change it, though, to handle cookies and headers. As you'll see further, this object gets passed around in the schema, validators, and services.

In our sample Scala application, we created the following custom context (look for app/config/AppConfig.scala):

1
2
3
4
case class Context(requestHeaders: Headers,
                  requestCookies: Cookies,
                  newHeaders: ListBuffer[(String, String)] = ListBuffer.empty,
                  newCookies: ListBuffer[Cookie] = ListBuffer.empty)

In Context, the following parameters need to be passed:

  • requestHeaders, contains the headers that came with the HTTP request
  • requestCookies, contains cookies that came with the HTTP request
  • newHeaders, stores new headers that we can add to the HTTP response
  • newCookies, stores new cookies to be added to the HTTP response

Pay attention that newHeaders and newCookies are mutable collections and we can change their state by adding, removing, or changing an element — a header or cookie respectively — from the list.

We instantiate Context in AppController.graphqlBody() after the GraphQL query is parsed and ready to be passed to the executor:

1
2
3
4
5
6
7
8
9
maybeQuery match {
  case Success((query, operationName, variables)) =>
    val httpContext = Context(request.headers, request.cookies)
    executeQuery(query, variables, operationName, httpContext)
      .map(_.withHeaders(httpContext.newHeaders: _*).withCookies(httpContext.newCookies: _*))
  case Failure(error) => Future.successful {
    BadRequest(error.getMessage)
  }
}

Note that most Sangria's tools have the general type Ctx that defines the type of context your Scala application is dealing with. By default, this type is Unit meaning no context is available. And the context must be the same for the entire application: You can't create another instance of Context in some other method.

Check out this small code example. This is a chunk of PostSchema with the GraphQL query posts to return all posts:

1
2
3
4
5
6
7
8
implicit val PostType: ObjectType[Context, Post] = deriveObjectType[Context, Post](ObjectTypeName("Post"))
val Queries: List[Field[Context, Unit]] = List(
  Field(
    name = "posts",
    fieldType = ListType(PostType),
    resolve = _ => postResolver.posts
  ),
)

Pay attention to the implementation of ObjectType, which has two generic parameters — one type for the context and another for the returned data. The Field type follows the same structure: The first generic type is the context and the second is Unit (the list of Queries can return different types of data, hence Unit is passed).

Theory talks are good, but practice is better. Let's use the described approach to access and modify HTTP headers and cookies in a Scala app.

Interacting with the Sangria context on the level of HTTP server

We get the headers and cookies from an HTTP request and writing new headers and cookies into the HTTP response.

Our solitary AppController contains the method executeQuery(), and we need to pass the context to the Sangria Executor.execute() method:

1
2
3
4
5
6
7
8
9
10
Executor.execute(
  schema = graphQL.Schema,
  queryAst = queryAst,
  userContext = context,
  variables = variables.getOrElse(Json.obj()),
  queryReducers = List(
    QueryReducer.rejectMaxDepth[Context](graphQL.maxQueryDepth),
    QueryReducer.rejectComplexQueries[Context](graphQL.maxQueryComplexity, (_, _) => TooComplexQueryError())
  )
)

But before we call executeQuery() with the context, we must create it and pass headers and cookies from the request (recall the maybeQuery variable created in graphqlBody()):

1
val httpContext = Context(request.headers, request.cookies) // Create the context

After the GraphQL query is executed, we can add new headers and cookies into the HTTP response using the methods withHeaders and withCookies (look into the same method graphqlBody()):

1
2
executeQuery(query, variables, operationName, httpContext)
  .map(_.withHeaders(httpContext.newHeaders: _*).withCookies(httpContext.newCookies: _*))

Similarly, you can add headers and cookies in any other HTTP Scala server.

Notice that the GraphQL query gets executed first, and only then does it get mapped to new headers and cookies. This means you need to extend the context with new headers and cookies during query execution in a schema, services, or validators.

Using a custom Sangria context in a Scala and GraphQL application

To demonstrate the use of the Sangria context in a Scala and GraphQL application, let's implement user authentication with the help of unique identifiers that will be stored in cookies on the client.

Our demo Scala app implements the functionality of anonymous forums where users don't need to register and where user authentication is implemented with IDs added to each post object. Therefore, the Post entity defines four fields:

1
2
3
4
case class Post(id: Option[Long] = None, // ID for the database record
               authorId: String, // ID for authentication
               title: String,
               content: String)

The field authorId, logically, is used to authenticate this user.

Now, let's create a method that accepts our custom Sangria context and verifies the availability of an ID in a cookie. If there's an ID, then we pass it to the callback. And if there's no ID, then we'll create a new UUID, add it to the list of newCookies, and pass it to the callback.

The following implementation you can find in app/services/PostsAuthorizeServiceImpl.scala:

1
2
3
4
5
6
7
8
override def withPostAuthorization[T](context: Context)(callback: String => Future[T]): Future[T] =
  context.requestCookies.get("my-id") match {
    case Some(id) => callback(id.value)
    case None =>
      val newId = UUID.randomUUID().toString
        context.newCookies += Cookie("my-id", newId)
        callback(newId)
  }

Next, our GraphQL schema PostSchema must be changed to use the authorization method before actually executing the mutation query. Here's the addPost mutation function in PostSchema:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Field(
  name = "addPost",
  fieldType = PostType,
  arguments = List(
    Argument("title", StringType),
    Argument("content", StringType)
  ),
  resolve = sangriaContext =>
    authorizeService.withPostAuthorization(sangriaContext.ctx)(
      userId =>
      postResolver.addPost(
        sangriaContext.args.arg[String]("title"),
        sangriaContext.args.arg[String]("content"),
        userId
      )
    )
)

In this implementation, we simply authenticate the user if they weren't authenticated earlier.

Let's also add the administrator to our application. For that functionality, we need to add a class for storing secret configurations:

1
2
3
4
@Singleton
class AccessConfig {
  def getAdminAccesstoken = "some_token"
}

The class app/config/AccessConfig stores a fake token used for verification. Let's create a method to verify if a user has the administrator rights (look for app/validators/AdminAccessValidatorImpl.scala):

1
2
3
4
5
6
override def withAdminAccessValidation[T](context: Context)(callback: => Future[T]): Future[T] = {
  context.requestCookies.get("admin-access-token") match {
    case Some(value) if config.adminAccessToken == value => callback
    case _ => Future.failed(Forbidden("You don't have admin rights."))
  }
}

This method accepts the context we created in AppController and the next operation declared as a callback (this will be our GraphQL resolve function). The method verifies if the cookie admin-access-token comes with the admin token and compares it with the token in AccessConfig.

Let's get back again to PostSchema and use the validator to verify if a mutation query came with the cookie:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Field(
  name = "updatePost",
  fieldType = PostType,
  arguments = List(
    Argument("id", LongType),
    Argument("title", StringType),
    Argument("content", StringType)
  ),
  resolve = sangriaContext =>
    adminAccessValidator.withAdminAccessValidation(sangriaContext.ctx)(
    postResolver.updatePost(
      sangriaContext.args.arg[Long]("id"),
      sangriaContext.args.arg[String]("title"),
      sangriaContext.args.arg[String]("content")
    )
  )
),
Field(
  name = "deletePost",
  fieldType = BooleanType,
  arguments = List(
    Argument("id", LongType)
  ),
  resolve =
    sangriaContext =>
      adminAccessValidator.withAdminAccessValidation(sangriaContext.ctx)(
      postResolver.deletePost(sangriaContext.args.arg[Long]("id"))
  )
)

Let's check if GraphQL queries are executed correctly by adding a post:

GraphQL query to add a post in a Scala GraphQL application

You can see the result on the right: There's a response confirming that a post was added.

Add a couple of other posts with test data and request all of them:

GraphQL query to get all posts in a Scala GraphQL application

Now, clean cookies using the developer tools in your browser, add a new post, and request all posts again:

GraphQL query to get all posts with different authors in a Scala GraphQL application

As we can see, the author identifier changed for the latest post. We can now check if the access right is verified using this mutation query:

GraphQL query to update a post in a Scala GraphQL application. The post is not updated because no access rights.

Our mutation query was denied because we didn't provide the authorization token!

Let's add the token "some_token" to the cookie admin-access-token in the developer tools and run the mutation query again:

Authorized GraphQL query to update a post in a Scala GraphQL application. The post is updated because access token was provided.

Our Scala application works as expected.

To sum up, we advise not to store the HTTP request in the Sangria context as this approach breaks the single responsibility pattern. Because context isn’t related to business logic (read: the resolver layer), you should avoid passing it in parameters into GraphQL resolve functions. Use services or validators instead, as we did in our demo Scala app.


We discussed the key point of using HTTP entities with Sangria in a GraphQL application. Try it out in your Scala application and comment below if you have any questions.

If you're looking for a developer or considering starting a new project,
we are always ready to help!