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 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?) {
if (value != null) {
setHeader("X-My-Header", value)
} else {
setHeader("X-My-Header", "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.updateCacheControl(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).