Skip to content

Not possible to have a custom context for subscription methods #945

@gerald24

Description

@gerald24

Library Version
com.expediagroup:graphql-kotlin-spring-server:4.0.0-alpha.7

Describe the bug
Unable to provide a custom context for subscriptions

To Reproduce
Subscription Example (it's not important here if context is of type GraphQLContext or any other, because currently a new instance of graphql.GraphQLContext will be provided anyway - see GraphQLRequest extension below):

import com.expediagroup.graphql.spring.operations.Subscription
import graphql.GraphQLContext
import org.springframework.stereotype.Component
import reactor.core.publisher.Flux
import java.time.Duration
import java.util.*

@Component
class NotificationSubscription : Subscription {

    fun notifications(context: GraphQLContext): Flux<String> {

        return Flux.interval(Duration.ofSeconds(5))
            .map {
                UUID.randomUUID().toString()
            }
    }
}

is not possible since on start we get an exception:

Caused by: com.expediagroup.graphql.exceptions.TypeNotSupportedException: Cannot convert graphql.GraphQLContext since it is not a valid GraphQL type or outside the supported packages

Adding "graphql" to graphql.packages config property is not desired and will result in an exception anyway:

Caused by: com.expediagroup.graphql.exceptions.EmptyInputObjectTypeException: Invalid GraphQLContextInput input object type - input object does not expose any fields.

But just noted - graphql.GraphQLContext is the default context, generated by the GraphQLRequest#toExecutionInput extension, if the Mono-subscriber-context will not have one:

fun GraphQLRequest.toExecutionInput(graphQLContext: Any? = null, dataLoaderRegistry: DataLoaderRegistry? = null): ExecutionInput =
    ExecutionInput.newExecutionInput()
        .query(this.query)
        .operationName(this.operationName)
        .variables(this.variables ?: emptyMap())
        .context(graphQLContext ?: GraphQLContext.newContext().build())
        .dataLoaderRegistry(dataLoaderRegistry ?: DataLoaderRegistry())
        .build()

Here is the default implementation of the SubscriptionHandler (SimpleSubscriptionHandler):

    override fun executeSubscription(graphQLRequest: GraphQLRequest): Flux<GraphQLResponse<*>> = Mono.subscriberContext()
        .flatMapMany { reactorContext ->
            val graphQLContext = reactorContext.getOrDefault<Any>(GRAPHQL_CONTEXT_KEY, null)
            graphQL.execute(graphQLRequest.toExecutionInput(graphQLContext))
                .getData<Publisher<ExecutionResult>>()
                .toFlux()
                .map { result -> result.toGraphQLResponse() }
                .onErrorResume { throwable ->
                    val error = SimpleKotlinGraphQLError(throwable).toGraphQLKotlinType()
                    Flux.just(GraphQLResponse<Any>(errors = listOf(error)))
                }
        }

But since I'm not able to add my context to the subscriber context (e.g in my custom ApolloSubscriptionHooks), the evaluation of reactorContext.getOrDefault<Any>(GRAPHQL_CONTEXT_KEY, null) will always be null, and therefore the GraphQLRequest.toExecutionInput extension will always offer a new instance of graphql.GraphQLContext.

Actually, the ApolloSubscriptionProtocolHandler offers a hook for onOperation, but will not link to the result of startSubscription and therefore the chain is broken

class ApolloSubscriptionProtocolHandler
  ...
    private fun onOperation(operationMessage: SubscriptionOperationMessage, session: WebSocketSession, graphQLContext: Any?): Flux<SubscriptionOperationMessage> =
        subscriptionHooks.onOperation(operationMessage, session, graphQLContext)
            .flatMapMany { startSubscription(operationMessage, session) 

Expected behavior

@Component
class NotificationSubscription : Subscription {

    fun notifications(context: MyContext): Flux<String> {

       // any logic with context ...

        return Flux.interval(Duration.ofSeconds(5))
            .map {
                UUID.randomUUID().toString()
            }
    }
}

As far as I understand reactive I should be possible to add my context like this:

  startSubscription(operationMessage, session).subscriberContext { it.put(GRAPHQL_CONTEXT_KEY, MyContext(...)) }

My application

I can extract the handshake info for the web socket connection in my custom ApolloSubscriptionHooks through the WebSocketSession, which holds my (JWT) access token as query parameter. With this token I can provide a user context to my subscription method .

Metadata

Metadata

Assignees

No one assigned

    Labels

    type: bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions