Skip to content

Commit

Permalink
Merge branch '1.2.x'
Browse files Browse the repository at this point in the history
  • Loading branch information
rstoyanchev committed Apr 22, 2024
2 parents 5977b2d + 7dd9cf5 commit 03c11d0
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 99 deletions.
Expand Up @@ -22,15 +22,18 @@
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.graphql.server.WebGraphQlHandler;
import org.springframework.graphql.server.support.SerializableGraphQlRequest;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.UnsupportedMediaTypeStatusException;

/**
* Abstract class for GraphQL Handler implementations using the HTTP transport.
*
* @author Brian Clozel
* @author Rossen Stoyanchev
*/
class AbstractGraphQlHttpHandler {

Expand All @@ -51,9 +54,26 @@ protected Mono<SerializableGraphQlRequest> readRequest(ServerRequest serverReque
return this.codecDelegate.decode(serverRequest.bodyToFlux(DataBuffer.class), contentType);
}
else {
return serverRequest.bodyToMono(SerializableGraphQlRequest.class);
return serverRequest.bodyToMono(SerializableGraphQlRequest.class)
.onErrorResume(
UnsupportedMediaTypeStatusException.class,
(ex) -> applyApplicationGraphQlFallback(ex, serverRequest));
}
}

private static Mono<SerializableGraphQlRequest> applyApplicationGraphQlFallback(
UnsupportedMediaTypeStatusException ex, ServerRequest request) {

// Spec requires application/json but some clients still use application/graphql
return "application/graphql".equals(request.headers().firstHeader(HttpHeaders.CONTENT_TYPE)) ?
ServerRequest.from(request)
.headers((headers) -> headers.setContentType(MediaType.APPLICATION_JSON))
.body(request.bodyToFlux(DataBuffer.class))
.build()
.bodyToMono(SerializableGraphQlRequest.class)
.log() :
Mono.error(ex);
}


}
Expand Up @@ -27,6 +27,7 @@
import org.springframework.graphql.server.WebGraphQlHandler;
import org.springframework.graphql.server.support.SerializableGraphQlRequest;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
Expand Down Expand Up @@ -81,15 +82,40 @@ protected GraphQlRequest readBody(ServerRequest request) throws ServletException
return (GraphQlRequest) this.messageConverter.read(SerializableGraphQlRequest.class,
new ServletServerHttpRequest(request.servletRequest()));
}
throw new HttpMediaTypeNotSupportedException(contentType, this.messageConverter.getSupportedMediaTypes(), request.method());
throw new HttpMediaTypeNotSupportedException(
contentType, this.messageConverter.getSupportedMediaTypes(), request.method());
}
else {
return request.body(SerializableGraphQlRequest.class);
try {
return request.body(SerializableGraphQlRequest.class);
}
catch (HttpMediaTypeNotSupportedException ex) {
return applyApplicationGraphQlFallback(request, ex);
}
}
}
catch (IOException ex) {
throw new ServerWebInputException("I/O error while reading request body", null, ex);
}
}

private static SerializableGraphQlRequest applyApplicationGraphQlFallback(
ServerRequest request, HttpMediaTypeNotSupportedException ex) throws HttpMediaTypeNotSupportedException {

// Spec requires application/json but some clients still use application/graphql
if ("application/graphql".equals(request.headers().firstHeader(HttpHeaders.CONTENT_TYPE))) {
try {
request = ServerRequest.from(request)
.headers((headers) -> headers.setContentType(MediaType.APPLICATION_JSON))
.body(request.body(byte[].class))
.build();
return request.body(SerializableGraphQlRequest.class);
}
catch (Throwable ex2) {
// ignore
}
}
throw ex;
}

}
Expand Up @@ -26,28 +26,29 @@
import com.jayway.jsonpath.JsonPath;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;

import org.springframework.core.codec.DataBufferEncoder;
import org.springframework.core.io.buffer.DefaultDataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.graphql.GraphQlRequest;
import org.springframework.graphql.GraphQlSetup;
import org.springframework.graphql.server.WebGraphQlHandler;
import org.springframework.graphql.server.support.SerializableGraphQlRequest;
import org.springframework.http.MediaType;
import org.springframework.http.codec.CodecConfigurer;
import org.springframework.http.codec.DecoderHttpMessageReader;
import org.springframework.http.codec.EncoderHttpMessageWriter;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.http.codec.json.Jackson2JsonDecoder;
import org.springframework.http.codec.json.Jackson2JsonEncoder;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.MockServerHttpResponse;
import org.springframework.mock.web.reactive.function.server.MockServerRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange;

