Skip to content

Commit

Permalink
Merge branch 'master' into fix/2032
Browse files Browse the repository at this point in the history
  • Loading branch information
paulbakker authored Jan 8, 2025
2 parents a361a55 + 7a65cde commit cc0b44e
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 167 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ DGS 10.0.0 finalizes the integration work by removing all the legacy modules and
This greatly reduces the footprint of the codebase, which will speed up feature development into the future!

Although the list of changes is large, you probably won't notice the difference for your applications!
Just make sure to use the (new) `netflix.graphql.dgs:dgs-starter` AKA `netflix.graphql.dgs:graphql-dgs-spring-graphql-starter` starter!
Just make sure to use the (new) `com.netflix.graphql.dgs:dgs-starter` AKA `com.netflix.graphql.dgs:graphql-dgs-spring-graphql-starter` starter!

See [release notes](https://github.com/Netflix/dgs-framework/releases/tag/v10.0.0) for a detailed overview of changes.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import com.netflix.graphql.dgs.DgsDataLoaderCustomizer
import com.netflix.graphql.dgs.DgsDataLoaderInstrumentation
import com.netflix.graphql.dgs.DgsDataLoaderOptionsProvider
import com.netflix.graphql.dgs.DgsDefaultPreparsedDocumentProvider
import com.netflix.graphql.dgs.DgsExecutionResult
import com.netflix.graphql.dgs.DgsFederationResolver
import com.netflix.graphql.dgs.DgsQueryExecutor
import com.netflix.graphql.dgs.DgsRuntimeWiring
Expand Down Expand Up @@ -102,6 +103,8 @@ import org.springframework.graphql.execution.RuntimeWiringConfigurer
import org.springframework.graphql.execution.SchemaReport
import org.springframework.graphql.execution.SelfDescribingDataFetcher
import org.springframework.graphql.execution.SubscriptionExceptionResolver
import org.springframework.graphql.server.WebGraphQlInterceptor
import org.springframework.graphql.server.WebGraphQlResponse
import org.springframework.http.HttpHeaders
import org.springframework.mock.web.MockHttpServletRequest
import org.springframework.web.bind.support.WebDataBinderFactory
Expand Down Expand Up @@ -520,6 +523,30 @@ open class DgsSpringGraphQLAutoConfiguration(
graphQLContextContributors,
)

/**
* Backward compatibility for setting response headers through a "dgs-response-headers" field in extensions, or using DgsExecutionResult.
* While this can easily be done through a custom WebGraphQlInterceptor, this bean provides backward compatibility with older code.
*/
@Bean
@ConditionalOnProperty(
prefix = "${AUTO_CONF_PREFIX}.dgs-response-headers",
name = ["enabled"],
havingValue = "true",
matchIfMissing = true,
)
open fun dgsHeadersInterceptor(): WebGraphQlInterceptor =
WebGraphQlInterceptor { request, chain ->
chain.next(request).doOnNext { response: WebGraphQlResponse ->
val responseHeadersExtension = response.extensions["dgs-response-headers"]
if (responseHeadersExtension is HttpHeaders) {
response.responseHeaders.addAll(responseHeadersExtension)
}
if (response.executionResult is DgsExecutionResult) {
response.responseHeaders.addAll((response.executionResult as DgsExecutionResult).headers)
}
}
}

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
open class WebMvcConfiguration(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* Copyright 2025 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.netflix.graphql.dgs.springgraphql.autoconfig

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.netflix.graphql.dgs.DgsComponent
import com.netflix.graphql.dgs.DgsExecutionResult
import com.netflix.graphql.dgs.DgsQuery
import graphql.ExecutionResult
import graphql.execution.instrumentation.InstrumentationState
import graphql.execution.instrumentation.SimplePerformantInstrumentation
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration
import org.springframework.boot.autoconfigure.graphql.servlet.GraphQlWebMvcAutoConfiguration
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.http.HttpHeaders
import org.springframework.http.MediaType
import org.springframework.stereotype.Component
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.post
import java.util.concurrent.CompletableFuture

@SpringBootTest(
classes = [
DgsHeadersSmokeTest.TestApp::class,
DgsSpringGraphQLAutoConfiguration::class,
GraphQlAutoConfiguration::class,
GraphQlWebMvcAutoConfiguration::class,
WebMvcAutoConfiguration::class,
],
properties = [
"dgs.graphql.schema-locations=classpath:/dgs-spring-graphql-smoke-test.graphqls",
],
)
@AutoConfigureMockMvc
class DgsHeadersSmokeTest {
@Autowired
lateinit var mockMvc: MockMvc

@Test
fun `Response headers can be set by using DgsExecutionResult`() {
val query =
"""
query {
dgsField
}
""".trimIndent()

data class GraphQlRequest(
val query: String,
)

mockMvc
.post("/graphql") {
content = jacksonObjectMapper().writeValueAsString(GraphQlRequest(query))
accept = MediaType.APPLICATION_JSON
contentType = MediaType.APPLICATION_JSON
}.andExpect {
status { isOk() }
header { string("dgsexecutionresultheader", "set from DgsExecutionResult") }
header { string("extensionheader", "set from extensions") }
}
}

@TestConfiguration
open class TestApp {
@DgsComponent
open class DgsTestDatafetcher {
@DgsQuery
fun dgsField(): String = "test from DGS"

@Component
open class MyIntrospection : SimplePerformantInstrumentation() {
override fun instrumentExecutionResult(
executionResult: ExecutionResult,
parameters: InstrumentationExecutionParameters,
state: InstrumentationState?,
): CompletableFuture<ExecutionResult> {
val headers = HttpHeaders()
headers.add("dgsexecutionresultheader", "set from DgsExecutionResult")

val extensionHeaders = HttpHeaders()
extensionHeaders.add("extensionheader", "set from extensions")
val updatedExecutionResult =
executionResult.transform { r ->
r.extensions(
mapOf(
Pair(
"dgs-response-headers",
extensionHeaders,
),
),
)
}

return CompletableFuture.completedFuture(
DgsExecutionResult
.builder()
.executionResult(updatedExecutionResult)
.headers(headers)
.build(),
)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,81 +18,14 @@ package com.netflix.graphql.dgs

import graphql.ExecutionResult
import graphql.ExecutionResultImpl
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity

class DgsExecutionResult(
private val executionResult: ExecutionResult,
private var headers: HttpHeaders,
val status: HttpStatus,
val headers: HttpHeaders,
val status: HttpStatus = HttpStatus.OK,
) : ExecutionResult by executionResult {
init {
addExtensionsHeaderKeyToHeader()
}

/** Read-Only reference to the HTTP Headers. */
fun headers(): HttpHeaders = HttpHeaders.readOnlyHttpHeaders(headers)

fun toSpringResponse(): ResponseEntity<Any> =
ResponseEntity(
toSpecification(),
headers,
status,
)

// Refer to https://github.com/Netflix/dgs-framework/pull/1261 for further details.
override fun toSpecification(): MutableMap<String, Any> {
val spec = executionResult.toSpecification()

val extensions =
spec["extensions"] as Map<*, *>?
?: return spec

if (DGS_RESPONSE_HEADERS_KEY in extensions) {
if (extensions.size == 1) {
spec -= "extensions"
} else {
spec["extensions"] = extensions - DGS_RESPONSE_HEADERS_KEY
}
}

return spec
}

// Refer to https://github.com/Netflix/dgs-framework/pull/1261 for further details.
private fun addExtensionsHeaderKeyToHeader() {
val extensions =
executionResult.extensions
?: return

val dgsResponseHeaders =
extensions[DGS_RESPONSE_HEADERS_KEY]
?: return

if (dgsResponseHeaders is Map<*, *> && dgsResponseHeaders.isNotEmpty()) {
// If the HttpHeaders are empty/read-only we need to switch to a new instance that allows us
// to store the headers that are part of the GraphQL response _extensions_.

val updatedHeaders = HttpHeaders.writableHttpHeaders(headers)

dgsResponseHeaders.forEach { (key, value) ->
if (key != null) {
updatedHeaders.add(key.toString(), value?.toString())
}
}
headers = HttpHeaders.readOnlyHttpHeaders(updatedHeaders)
} else {
logger.warn(
"{} must be of type java.util.Map, but was {}",
DGS_RESPONSE_HEADERS_KEY,
dgsResponseHeaders.javaClass.name,
)
}
}

/**
* Facilitate the construction of a [DgsExecutionResult] instance.
*/
Expand Down Expand Up @@ -128,10 +61,6 @@ class DgsExecutionResult(
}

companion object {
// defined in here and DgsRestController, for backwards compatibility. Keep these two variables synced.
const val DGS_RESPONSE_HEADERS_KEY = "dgs-response-headers"
private val logger: Logger = LoggerFactory.getLogger(DgsExecutionResult::class.java)

@JvmStatic
fun builder(): Builder = Builder()
}
Expand Down

This file was deleted.

0 comments on commit cc0b44e

Please sign in to comment.