Configure WebSocket transport with GraphiQL
This commit enhances the GraphiQL `index.html` to rely in the graphiql-tools fetcher function; this supports both HTTP and WebSocket transports. As a follow-up, this also updates the `GraphiQlHandler` implementations to also send a `wsPath` query param if a specific path has been configured for the WebSocket support. As an additional change, this commit also improves the fix for gh-231 as it was incomplete: the generated query params were not taking the context path into account. Closes gh-131 Closes gh-231
This commit is contained in:
@@ -23,8 +23,10 @@ import reactor.core.publisher.Mono;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import org.springframework.web.util.UriBuilder;
|
||||
|
||||
/**
|
||||
* Spring WebFlux handler to serve a GraphiQl UI page.
|
||||
@@ -37,6 +39,8 @@ public class GraphiQlHandler {
|
||||
|
||||
private final String graphQlPath;
|
||||
|
||||
private final String graphQlWsPath;
|
||||
|
||||
private final Resource htmlResource;
|
||||
|
||||
|
||||
@@ -44,18 +48,21 @@ public class GraphiQlHandler {
|
||||
* Constructor that serves the default {@code graphiql/index.html} included
|
||||
* in the {@code spring-graphql} module.
|
||||
* @param graphQlPath the path to the GraphQL endpoint
|
||||
* @param graphQlWsPath optional path to the GraphQL WebSocket endpoint
|
||||
*/
|
||||
public GraphiQlHandler(String graphQlPath) {
|
||||
this(graphQlPath, new ClassPathResource("graphiql/index.html"));
|
||||
public GraphiQlHandler(String graphQlPath, String graphQlWsPath) {
|
||||
this(graphQlPath, graphQlWsPath, new ClassPathResource("graphiql/index.html"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with the HTML page to serve.
|
||||
* @param graphQlPath the path to the GraphQL endpoint
|
||||
* @param graphQlWsPath optional path to the GraphQL WebSocket endpoint
|
||||
* @param htmlResource the GraphiQL page to serve
|
||||
*/
|
||||
public GraphiQlHandler(String graphQlPath, Resource htmlResource) {
|
||||
public GraphiQlHandler(String graphQlPath, String graphQlWsPath, Resource htmlResource) {
|
||||
this.graphQlPath = graphQlPath;
|
||||
this.graphQlWsPath = graphQlWsPath;
|
||||
this.htmlResource = htmlResource;
|
||||
}
|
||||
|
||||
@@ -71,7 +78,20 @@ public class GraphiQlHandler {
|
||||
}
|
||||
|
||||
private URI getRedirectUrl(ServerRequest request) {
|
||||
return request.uriBuilder().queryParam("path", this.graphQlPath).build();
|
||||
UriBuilder builder = request.uriBuilder();
|
||||
String pathQueryParam = applyContextPath(request, this.graphQlPath);
|
||||
builder.queryParam("path", pathQueryParam);
|
||||
|
||||
if (StringUtils.hasText(this.graphQlWsPath)) {
|
||||
String wsPathQueryParam = applyContextPath(request, this.graphQlWsPath);
|
||||
builder.queryParam("wsPath", wsPathQueryParam);
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private String applyContextPath(ServerRequest request, String path) {
|
||||
String contextPath = request.requestPath().contextPath().toString();
|
||||
return StringUtils.hasText(contextPath) ? contextPath + path : path;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,8 +21,11 @@ import java.net.URI;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.servlet.function.ServerRequest;
|
||||
import org.springframework.web.servlet.function.ServerResponse;
|
||||
import org.springframework.web.util.UriBuilder;
|
||||
|
||||
/**
|
||||
* Spring MVC handler to serve a GraphiQl UI page.
|
||||
@@ -35,25 +38,31 @@ public class GraphiQlHandler {
|
||||
|
||||
private final String graphQlPath;
|
||||
|
||||
private final String graphQlWsPath;
|
||||
|
||||
private final Resource htmlResource;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor that serves the default {@code graphiql/index.html} included
|
||||
* in the {@code spring-graphql} module.
|
||||
* @param graphQlPath the path to the GraphQL endpoint
|
||||
* @param graphQlPath the path to the GraphQL HTTP endpoint
|
||||
* @param graphQlWsPath optional path to the GraphQL WebSocket endpoint
|
||||
*/
|
||||
public GraphiQlHandler(String graphQlPath) {
|
||||
this(graphQlPath, new ClassPathResource("graphiql/index.html"));
|
||||
public GraphiQlHandler(String graphQlPath, String graphQlWsPath) {
|
||||
this(graphQlPath, graphQlWsPath, new ClassPathResource("graphiql/index.html"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with the HTML page to serve.
|
||||
* @param graphQlPath the path to the GraphQL endpoint
|
||||
* @param graphQlPath the path to the GraphQL HTTP endpoint
|
||||
* @param graphQlWsPath optional path to the GraphQL WebSocket endpoint
|
||||
* @param htmlResource the GraphiQL page to serve
|
||||
*/
|
||||
public GraphiQlHandler(String graphQlPath, Resource htmlResource) {
|
||||
public GraphiQlHandler(String graphQlPath, String graphQlWsPath, Resource htmlResource) {
|
||||
Assert.hasText(graphQlPath, "graphQlPath should not be empty");
|
||||
this.graphQlPath = graphQlPath;
|
||||
this.graphQlWsPath = graphQlWsPath;
|
||||
this.htmlResource = htmlResource;
|
||||
}
|
||||
|
||||
@@ -71,7 +80,21 @@ public class GraphiQlHandler {
|
||||
private URI getRedirectUrl(ServerRequest request) {
|
||||
String contextPath = request.requestPath().contextPath().toString();
|
||||
String path = request.requestPath().pathWithinApplication().toString();
|
||||
return request.uriBuilder().replacePath(contextPath).path(path).queryParam("path", this.graphQlPath).build();
|
||||
UriBuilder builder = request.uriBuilder().replacePath(contextPath).path(path);
|
||||
|
||||
String pathQueryParam = applyContextPath(request, this.graphQlPath);
|
||||
builder.queryParam("path", pathQueryParam);
|
||||
|
||||
if (StringUtils.hasText(this.graphQlWsPath)) {
|
||||
String wsPathQueryParam = applyContextPath(request, this.graphQlWsPath);
|
||||
builder.queryParam("wsPath", wsPathQueryParam);
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private String applyContextPath(ServerRequest request, String path) {
|
||||
String contextPath = request.requestPath().contextPath().toString();
|
||||
return StringUtils.hasText(contextPath) ? contextPath + path : path;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,18 +1,3 @@
|
||||
<!--
|
||||
~ Copyright 2002-2020 the original author or authors.
|
||||
~
|
||||
~ 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
|
||||
~
|
||||
~ https://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.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
@@ -28,61 +13,29 @@
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!--
|
||||
This GraphiQL example depends on Promise and fetch, which are available in
|
||||
modern browsers, but can be "polyfilled" for older browsers.
|
||||
GraphiQL itself depends on React DOM.
|
||||
If you do not want to rely on a CDN, you can host these files locally or
|
||||
include them directly in your favored resource bunder.
|
||||
-->
|
||||
<script
|
||||
crossorigin
|
||||
src="https://unpkg.com/react@16/umd/react.development.js"
|
||||
></script>
|
||||
<script
|
||||
crossorigin
|
||||
src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"
|
||||
></script>
|
||||
|
||||
<!--
|
||||
These two files can be found in the npm module, however you may wish to
|
||||
copy them directly into your environment, or perhaps include them in your
|
||||
favored resource bundler.
|
||||
-->
|
||||
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
|
||||
<link rel="stylesheet" href="https://unpkg.com/graphiql/graphiql.min.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="graphiql">Loading...</div>
|
||||
<script
|
||||
src="https://unpkg.com/graphiql/graphiql.min.js"
|
||||
type="application/javascript"
|
||||
></script>
|
||||
<script src="https://unpkg.com/graphiql/graphiql.min.js" type="application/javascript"></script>
|
||||
<script>
|
||||
function graphQLFetcher(path) {
|
||||
return (graphQLParams, options = {}) => fetch(
|
||||
`${location.protocol}//${location.host}${path}`,
|
||||
{
|
||||
method: 'post',
|
||||
headers: {
|
||||
...options.headers,
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(graphQLParams),
|
||||
credentials: 'omit',
|
||||
},
|
||||
).then(function (response) {
|
||||
return response.json().catch(function () {
|
||||
return response.text();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const path = params.get("path") || "/graphiql";
|
||||
const gqlFetcher = graphQLFetcher(path);
|
||||
|
||||
const path = params.get("path") || "/graphql";
|
||||
const url = `${location.protocol}//${location.host}${path}`;
|
||||
|
||||
const wsPath = params.get("wsPath") || "/graphql";
|
||||
const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const subscriptionUrl = `${wsProtocol}//${location.host}${wsPath}`;
|
||||
|
||||
const gqlFetcher = GraphiQL.createFetcher({
|
||||
url,
|
||||
subscriptionUrl,
|
||||
});
|
||||
|
||||
ReactDOM.render(
|
||||
React.createElement(GraphiQL, {
|
||||
fetcher: gqlFetcher,
|
||||
|
||||
@@ -45,7 +45,7 @@ class GraphiQlHandlerTests {
|
||||
|
||||
private static final List<HttpMessageReader<?>> MESSAGE_READERS = Collections.emptyList();
|
||||
|
||||
private final GraphiQlHandler handler = new GraphiQlHandler("/graphql",
|
||||
private final GraphiQlHandler handler = new GraphiQlHandler("/graphql", null,
|
||||
new ByteArrayResource("GRAPHIQL".getBytes(StandardCharsets.UTF_8)));
|
||||
|
||||
|
||||
@@ -60,6 +60,19 @@ class GraphiQlHandlerTests {
|
||||
assertThat(response.headers().getLocation().toASCIIString()).isEqualTo("/graphiql?path=/graphql");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRedirectWithPathAndWsPathQueryParameter() {
|
||||
GraphiQlHandler wsHandler = new GraphiQlHandler("/graphql", "/graphql",
|
||||
new ByteArrayResource("GRAPHIQL".getBytes(StandardCharsets.UTF_8)));
|
||||
MockServerHttpRequest httpRequest = MockServerHttpRequest.get("/graphiql").build();
|
||||
MockServerWebExchange exchange = MockServerWebExchange.from(httpRequest);
|
||||
ServerRequest request = ServerRequest.create(exchange, MESSAGE_READERS);
|
||||
ServerResponse response = wsHandler.handleRequest(request).block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.TEMPORARY_REDIRECT);
|
||||
assertThat(response.headers().getLocation()).isNotNull();
|
||||
assertThat(response.headers().getLocation().toASCIIString()).isEqualTo("/graphiql?path=/graphql&wsPath=/graphql");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldServeGraphiQlHtmlResource() {
|
||||
MockServerHttpRequest httpRequest = MockServerHttpRequest.get("/graphiql").queryParam("path", "/graphql").build();
|
||||
@@ -82,7 +95,7 @@ class GraphiQlHandlerTests {
|
||||
ServerResponse response = this.handler.handleRequest(request).block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.TEMPORARY_REDIRECT);
|
||||
assertThat(response.headers().getLocation()).isNotNull();
|
||||
assertThat(response.headers().getLocation().toASCIIString()).isEqualTo("/context/graphiql?path=/graphql");
|
||||
assertThat(response.headers().getLocation().toASCIIString()).isEqualTo("/context/graphiql?path=/context/graphql");
|
||||
}
|
||||
|
||||
private String getResponseContent(MockServerWebExchange exchange, ServerResponse response) {
|
||||
|
||||
@@ -47,7 +47,7 @@ class GraphiQlHandlerTests {
|
||||
|
||||
private static final List<HttpMessageConverter<?>> MESSAGE_READERS = Collections.emptyList();
|
||||
|
||||
private GraphiQlHandler handler = new GraphiQlHandler("/graphql",
|
||||
private GraphiQlHandler handler = new GraphiQlHandler("/graphql", null,
|
||||
new ByteArrayResource("GRAPHIQL".getBytes(StandardCharsets.UTF_8)));
|
||||
|
||||
@Test
|
||||
@@ -60,6 +60,18 @@ class GraphiQlHandlerTests {
|
||||
assertThat(response.headers().getLocation().toASCIIString()).isEqualTo("http://localhost/graphiql?path=/graphql");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRedirectWithPathAndWsPathQueryParameter() {
|
||||
GraphiQlHandler wsHandler = new GraphiQlHandler("/graphql", "/graphql",
|
||||
new ByteArrayResource("GRAPHIQL".getBytes(StandardCharsets.UTF_8)));
|
||||
MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/graphiql");
|
||||
ServerRequest request = ServerRequest.create(servletRequest, MESSAGE_READERS);
|
||||
ServerResponse response = wsHandler.handleRequest(request);
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.TEMPORARY_REDIRECT);
|
||||
assertThat(response.headers().getLocation()).isNotNull();
|
||||
assertThat(response.headers().getLocation().toASCIIString()).isEqualTo("http://localhost/graphiql?path=/graphql&wsPath=/graphql");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldServeGraphiQlHtmlResource() throws Exception {
|
||||
MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/graphiql");
|
||||
@@ -73,13 +85,15 @@ class GraphiQlHandlerTests {
|
||||
|
||||
@Test
|
||||
void shouldConsiderContextPathWhenRedirecting() {
|
||||
GraphiQlHandler wsHandler = new GraphiQlHandler("/graphql", "/graphql",
|
||||
new ByteArrayResource("GRAPHIQL".getBytes(StandardCharsets.UTF_8)));
|
||||
MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/context/graphiql");
|
||||
servletRequest.setContextPath("/context");
|
||||
ServerRequest request = ServerRequest.create(servletRequest, MESSAGE_READERS);
|
||||
ServerResponse response = this.handler.handleRequest(request);
|
||||
ServerResponse response = wsHandler.handleRequest(request);
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.TEMPORARY_REDIRECT);
|
||||
assertThat(response.headers().getLocation()).isNotNull();
|
||||
assertThat(response.headers().getLocation().toASCIIString()).isEqualTo("http://localhost/context/graphiql?path=/graphql");
|
||||
assertThat(response.headers().getLocation().toASCIIString()).isEqualTo("http://localhost/context/graphiql?path=/context/graphql&wsPath=/context/graphql");
|
||||
}
|
||||
|
||||
private String getResponseContent(MockHttpServletRequest servletRequest, ServerResponse response) throws ServletException, IOException {
|
||||
|
||||
Reference in New Issue
Block a user