Request Context
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.
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.