Request Context

Managing request-scoped data and context in Viaduct applications.

Viaduct provides comprehensive support for managing request-specific context throughout your GraphQL operations. This is essential for handling authentication, authorization, tenant isolation, and other request-scoped concerns.

Request context approaches

Viaduct supports multiple approaches to managing request context, allowing you to choose the pattern that best fits your application architecture.

1. Framework-based request scoping

The recommended approach is to leverage your web framework’s built-in request scoping mechanisms. This provides automatic lifecycle management and prevents context leakage between requests.

Example with Micronaut

Create a request-scoped service:

@RequestScope
open class SecurityAccessContext {
   private var securityAccess: String? = null

   companion object {
       private const val ADMIN_ACCESS = "admin"
   }

   /**
    * Sets the security access level for the current request.
    */
   open fun setSecurityAccess(securityAccess: String?) {
       this.securityAccess = securityAccess
   }

   /**
    * Executes the given block only if the user has admin access.
    *
    * @throws SecurityException if the user lacks admin permissions
    */
   open fun <T> validateAccess(block: () -> T): T {
       if (securityAccess != ADMIN_ACCESS) {
           throw SecurityException("Insufficient permissions!")
       }
       return block()
   }
}

Populate the context in your GraphQL controller:

@Controller
class ViaductRestController(
 private val viaduct: Viaduct,
 private val securityAccessService: SecurityAccessContext
) {
 @Post("/graphql")
 suspend fun graphql(
     @Body request: Map<String, Any>,
     @Header(SCOPES_HEADER) scopesHeader: String?,
     @Header("security-access") securityAccess: String?
 ): HttpResponse<Map<String, Any>> {
     securityAccessService.setSecurityAccess(securityAccess)
     val executionInput = createExecutionInput(request)
     // tag::run_query[7] Runs the query example
     val scopes = parseScopes(scopesHeader)
     val schemaId = determineSchemaId(scopes)
     val result = viaduct.executeAsync(executionInput, schemaId).await()
     return HttpResponse.status<Map<String, Any>>(statusCode(result)).body(result.toSpecification())
 }

Use the context in your resolvers via dependency injection:

@Resolver
class CreateCharacterMutation
@Inject
constructor(
    private val characterRepository: CharacterRepository,
    private val securityAccessContext: SecurityAccessContext
) : MutationResolvers.CreateCharacter() {
    override suspend fun resolve(ctx: Context): Character =
        securityAccessContext.validateAccess {
            val input = ctx.arguments.input
            val homeworldId = input.homeworldId
            val speciesId = input.speciesId

            // TODO: Validate homeworld and species are valid ids

            // Create and store the new character
            val character = characterRepository.add(
                com.example.starwars.modules.filmography.characters.models.Character(
                    id = "",
                    name = input.name,
                    birthYear = input.birthYear,
                    eyeColor = input.eyeColor,
                    gender = input.gender,
                    hairColor = input.hairColor,
                    height = input.height,
                    mass = input.mass?.toFloat(),
                    homeworldId = homeworldId?.internalID,
                    speciesId = speciesId?.internalID,
                )
            )

            // Build and return the GraphQL Character object from the created entity
            CharacterBuilder(ctx).build(character)
        }
}

Benefits:

  • Automatic lifecycle management
  • Type-safe access to context data
  • Prevents data leakage between requests
  • Leverages framework’s dependency injection
  • Clean separation of concerns

2. ExecutionInput request context

For simpler installations or when you need to pass arbitrary context without setting up framework scoping, you can use ExecutionInput.requestContext.

Setting the request context

The ExecutionInput interface includes a requestContext property:

When creating your ExecutionInput, include the request context:

private fun createExecutionInput(request: Map<String, Any>): ExecutionInput {
    @Suppress("UNCHECKED_CAST")
    return ExecutionInput.create(
        operationText = request[QUERY_FIELD] as String,
        variables = (request[VARIABLES_FIELD] as? Map<String, Any>) ?: emptyMap(),
        requestContext = emptyMap<String, Any>(),
    )

 val requestContext = mapOf(
  "securityAccess" to securityAccess,
  "tenantId" to tenantId
 )
 val executionInput = ExecutionInput.create(
  operationText = request["query"] as String,
  variables = (request["variables"] as? Map<String, Any>) ?: emptyMap(),
  requestContext = requestContext
 )

Accessing request context in resolvers

Inside your resolvers, access the request context through the resolver context:

@Resolver
class FooQuery : FooResolber.Character() {
    override suspend fun resolve(ctx: Context): Foo? {
        val requestContext = ctx.requestContext as? Map<*, *>
        ...
    }
}

Benefits:

  • Simple to set up
  • No additional framework configuration needed
  • Direct access to context data
  • Flexible for passing arbitrary data

Considerations:

  • Requires manual type casting
  • Less type-safe than framework approach
  • No automatic lifecycle management
  • Context must be manually threaded through execution

Choosing an approach

For production applications with complex authorization needs, authentication, or multi-tenancy, use framework-based request scoping. It provides automatic lifecycle management and type safety.

For simpler installations or prototypes where you need to pass arbitrary context without framework configuration, use ExecutionInput.requestContext.