import static org.assertj.core.api.Assertions.assertThat;

Expand All @@ -57,72 +58,101 @@
*/
public class GraphQlHttpHandlerTests {

private final GraphQlHttpHandler greetingHandler = GraphQlSetup.schemaContent("type Query { greeting: String }")
.queryFetcher("greeting", (env) -> "Hello").toHttpHandlerWebFlux();
private static final List<HttpMessageReader<?>> MESSAGE_READERS =
List.of(new DecoderHttpMessageReader<>(new Jackson2JsonDecoder()));

private final GraphQlHttpHandler greetingHandler =
GraphQlSetup.schemaContent("type Query { greeting: String }")
.queryFetcher("greeting", (env) -> "Hello")
.toHttpHandlerWebFlux();


@Test
void shouldProduceApplicationJsonByDefault() {
void shouldProduceApplicationJsonByDefault() throws Exception {
String document = "{greeting}";
MockServerHttpRequest httpRequest = MockServerHttpRequest.post("/")
.contentType(MediaType.APPLICATION_JSON).accept(MediaType.ALL).build();
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.ALL)
.body(initRequestBody(document));

MockServerHttpResponse response = handleRequest(httpRequest, this.greetingHandler);

assertThat(response.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
StepVerifier.create(response.getBodyAsString())
.expectNext("{\"data\":{\"greeting\":\"Hello\"}}")
.verifyComplete();
}

@Test
void shouldSupportApplicationGraphQl() throws Exception {
String document = "{greeting}";
MockServerHttpResponse httpResponse = handleRequest(
httpRequest, this.greetingHandler, initRequest(document));
MockServerHttpRequest httpRequest = MockServerHttpRequest.post("/")
.contentType(MediaType.parseMediaType("application/graphql"))
.accept(MediaType.ALL)
.body(initRequestBody(document));

assertThat(httpResponse.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
MockServerHttpResponse response = handleRequest(httpRequest, this.greetingHandler);

assertThat(response.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
StepVerifier.create(response.getBodyAsString())
.expectNext("{\"data\":{\"greeting\":\"Hello\"}}")
.verifyComplete();
}

@Test
void shouldProduceApplicationGraphQl() {
void shouldProduceApplicationGraphQl() throws Exception {
MockServerHttpRequest httpRequest = MockServerHttpRequest.post("/")
.contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_GRAPHQL_RESPONSE).build();
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_GRAPHQL_RESPONSE)
.body(initRequestBody("{greeting}"));

MockServerHttpResponse httpResponse = handleRequest(
httpRequest, this.greetingHandler, initRequest("{greeting}"));
MockServerHttpResponse httpResponse = handleRequest(httpRequest, this.greetingHandler);

assertThat(httpResponse.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_GRAPHQL_RESPONSE);
}

@Test
void shouldProduceApplicationJson() {
void shouldProduceApplicationJson() throws Exception {
MockServerHttpRequest httpRequest = MockServerHttpRequest.post("/")
.contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON).build();
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.body(initRequestBody("{greeting}"));

MockServerHttpResponse httpResponse = handleRequest(
httpRequest, this.greetingHandler, initRequest("{greeting}"));
MockServerHttpResponse httpResponse = handleRequest(httpRequest, this.greetingHandler);

assertThat(httpResponse.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
}

@Test
void locale() {
void locale() throws Exception {
GraphQlHttpHandler handler = GraphQlSetup.schemaContent("type Query { greeting: String }")
.queryFetcher("greeting", (env) -> "Hello in " + env.getLocale())
.toHttpHandlerWebFlux();

MockServerHttpRequest httpRequest = MockServerHttpRequest.post("/")
.contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_GRAPHQL_RESPONSE)
.acceptLanguageAsLocales(Locale.FRENCH).build();
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_GRAPHQL_RESPONSE)
.acceptLanguageAsLocales(Locale.FRENCH)
.body(initRequestBody("{greeting}"));

MockServerHttpResponse httpResponse = handleRequest(
httpRequest, handler, initRequest("{greeting}"));
MockServerHttpResponse httpResponse = handleRequest(httpRequest, handler);

assertThat(httpResponse.getBodyAsString().block())
.isEqualTo("{\"data\":{\"greeting\":\"Hello in fr\"}}");
}

@Test
void shouldSetExecutionId() {
void shouldSetExecutionId() throws Exception {
GraphQlHttpHandler handler = GraphQlSetup.schemaContent("type Query { showId: String }")
.queryFetcher("showId", (env) -> env.getExecutionId().toString())
.toHttpHandlerWebFlux();

MockServerHttpRequest httpRequest = MockServerHttpRequest.post("/")
.contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_GRAPHQL_RESPONSE).build();
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_GRAPHQL_RESPONSE)
.body(initRequestBody("{showId}"));

MockServerHttpResponse httpResponse = handleRequest(
httpRequest, handler, initRequest("{showId}"));
MockServerHttpResponse httpResponse = handleRequest(httpRequest, handler);

DocumentContext document = JsonPath.parse(httpResponse.getBodyAsString().block());
String id = document.read("data.showId", String.class);
Expand All @@ -134,24 +164,25 @@ void shouldUseCustomCodec() {
WebGraphQlHandler webGraphQlHandler = GraphQlSetup.schemaContent("type Query { showId: String }")
.queryFetcher("showId", (env) -> env.getExecutionId().toString())
.toWebGraphQlHandler();

ObjectMapper mapper = new ObjectMapper();
CodecConfigurer configurer = ServerCodecConfigurer.create();
configurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(mapper));
configurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(mapper));
GraphQlHttpHandler httpHandler = new GraphQlHttpHandler(webGraphQlHandler, configurer);

byte[] bytes = "{\"query\": \"{showId}\"}".getBytes(StandardCharsets.UTF_8);
Flux<DefaultDataBuffer> body = Flux.just(DefaultDataBufferFactory.sharedInstance.wrap(bytes));

MockServerHttpRequest httpRequest = MockServerHttpRequest.post("/")
.contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_GRAPHQL_RESPONSE).build();
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_GRAPHQL_RESPONSE)
.body(body);

