Skip to content

Commit fdd8cb5

Browse files
committed
fix(#469): support async timeout with proper error
1 parent 159eb3d commit fdd8cb5

File tree

5 files changed

+73
-28
lines changed

5 files changed

+73
-28
lines changed

build.gradle

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ plugins {
3131
id "org.sonarqube" version "3.2.0"
3232
id "jacoco"
3333
id 'io.codearte.nexus-staging' version '0.30.0'
34-
id 'com.github.sherter.google-java-format' version '0.9' apply false
3534
}
3635

3736
sonarqube {
@@ -49,7 +48,6 @@ subprojects {
4948
apply plugin: 'java'
5049
apply plugin: 'maven-publish'
5150
apply plugin: 'signing'
52-
apply plugin: 'com.github.sherter.google-java-format'
5351

5452
repositories {
5553
mavenLocal()
@@ -80,8 +78,6 @@ subprojects {
8078

8179
compileJava.dependsOn(processResources)
8280

83-
compileJava.mustRunAfter verifyGoogleJavaFormat
84-
8581
test {
8682
useJUnitPlatform()
8783

graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLInvoker.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
import java.util.List;
1313
import java.util.concurrent.CompletableFuture;
1414
import lombok.RequiredArgsConstructor;
15+
import lombok.extern.slf4j.Slf4j;
1516

17+
@Slf4j
1618
@RequiredArgsConstructor
1719
public class GraphQLInvoker {
1820

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package graphql.kickstart.servlet;
2+
3+
import java.io.IOException;
4+
import javax.servlet.AsyncEvent;
5+
import javax.servlet.AsyncListener;
6+
7+
interface AsyncTimeoutListener extends AsyncListener {
8+
9+
default void onComplete(AsyncEvent event) throws IOException {}
10+
11+
default void onError(AsyncEvent event) throws IOException {}
12+
13+
default void onStartAsync(AsyncEvent event) throws IOException {}
14+
}

graphql-java-servlet/src/main/java/graphql/kickstart/servlet/HttpRequestInvokerImpl.java

Lines changed: 56 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,20 @@
22

33
import static graphql.kickstart.servlet.HttpRequestHandler.STATUS_BAD_REQUEST;
44

5+
import graphql.ExecutionResultImpl;
56
import graphql.kickstart.execution.GraphQLInvoker;
67
import graphql.kickstart.execution.GraphQLQueryResult;
8+
import graphql.kickstart.execution.error.GenericGraphQLError;
79
import graphql.kickstart.execution.input.GraphQLBatchedInvocationInput;
810
import graphql.kickstart.execution.input.GraphQLInvocationInput;
911
import graphql.kickstart.execution.input.GraphQLSingleInvocationInput;
1012
import graphql.kickstart.servlet.input.BatchInputPreProcessResult;
1113
import graphql.kickstart.servlet.input.BatchInputPreProcessor;
1214
import java.io.IOException;
1315
import java.io.UncheckedIOException;
16+
import java.util.Optional;
1417
import java.util.concurrent.CompletableFuture;
18+
import java.util.concurrent.atomic.AtomicReference;
1519
import javax.servlet.AsyncContext;
1620
import javax.servlet.http.HttpServletRequest;
1721
import javax.servlet.http.HttpServletResponse;
@@ -31,43 +35,65 @@ public void execute(
3135
GraphQLInvocationInput invocationInput,
3236
HttpServletRequest request,
3337
HttpServletResponse response) {
38+
ListenerHandler listenerHandler =
39+
ListenerHandler.start(request, response, configuration.getListeners());
3440
if (request.isAsyncSupported()) {
35-
AsyncContext asyncContext =
36-
request.isAsyncStarted()
37-
? request.getAsyncContext()
38-
: request.startAsync(request, response);
39-
asyncContext.setTimeout(configuration.getAsyncTimeout());
40-
invokeAndHandle(invocationInput, request, response)
41-
.thenAccept(aVoid -> asyncContext.complete());
41+
invokeAndHandleAsync(invocationInput, request, response, listenerHandler);
4242
} else {
43-
invokeAndHandle(invocationInput, request, response).join();
43+
invokeAndHandle(invocationInput, request, response, listenerHandler).join();
4444
}
4545
}
4646

47+
private void invokeAndHandleAsync(
48+
GraphQLInvocationInput invocationInput,
49+
HttpServletRequest request,
50+
HttpServletResponse response,
51+
ListenerHandler listenerHandler) {
52+
AsyncContext asyncContext =
53+
request.isAsyncStarted()
54+
? request.getAsyncContext()
55+
: request.startAsync(request, response);
56+
asyncContext.setTimeout(configuration.getAsyncTimeout());
57+
AtomicReference<CompletableFuture<Void>> futureHolder = new AtomicReference<>();
58+
AsyncTimeoutListener timeoutListener =
59+
event -> {
60+
Optional.ofNullable(futureHolder.get()).ifPresent(it -> it.cancel(true));
61+
writeResultResponse(
62+
invocationInput,
63+
GraphQLQueryResult.create(
64+
new ExecutionResultImpl(new GenericGraphQLError("Timeout"))),
65+
(HttpServletRequest) event.getAsyncContext().getRequest(),
66+
(HttpServletResponse) event.getAsyncContext().getResponse());
67+
listenerHandler.onError(event.getThrowable());
68+
};
69+
asyncContext.addListener(timeoutListener);
70+
asyncContext.start(
71+
() ->
72+
futureHolder.set(
73+
invokeAndHandle(invocationInput, request, response, listenerHandler)
74+
.thenAccept(aVoid -> asyncContext.complete())));
75+
}
76+
4777
private CompletableFuture<Void> invokeAndHandle(
4878
GraphQLInvocationInput invocationInput,
4979
HttpServletRequest request,
50-
HttpServletResponse response) {
51-
ListenerHandler listenerHandler =
52-
ListenerHandler.start(request, response, configuration.getListeners());
80+
HttpServletResponse response,
81+
ListenerHandler listenerHandler) {
5382
return invoke(invocationInput, request, response)
54-
.thenAccept(
55-
result ->
56-
writeResultResponse(invocationInput, result, request, response, listenerHandler))
57-
.exceptionally(t -> writeErrorResponse(t, response, listenerHandler))
58-
.thenAccept(aVoid -> listenerHandler.onFinally());
83+
.thenAccept(it -> writeResultResponse(invocationInput, it, request, response))
84+
.thenAccept(it -> listenerHandler.onSuccess())
85+
.exceptionally(t -> writeBadRequestError(t, response, listenerHandler))
86+
.thenAccept(it -> listenerHandler.onFinally());
5987
}
6088

6189
private void writeResultResponse(
6290
GraphQLInvocationInput invocationInput,
6391
GraphQLQueryResult queryResult,
6492
HttpServletRequest request,
65-
HttpServletResponse response,
66-
ListenerHandler listenerHandler) {
93+
HttpServletResponse response) {
6794
QueryResponseWriter queryResponseWriter = createWriter(invocationInput, queryResult);
6895
try {
6996
queryResponseWriter.write(request, response);
70-
listenerHandler.onSuccess();
7197
} catch (IOException e) {
7298
throw new UncheckedIOException(e);
7399
}
@@ -78,12 +104,18 @@ protected QueryResponseWriter createWriter(
78104
return queryResponseWriterFactory.createWriter(invocationInput, queryResult, configuration);
79105
}
80106

81-
private Void writeErrorResponse(
107+
private Void writeBadRequestError(
82108
Throwable t, HttpServletResponse response, ListenerHandler listenerHandler) {
83-
response.setStatus(STATUS_BAD_REQUEST);
84-
log.info(
85-
"Bad request: path was not \"/schema.json\" or no query variable named \"query\" given", t);
86-
listenerHandler.onError(t);
109+
if (!response.isCommitted()) {
110+
response.setStatus(STATUS_BAD_REQUEST);
111+
log.info(
112+
"Bad request: path was not \"/schema.json\" or no query variable named \"query\" given",
113+
t);
114+
listenerHandler.onError(t);
115+
} else {
116+
log.warn(
117+
"Cannot write GraphQL response, because the HTTP response is already committed. It most likely timed out.");
118+
}
87119
return null;
88120
}
89121

graphql-java-servlet/src/main/java/graphql/kickstart/servlet/SingleQueryResponseWriter.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,6 @@ public void write(HttpServletRequest request, HttpServletResponse response) thro
2323
byte[] contentBytes = graphQLObjectMapper.serializeResultAsBytes(result);
2424
response.setContentLength(contentBytes.length);
2525
response.getOutputStream().write(contentBytes);
26+
response.getOutputStream().flush();
2627
}
2728
}

0 commit comments

Comments
 (0)