I'm using graphql-java 20.0 (in spring-boot-starter-graphql in Kotlin) and want to add custom headers to my resolver's responses. Currently, my resolvers return just the entity (of type MyEntity
) that should be included in the graphql response - which works:
@QueryMapping
fun myResolver(
@Argument arg1: String,
@Argument arg2: MyInput,
): MyEntity = service.getMyEntity(arg1, arg2)
I tried changing this to a ResponseEntity as recommended in this SO answer but that's apparently not handled by graphql-java and results in my responses' data being null:
/* this does not work, since graphql-java doe not map the ResponseEntity's body
and results in a graphql response with null in the data object */
@QueryMapping
fun myResolver(
@Argument arg1: String,
@Argument arg2: MyInput,
): ResponseEntity<MyEntity> = ResponseEntity.ok().run {
this.header("X-My-Header", "whatever")
}.body(service.getMyEntity(arg1, arg2))
I could not, however, find any alternative that allows me to set custom headers alongside my response. On StackOverflow, I only found answers for Laravel and Astro. On the graphql-spring-boot repo there's a similar question left unanswered since almost two years.
Does anyone know of a way to set custom headers in my graphql resolvers? I need to do this because my headers need to be different based on some request properties.
If someone find's a cleaner solution, please let me know. But for now, this works:
I created a ThreadLocal
to hold the header value:
object GraphQLMyHeaderThreadLocalStorage {
private val context = ThreadLocal<String>()
var value: String?
get() = context.get()
set(value) = value?.let { context.set(it) } ?: context.remove()
fun clear() = context.remove()
}
In my resolver, I can now set this ThreadLocal
with my request-specific header value:
@QueryMapping
fun myResolver(
@Argument arg1: String,
@Argument arg2: MyInput,
): MyEntity = service.getMyEntity(arg1, arg2).also {
GraphQLMyHeaderThreadLocalStorage.value = "whatever inferred from ${it}"
}
And I can still modify my response in a Filter
if I wrap it in advance and do the modification after chain.doFilter()
:
class GraphQLMyHeaderFilter : Filter {
@Throws(IOException::class, ServletException::class)
override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
if (!response.isCommitted) {
val responseWrapper = object : HttpServletResponseWrapper(response as HttpServletResponse) {
fun updateMyHeader(value: String?) {
setHeader("X-My-Header", value ?: "default value")
}
}
chain.doFilter(request, responseWrapper)
// modify the response after the resolver was called
if (!response.isCommitted) {
val headerValue = try {
GraphQLMyHeaderThreadLocalStorage.value
} finally {
GraphQLMyHeaderThreadLocalStorage.clear()
}
responseWrapper.updateMyHeader(headerValue)
}
} else {
chain.doFilter(request, response)
}
}
}
@Configuration
class FilterConfig {
@Bean
fun graphQLMyHeaderFilter(): FilterRegistrationBean<GraphQLMyHeaderFilter> {
val registrationBean = FilterRegistrationBean<GraphQLMyHeaderFilter>()
registrationBean.filter = GraphQLMyHeaderFilter()
registrationBean.addUrlPatterns("/graphql")
return registrationBean
}
}
Notes:
response.isCommitted
checks were actually not necessary in my experiments, but I'm rather safe than sorry.FilterConfig
. To apply it to all endpoints, you can either use the "/*"
pattern instead of "/graphql"
or delete the FilterConfig
and annotate GraphQLMyHeaderFilter
with @Component
.GraphQLMyHeaderThreadLocalStorage.clear()
afterwards so the state doesn't leak into following requests.Filter
was the only option I found where I can still modify the (uncommitted) response after my resolver was called. ResponseBodyAdvice
was not even called for GraphQL requests in my experiments. HandlerInterceptor
was accessed, but HandlerInterceptor.preHandle()
was executed before the resolver (twice even) and HandlerInterceptor.postHandle()
receives the already committed response (i.e., cannot modify the response anymore).