Skip to main content
The Cosmo Router can be easily extended by providing custom modules. Modules are pure Go code and can implement one or multiple interfaces. The following interfaces are provided:
  • core.RouterOnRequestHandler Implements a custom middleware that runs before most internal middleware in the router for each client request. Most importantly this is called before tracing and authentication logic for each request. Use case: Custom Authentication Logic, Custom Tracing Logic, Early return, Request Validation.
  • core.RouterMiddlewareHandler Implements a custom middleware on the router. The middleware is called for every client request. It allows you to modify the request before it is processed by the GraphQL engine. Use case: Logging, Caching, Early return, Request Validation, Header manipulation.
  • core.EnginePreOriginHandler Implements a custom handler that is executed before the request is sent to the subgraph. This handler is called for every subgraph request. Use case: Logging, Request signing, Short-circuiting with mock responses. For setting headers that affect request identity (e.g. tenant IDs), use RouterOnRequestHandler or RouterMiddlewareHandler instead — see Request Deduplication.
  • core.EnginePostOriginHandler Implement a custom handler executed after the request to the subgraph but before the response is passed to the GraphQL engine. This handler is called for every subgraph response. Use cases: Logging, Caching.
  • core.Provisioner Implements a Module lifecycle hook that is executed when the module is instantiated. Use it to prepare your module and validate the configuration.
  • core.Cleaner Implements a Module lifecycle hook that is executed after the server is shutdown. Use it to close connections gracefully or for any other cleanup.
*OriginHandler handlers are called concurrently when your GraphQL operation results in multiple subgraph requests. Due to that circumstance, you should handle the initial router request/response objects ctx.Request() and ctx.ResponseWriter() as read-only objects. Any modification without protecting them from concurrent writes, e.g., by a mutex, results in a race condition.
RouterOnRequestHander is only available since Router 0.188.0
If you are upgrading from a Router version prior to 0.278.0 and use EnginePreOriginHandler, the request deduplication behavior has changed. Headers set in OnOriginRequest are no longer included in the deduplication key. See the Custom Modules Migration Guide for details on how to adapt your modules.

Learn by Example

If you want to see how to develop your own modules, check out our examples repository. It contains examples to build and upgrade your own router.

Router Examples

Explore production-ready examples of custom modules and router setups.

Concepts

The following sections will guide you through important concepts and best practices for developing custom modules.

Priority Loading of Modules

When loading multiple modules, the order is not inherently guaranteed. To ensure a specific loading order, you can use the Priority option. Modules with lower priority numbers are loaded first. Below is an example configuration:
In this example, the module myModule has a priority of 1, meaning it will be loaded before modules with higher priority values.

Access the GraphQL operation

During the client request, you have access to the actual GraphQL operation. Simply call:

Access sha256Hash

You can access the sha256Hash of the operation using the following:
However, the sha256Hash is not computed by default and is only computed if it is required by the router. Some of the scenarios where it will be required by the router are:
  • When the sha256Hash is used in expressions
  • When the sha256Hash is added as a custom attribute for telemetry or access logs
  • When using Persisted Operations
  • If persisted_operations.log_unknown or persisted_operations.safelist.enabled is set to true
However, if you are only using the sha256Hash in custom modules, you will need to explicitly force it to be computed. You can do this by calling SetForceSha256Compute() in the RouterOnRequestHandler hook, as it’s the only hook that runs before the sha256Hash is computed by the router:
You can verify whether the sha256Hash has already been computed by the router by checking if the value is empty, without calling the SetForceSha256Compute() method above. The following example shows how you can access and use the sha256Hash in custom modules:

Access query plan information

In Middleware, you can access some stats about the query plan that will be used for the operation.
QueryPlanStats includes the following info:
  • Total count of subgraph fetches
  • A map of subgraph names to the number of times they will be fetched
Middleware is executed before any of the fetches are made, so you can use this information as a heuristic cost for rate limiting, etc.

Access operation timings

In Middleware, you can access timing information for the various stages of operation processing. This is useful for capturing per-request performance metrics in custom telemetry pipelines.
OperationTimings includes the following fields (all time.Duration):
  • ParsingTime – Time spent parsing the GraphQL operation
  • ValidationTime – Time spent validating the operation against the schema
  • NormalizationTime – Time spent normalizing the operation
  • PlanningTime – Time spent generating the query plan
If called too early in the request chain, timing values may be inaccurate. Using Timings() in the Middleware handler is recommended.

Access Request Context

In every handler, you can add/remove, or modify response headers. We also provide a convenient, safe way to share data across handlers.

Access Subgraph through Request Context

Through the request context you can retrieve the active subgraph for the current request. This can be done in the OnOriginRequest hook as show below
A more complex example including tests is accessible at https://github.com/wundergraph/cosmo/tree/main/router/cmd/custom

Access authentication information

Authentication information, including claims and the provider that authenticated the request, can be accessed through core.RequestContext.Authentication()

Change Authentication Information

Above, we showed how to access Authentication information. There can be cases where your authentication could depend partly on another system, and you want to set elements for use with other directives, such as @requiresScopes. In order to do that, you can use auth.SetScopes() to manually change the authentication’s scopes.
.SetScopes() overwrites the existing scopes. If you’d like to append/preserve the built-in scopes, you can first use auth.Claims() to get the existing scopes, and incorporate that into the updates scopes.
If you have to set the authentication scopes, but the authentication could be not set, you can call the method ctx.SetAuthenticationScopes(scopes []string) that, if the Authentication is not set, it will initialize it with an empty object and set the scopes. If the authentication is already set, it will just override the scopes.
The scopes will be available to subsequent custom modules, just like when using SetScopes().

Do Changes Before Authentication Occurs

In the previous section, the Middleware runs after the authentication of the request. However, sometimes you might want to run authentication related logic before the authentication actually happens. For example, let’s say that your client sends the Authorization header without the Bearer part in the header and you want to add Bearer to the header, for this you can use the RouterOnRequestHandler hook.

Return GraphQL conform errors

Please always use core.WriteResponseError to return an error. It ensures that the request is properly tracked for tracing and metrics.

Access The Error Set In The Router

We expose the Error() method as part of the context, which when called will return the error set to request.error in expressions. Note that if you call Error() in the hooks, you are most likely to get nil. This is because when the hook is called, the error has not been set internally yet. The recommended approach is to use a custom response writer, which will be called after the error has been set internally, thus the value from Error() will be non-nil when called from the custom response writer. When using a custom response writer, we recommend wrapping the writer in the RouterOnRequest hook (see example below), as this is the first hook that is called in a request lifecycle.
The Write method in the custom response writer can be called multiple times from the router for the same request.

Request Handler lifecycle

The current module handler allow to intercept and modify request / response subgraphs.

Module Configuration

If you need to pass external configuration values to your module, you can do so easily by annotating the fields in your module struct. Fields must start with an uppercase letter to make them accessible.

Example Config file

Based on the example above we will populate the field Value with the value 1. You can also validate your config in the core.Provisioner handler.
config.yaml

Migrations

From pre-0.278.0 to 0.278.0+: Request Deduplication

Starting with Router 0.278.0, request deduplication moved from the HTTP transport layer to the engine/loader layer. If your custom modules use EnginePreOriginHandler to set headers that affect request identity (e.g. tenant IDs, user tokens), you need to migrate them to earlier hooks. See the full migration guide for step-by-step instructions.

Future Plans

We’re currently working on the iteration of the module system. It will allow you to fully customize the router with a complete overhaul of the development experience. The new module system will be available in the next major release of the router. If you have any questions or suggestions, please reach out to us on the GitHub ADR.