Skip to content

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()
    }
}

View full file on GitHub

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();
  }
}

View full file on GitHub

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())
    }

View full file on GitHub

@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()));
  }

View full file on GitHub

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)
            }
    }

View full file on GitHub

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);
                }));
  }
}

View full file on GitHub

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:

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

View full file on GitHub

ExecutionInput executionInput =
    ExecutionInput.builder()
        .operationText(query)
        .variables(readVariables(request))
        .requestContext(
            Map.of(
                SecurityAccessContext.REQUEST_CONTEXT_KEY,
                new SecurityAccessContext(securityAccess)))
        .build();

View full file on GitHub

The context can carry application-specific values; construct it explicitly when preparing the execution input:

 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
 )
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:

@Resolver
class FooQuery : FooResolver.Character() {
    override suspend fun resolve(ctx: Context): Foo? {
        val requestContext = ctx.requestContext as? Map<*, *>
        ...
    }
}
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 ExecutionInput API and runtime execution model, see the Viaduct API developer reference.