When we hooked up our GraphQL FastAPI app with Sentry, it seemed to work out-of-the-box using Sentry’s SentryAsgiMiddleware. On second look, the most relevant part of the stack trace was “Removed because of size limits”. The Sentry events contained no useful information.

Sentry error message is cut off

Problem

FastAPI is built on Starlette, and Starlette uses Graphene to support GraphQL. To respond with nice error messages, Graphene (by design) catches all exceptions and logs errors instead. The exceptions aren’t bubbling up to Sentry. This means our Sentry event is based on the log entry text, instead of being captured from the exception object. Without the exception object, Sentry will try to put as much of the log entry text in the message as possible, which turns out to have a limit of 512 characters.

Solution using middleware, in three steps

Exceptions should be captured using Sentry’s capture_exception method. Sentry’s middleware does that for us under the hood, but we can write a bit of middleware that does the same for us in Graphene.

Step 1: Write some Graphene middleware

import sentry_sdk

class GraphQLSentryMiddleware(object):

   def resolve(self, next, root, info, **args):
       promise = next(root, info, **args)
       # Capture exceptions, and reraise such that Graphene can respond with a nice error message.
       return promise.then(did_reject=capture_and_reraise)


async def capture_and_reraise(e):
   sentry_sdk.capture_exception(e)
   raise e

After being hooked up in the next steps, Graphene will invoke the resolve method on every GraphQL request, and do the following:

  1. Continue the evaluation, by calling next(root, info, **args) and returning the promise.
  2. When the promise is rejected, call sentry_sdk.capture_exception to capture the exception to Sentry. Then let it bubble up such that Graphene can respond with a nice error message.

Step 2: Add middleware to Graphene (warning: copy-pasted code ahead)

Now that we have defined our middleware, we need to add it to Graphene, such that the resolve method above is called on every GraphQL request. At the time of writing this, Starlette doesn’t provide a way to pass middleware to Graphene, so we have to extend its GraphQLApp class. Most of the code below is copied from the execute method in starlette/graphql.py. There are two modifications:

  1. The __init__ method takes a middleware argument.
  2. The middleware argument is passed to Graphene’s execute method.
from starlette.graphql import GraphQLApp
from starlette.concurrency import run_in_threadpool

class GraphQLAppWithMiddleware(GraphQLApp):
   def __init__(self, *args, **kwargs):
       """
       :param middleware: List of Graphene middleware.
                          See https://docs.graphene-python.org/en/latest/execution/middleware/
       """
       self._middleware = kwargs.pop('middleware', None)
       super().__init__(*args, **kwargs)

   async def execute(  # type: ignore
       self, query, variables=None, context=None, operation_name=None
   ):
       if self.is_async:
           return await self.schema.execute(
               query,
               variables=variables,
               operation_name=operation_name,
               executor=self.executor,
               return_promise=True,
               context=context,
               middleware=self._middleware,  # <-- This line was added to Starlette's execute method.
           )
       else:
           return await run_in_threadpool(
               self.schema.execute,
               query,
               variables=variables,
               operation_name=operation_name,
               context=context,
               middleware=self._middleware,  # <-- And this line as well.
           )

Step 3:

The last step is to use GraphQLAppWithMiddleware instead of Starlette’s GraphQLApp:

from graphql.execution.executors.asyncio import AsyncioExecutor

app.add_route("/", GraphQLAppWithMiddleware(
   schema=schema,
   executor_class=AsyncioExecutor,
   middleware=[GraphQLSentryMiddleware()]))

Final result

Detailed Sentry events! 🎉

Full Sentry event


Tagged with: #Sentry, #Graphene, #GraphQL, #FastAPI, #Starlette, #Middleware