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:
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: _*)
}
Code language: PHP (php)
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
):
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 requestrequestCookies
, contains cookies that came with the HTTP requestnewHeaders
, stores new headers that we can add to the HTTP responsenewCookies
, 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:
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)
}
}
Code language: JavaScript (javascript)
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:
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
),
)
Code language: PHP (php)
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:
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())
)
)
Code language: PHP (php)
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()
):
val httpContext = Context(request.headers, request.cookies) // Create the context
Code language: JavaScript (javascript)
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()
):
executeQuery(query, variables, operationName, httpContext)
.map(_.withHeaders(httpContext.newHeaders: _*).withCookies(httpContext.newCookies: _*))
Code language: CSS (css)
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:
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
:
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)
}
Code language: JavaScript (javascript)
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
:
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
)
)
)
Code language: JavaScript (javascript)
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:
@Singleton
class AccessConfig {
def getAdminAccesstoken = "some_token"
}
Code language: JavaScript (javascript)
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
):
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."))
}
}
Code language: JavaScript (javascript)
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:
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"))
)
)
Code language: JavaScript (javascript)
Let’s check if GraphQL queries are executed correctly by adding a post:
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:
Now, clean cookies using the developer tools in your browser, add a new post, and request all posts again:
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:
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:
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.