Add TimeoutWebGraphQlInterceptor
This commit adds a new web interceptor that can be configured with a specific duration. If the response is not produced within this timeline, the interceptor sends a HTTP error status to the client (by default, "REQUEST TIMEOUT" but this can be configured) and sends a CANCEL signal upstream. This CANCEL signal flows up to controller methods and maually registered data fetchers, if they have a reactive return type. Processing will be automatically aborted. For other types of date fetchers, applications can retrieve a publisher from the GraphQL context and get notified of cancellations. Closes gh-450
This commit is contained in:
@@ -395,6 +395,35 @@ immediately after the GraphQL Java engine returns, which would be the case if th
|
||||
request is simple enough and did not require asynchronous data fetching.
|
||||
|
||||
|
||||
[[execution.timeout]]
|
||||
== GraphQL Request Timeout
|
||||
|
||||
GraphQL clients can send requests that will consume lots of resources on the server side.
|
||||
There are many ways to protect against this, and one of them is to configure a request timeout.
|
||||
This ensures that requests are closed on the server side if the response takes too long to materialize.
|
||||
|
||||
Spring for GraphQL provides a `TimeoutWebGraphQlInterceptor` for the web transports.
|
||||
Applications can configure this interceptor with a timeout duration; if the request times out, the server errors with a specific HTTP status.
|
||||
In this case, the interceptor will send a "cancel" signal up the chain and reactive data fetchers will automatically cancel any ongoing work.
|
||||
|
||||
This interceptor can be configured on the `WebGraphQlHandler`:
|
||||
|
||||
include-code::WebGraphQlHandlerTimeout[tag=interceptor,indent=0]
|
||||
|
||||
In a Spring Boot application, contributing the interceptor as a bean is enough:
|
||||
|
||||
include-code::HttpTimeoutConfiguration[]
|
||||
|
||||
For more transport-specific timeouts, there are dedicated properties on the handler implementations like
|
||||
`GraphQlWebSocketHandler` and `GraphQlSseHandler`.
|
||||
|
||||
NOTE: While reactive data fetchers are cancelled automatically, this cannot be done for others
|
||||
as there is no consistent way to cancel processing. In this case, controller methods can get
|
||||
the cancellation signal from a `Mono` in the GraphQL context and manually cancel work.
|
||||
|
||||
Here is an example of using the cancellation signal to abort processing inside a controller method:
|
||||
|
||||
include-code::TimeoutController[tag=cancel,indent=0]
|
||||
|
||||
|
||||
[[execution.reactivedatafetcher]]
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2020-2025 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.
|
||||
*/
|
||||
|
||||
package org.springframework.graphql.docs.execution.timeout;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.graphql.server.TimeoutWebGraphQlInterceptor;
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class HttpTimeoutConfiguration {
|
||||
|
||||
@Bean
|
||||
public TimeoutWebGraphQlInterceptor timeoutWebGraphQlInterceptor() {
|
||||
return new TimeoutWebGraphQlInterceptor(Duration.ofSeconds(5));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2020-2025 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.
|
||||
*/
|
||||
|
||||
package org.springframework.graphql.docs.execution.timeout;
|
||||
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import graphql.GraphQLContext;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.graphql.ExecutionGraphQlRequest;
|
||||
import org.springframework.graphql.data.method.annotation.Argument;
|
||||
import org.springframework.graphql.data.method.annotation.QueryMapping;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
@Controller
|
||||
public class TimeoutController {
|
||||
|
||||
BookCache bookCache = new BookCache();
|
||||
|
||||
// tag::cancel[]
|
||||
@QueryMapping
|
||||
public Book bookById(@Argument Long id, GraphQLContext context) throws Exception {
|
||||
|
||||
Mono<Void> cancel = context.get(ExecutionGraphQlRequest.CANCEL_PUBLISHER_CONTEXT_KEY);
|
||||
Future<Book> bookFuture = this.bookCache.fetchBook(id);
|
||||
cancel.doOnCancel(() -> bookFuture.cancel(true)).subscribe();
|
||||
return bookFuture.get();
|
||||
}
|
||||
// end::cancel[]
|
||||
|
||||
record Book(String title, String author) {
|
||||
|
||||
}
|
||||
|
||||
class BookCache {
|
||||
|
||||
public Future<Book> fetchBook(Long id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2020-2025 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.
|
||||
*/
|
||||
|
||||
package org.springframework.graphql.docs.execution.timeout;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.springframework.graphql.execution.DefaultExecutionGraphQlService;
|
||||
import org.springframework.graphql.execution.GraphQlSource;
|
||||
import org.springframework.graphql.server.TimeoutWebGraphQlInterceptor;
|
||||
import org.springframework.graphql.server.WebGraphQlHandler;
|
||||
import org.springframework.graphql.server.webmvc.GraphQlHttpHandler;
|
||||
|
||||
public class WebGraphQlHandlerTimeout {
|
||||
|
||||
void configureWebGraphQlHandler() {
|
||||
GraphQlSource graphQlSource = GraphQlSource.schemaResourceBuilder().build();
|
||||
DefaultExecutionGraphQlService executionGraphQlService = new DefaultExecutionGraphQlService(graphQlSource);
|
||||
|
||||
// tag::interceptor[]
|
||||
TimeoutWebGraphQlInterceptor timeoutInterceptor = new TimeoutWebGraphQlInterceptor(Duration.ofSeconds(5));
|
||||
WebGraphQlHandler webGraphQlHandler = WebGraphQlHandler
|
||||
.builder(executionGraphQlService)
|
||||
.interceptor(timeoutInterceptor)
|
||||
.build();
|
||||
GraphQlHttpHandler httpHandler = new GraphQlHttpHandler(webGraphQlHandler);
|
||||
// end::interceptor[]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user