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¶
Compare how the demos hold per-request security state:
@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()
}
}
The Java demo uses an immutable value passed in ExecutionInput.requestContext, rather than a Micronaut request-scoped bean. Its helper validates the context entry before use; the next section explains this route.
package com.example.starwars.common;
/** Per-execution security context used by the mutation examples. */
public final class SecurityAccessContext {
private static final String ADMIN_ACCESS = "admin";
public static final String REQUEST_CONTEXT_KEY = SecurityAccessContext.class.getName();
private final String securityAccess;
public SecurityAccessContext(String securityAccess) {
this.securityAccess = securityAccess;
}
public static SecurityAccessContext from(Object requestContext) {
if (requestContext instanceof Map<?, ?> values
&& values.get(REQUEST_CONTEXT_KEY) instanceof SecurityAccessContext securityContext) {
return securityContext;
}
return new SecurityAccessContext(null);
}
public <T> T validateAccess(Supplier<T> block) {
if (!ADMIN_ACCESS.equals(securityAccess)) {
throw new SecurityException("Insufficient permissions!");
}
return block.get();
}
}
Populate the context in your GraphQL controller:
@Controller
class ViaductRestController(
private val viaduct: Viaduct,
private val schemas: StarWarsSchemas,
private val securityAccessService: SecurityAccessContext
) {
@Post("/graphql")
suspend fun graphql(
@Body request: Map<String, Any>,
@Header("security-access") securityAccess: String?
): HttpResponse<Map<String, Any?>> = execute(request, schemas.default.schemaId, securityAccess)
@Post("/extras/graphql")
suspend fun extrasGraphql(
@Body request: Map<String, Any>,
@Header("security-access") securityAccess: String?
): HttpResponse<Map<String, Any?>> = execute(request, schemas.extras.schemaId, securityAccess)
private suspend fun execute(
request: Map<String, Any>,
schemaId: SchemaId,
securityAccess: String?
): HttpResponse<Map<String, Any?>> {
val query = request[QUERY_FIELD] as? String
?: return HttpResponse.badRequest(
mapOf("errors" to listOf(mapOf("message" to "Missing 'query' field")))
)
securityAccessService.setSecurityAccess(securityAccess)
val executionInput = createExecutionInput(query, request)
val result = viaduct.execute(executionInput, schemaId)
return HttpResponse.ok(result.toSpecification())
}
@Controller
public final class ViaductRestController {
private final Viaduct viaduct;
private final ViaductConfiguration.StarWarsSchemas schemas;
public ViaductRestController(Viaduct viaduct, ViaductConfiguration.StarWarsSchemas schemas) {
this.viaduct = viaduct;
this.schemas = schemas;
}
@Post("/graphql")
public CompletableFuture<HttpResponse<Map<String, Object>>> graphql(
@Body Map<String, Object> request,
@Header("security-access") @Nullable String securityAccess) {
return execute(request, schemas.defaultSchema().getSchemaId(), securityAccess);
}
@Post("/extras/graphql")
public CompletableFuture<HttpResponse<Map<String, Object>>> extrasGraphql(
@Body Map<String, Object> request,
@Header("security-access") @Nullable String securityAccess) {
return execute(request, schemas.extras().getSchemaId(), securityAccess);
}
private CompletableFuture<HttpResponse<Map<String, Object>>> execute(
Map<String, Object> request, SchemaId schemaId, @Nullable String securityAccess) {
Object queryValue = request.get("query");
if (!(queryValue instanceof String query)) {
return CompletableFuture.completedFuture(
HttpResponse.badRequest(
Map.of("errors", List.of(Map.of("message", "Missing 'query' field")))));
}
ExecutionInput executionInput =
ExecutionInput.builder()
.operationText(query)
.variables(readVariables(request))
.requestContext(
Map.of(
SecurityAccessContext.REQUEST_CONTEXT_KEY,
new SecurityAccessContext(securityAccess)))
.build();
return viaduct
.executeAsync(executionInput, schemaId)
.thenApply(result -> HttpResponse.ok(result.toSpecification()));
}
Use the context in your resolvers:
@Resolver
@Prototype
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)
}
}
The Java resolver receives repository dependencies through Micronaut but reads security state from context.getRequestContext(). This avoids depending on a framework request scope during asynchronous execution.
package com.example.starwars.modules.filmography.characters.mutations;
/** Creates a character after validating request-scoped admin access. */
@Resolver
@Prototype
public final class CreateCharacterMutation extends MutationResolvers.CreateCharacter {
private final CharacterRepository characterRepository;
@Inject
public CreateCharacterMutation(CharacterRepository characterRepository) {
this.characterRepository = characterRepository;
}
@Override
public CompletableFuture<viaduct.java.grts.Character> resolve(Context context) {
return CompletableFuture.completedFuture(
SecurityAccessContext.from(context.getRequestContext())
.validateAccess(
() -> {
CreateCharacterInput input = context.getArguments().getInput();
Character character =
characterRepository.add(
new Character(
"",
input.getName(),
input.getBirthYear(),
input.getEyeColor(),
input.getGender(),
input.getHairColor(),
input.getHeight(),
input.getMass() == null ? null : input.getMass().floatValue(),
input.getHomeworldId() == null
? null
: input.getHomeworldId().getInternalID(),
input.getSpeciesId() == null
? null
: input.getSpeciesId().getInternalID()));
return new CharacterBuilder(context).build(character);
}));
}
}
Framework-scoping benefits (Kotlin example):
- 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:
The context can carry application-specific values; construct it explicitly when preparing the execution input:
Map<String, Object> requestContext = Map.of(
SecurityAccessContext.REQUEST_CONTEXT_KEY,
new SecurityAccessContext(securityAccess));
ExecutionInput executionInput = ExecutionInput.builder()
.operationText(query)
.variables(variables)
.requestContext(requestContext)
.build();
The controller creates a fresh context for each execution. Do not store request credentials in a shared singleton.
Accessing request context in resolvers¶
Inside your resolvers, access the request context through the resolver context:
SecurityAccessContext security =
SecurityAccessContext.from(context.getRequestContext());
return CompletableFuture.completedFuture(
security.validateAccess(() -> characterRepository.delete(characterId)));
This illustrates the request-context access used by DeleteCharacterMutation.java; the full resolver also removes relationships and handles a missing character. The context is also propagated to subqueries and submutations in the same execution.
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.
For the complete
ExecutionInputAPI and runtime execution model, see the Viaduct API developer reference.