MockServerWebExchange exchange = MockServerWebExchange.from(httpRequest);
MockServerRequest serverRequest = MockServerRequest.builder()
.exchange(exchange)
.uri(((ServerWebExchange) exchange).getRequest().getURI())
.method(((ServerWebExchange) exchange).getRequest().getMethod())
.headers(((ServerWebExchange) exchange).getRequest().getHeaders())
.body(Flux.just(DefaultDataBufferFactory.sharedInstance.wrap("{\"query\":\"{showId}\"}".getBytes(StandardCharsets.UTF_8))));

httpHandler.handleRequest(serverRequest)
ServerRequest request = ServerRequest.create(exchange, configurer.getReaders());

new GraphQlHttpHandler(webGraphQlHandler, configurer)
.handleRequest(request)
.flatMap(response -> response.writeTo(exchange, new EmptyContext()))
.block();

Expand All @@ -160,23 +191,15 @@ void shouldUseCustomCodec() {
assertThat(id).isEqualTo(httpRequest.getId());
}

private static SerializableGraphQlRequest initRequest(String document) {
private static String initRequestBody(String document) throws Exception {
SerializableGraphQlRequest request = new SerializableGraphQlRequest();
request.setQuery(document);
return request;
return new ObjectMapper().writeValueAsString(request);
}

private MockServerHttpResponse handleRequest(
MockServerHttpRequest httpRequest, GraphQlHttpHandler handler, GraphQlRequest body) {

private MockServerHttpResponse handleRequest(MockServerHttpRequest httpRequest, GraphQlHttpHandler handler) {
MockServerWebExchange exchange = MockServerWebExchange.from(httpRequest);

MockServerRequest serverRequest = MockServerRequest.builder()
.exchange(exchange)
.uri(((ServerWebExchange) exchange).getRequest().getURI())
.method(((ServerWebExchange) exchange).getRequest().getMethod())
.headers(((ServerWebExchange) exchange).getRequest().getHeaders())
.body(Mono.just(body));
ServerRequest serverRequest = ServerRequest.create(exchange, MESSAGE_READERS);

handler.handleRequest(serverRequest)
.flatMap(response -> response.writeTo(exchange, new DefaultContext()))
Expand Down

0 comments on commit 03c11d0

Please sign in to comment.