-
Notifications
You must be signed in to change notification settings - Fork 371
Description
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 .