typelevel / typelevel/grackle

Add http(4s) module

Open
#946 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Scala
Stars
189
Forks
32
Avg merge
18h 31m
Merged PRs (30d)
37

Description

Feedback welcome! Comment below if this seems interesting or you have ideas/opinions

The problem

Grackle does not provide a http4s module, only the example shown in the demo which creates quite a bit of boilerplate for anyone wanting to get a GraphQL server running from a working Mapping.

The Demo is a basic implementation of a GraphQL endpoint, only implementing a small part of the GraphQL-over-HTTP spec. For instance, it misses standard-format JSON errors which clients understand, standard status codes (like 422 on malformed operations), Content-Type and Accept negotiation, the application/graphql-response+json response Content-Type, and any subtleties from what the spec calls "legacy server" implementations (which is what the demo server can be called).

There is also no WebSocket demo server. The graphql-ws spec is its own whole beast with message conformity, connection handling, cancellation, subscriptions and close codes.

There is also graphiql which provides a web IDE to make it easy to test out operations on a server.

Both the HTTP and WS spec are not trivial, and should not be something users of Grackle have to implement themselves. Grackle providing a http4s module that implements both of these specs properly would make it easier to get started with GraphQL in Scala, prevent gotcha's from the spec, and improve how any client handles and expects errors from a server.

The solution

https://github.com/gemini-hlsw/lucuma-graphql-routes is a library that implements both the HTTP and WS spec, for use in several GraphQL servers that use either HTTP or WebSockets. It's been running in production against Scala, TypeScript and Python clients. I've been improving the library and it is now almost fully compliant to the spec, even supporting things like cancellation over WebSocket connections. I believe it would be a good base to build a http4s module from in Grackle.

Example:

import lucuma.graphql.routes.GraphQLService
import lucuma.graphql.routes.Routes
import org.http4s.HttpRoutes
import org.http4s.server.websocket.WebSocketBuilder2
import org.http4s.headers.Authorization

val wsb: WebSocketBuilder2[F] = ???

// Wrap your mapping in a `GraphQLService`
def service(user: MyUser) = GraphQLService[F](new MyMapping[F](user))

// Create http4s routes
val routes: HttpRoutes[F] = Routes.forService(
  // `Option[Authorization] => F[Option[GraphQLService[F]]]`
  (authorization: Option[Authorization]) =>
    // Pass Authorization header to our SSO client to get user info
    authorization.flatTraverse(ssoClient.get)
      .map:
        case None       =>
          // Reject unauthorized requests
          none
        case Some(user) =>
          GraphQLService[F](service(user)).some,
  wsb
)

The discussion

Here are all my bike-shedding ideas I could think of. I want to open this issue to create some discussion and find out how people might expect to use the library.

  • Tracing from otel4s, with extensions for WebSocket containing a traceparent key or HTTP header for tracing through multiple applications. Potentially should be configurable. Currently not a dependency in Grackle.
  • Logging from log4cats. Already a dependency for Doobie, Skunk and the demo modules. IMO this is pretty set in stone.
  • Scala version support. lucuma-graphql-routes is only built against Scala 3. It should be possible to rewrite to support 2.13, but I'd rather avoid it and simply only support Scala 3 for the http4s module. Cats-effect recently updated their policy to only continue support of 2.12 if possible within minor effort. I believe we can build on that reasoning to only support 3.
  • lucuma-graphql-routes has a minor dependency on clue-model which contains some model classes for subscription messages (and more). This can be moved to a small separate module in Grackle that Clue could then replace its model with.
  • Authorization: this hard-codes on either a Authorization HTTP header or Authorization passed in the connection_params WebSocket object. The spec doesn't say anything about authentication, so this should probably be pluggable.
    • One downside to the usage (see example above) is it will create a new service for each request, as the service needs to be passed a user. Which in most cases also means schema validate and more is unnecessarily done on each request. With the API above I don't really see a way to avoid that. Maybe another option would be to instantiate once, set the Authorization in an IOLocal and use a def auth: F[Authorization] = ioLocal.get` or something. Ideas welcome.
  • Endpoints: most libraries let you customize the HTTP, WebSocket and GraphiQL endpoints. The default is usually to have them all on the same endpoint (and look at the Accept header to decide strategy). We should also let users disable endpoints if they don't need them

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the demo in demo/src/main/scala/demo and compare its GraphQL endpoint with gemini-hlsw/lucuma-graphql-routes. Review the HTTP and WebSocket specifications and the unresolved choices around authorization, endpoints, Scala versions, tracing, and dependencies. Done means a settled design and an http4s module that implements the required HTTP and WebSocket behavior without the demo's boilerplate.

Written by the indexing model from the issue text.

Assessment

Tech stack
scala
Domain
api, backend
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
28/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.