-
Notifications
You must be signed in to change notification settings - Fork 113
Make HttpRequestHandlerImpl.handle() async when possible (fixes #259) #268
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
cfad30e
2e9f8b2
b5fe5c7
2c9e1d3
c28f9cf
a207496
65b28b2
9895e0c
29b3421
05fa3af
fb087e1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ | |
import graphql.kickstart.execution.input.GraphQLBatchedInvocationInput; | ||
import graphql.kickstart.execution.input.GraphQLInvocationInput; | ||
import graphql.kickstart.execution.input.GraphQLSingleInvocationInput; | ||
import java.util.ArrayList; | ||
import java.util.List; | ||
import java.util.concurrent.CompletableFuture; | ||
import lombok.AllArgsConstructor; | ||
|
@@ -28,26 +29,36 @@ public CompletableFuture<ExecutionResult> executeAsync( | |
} | ||
|
||
public GraphQLQueryResult query(GraphQLInvocationInput invocationInput) { | ||
return queryAsync(invocationInput).join(); | ||
} | ||
|
||
public CompletableFuture<GraphQLQueryResult> queryAsync(GraphQLInvocationInput invocationInput) { | ||
if (invocationInput instanceof GraphQLSingleInvocationInput) { | ||
return GraphQLQueryResult.create(query((GraphQLSingleInvocationInput) invocationInput)); | ||
return executeAsync((GraphQLSingleInvocationInput)invocationInput).thenApply(GraphQLQueryResult::create); | ||
} | ||
GraphQLBatchedInvocationInput batchedInvocationInput = (GraphQLBatchedInvocationInput) invocationInput; | ||
return GraphQLQueryResult.create(query(batchedInvocationInput)); | ||
return executeAsync(batchedInvocationInput).thenApply(GraphQLQueryResult::create); | ||
} | ||
|
||
private ExecutionResult query(GraphQLSingleInvocationInput singleInvocationInput) { | ||
return executeAsync(singleInvocationInput).join(); | ||
private CompletableFuture<List<ExecutionResult>> executeAsync(GraphQLBatchedInvocationInput batchedInvocationInput) { | ||
GraphQL graphQL = batchedDataLoaderGraphQLBuilder.newGraphQL(batchedInvocationInput, graphQLBuilder); | ||
return sequence( | ||
batchedInvocationInput.getExecutionInputs().stream() | ||
.map(executionInput -> proxy.executeAsync(graphQL, executionInput)) | ||
.collect(toList())); | ||
} | ||
|
||
private List<ExecutionResult> query(GraphQLBatchedInvocationInput batchedInvocationInput) { | ||
GraphQL graphQL = batchedDataLoaderGraphQLBuilder | ||
.newGraphQL(batchedInvocationInput, graphQLBuilder); | ||
return batchedInvocationInput.getExecutionInputs().stream() | ||
.map(executionInput -> proxy.executeAsync(graphQL, executionInput)) | ||
.collect(toList()) | ||
.stream() | ||
.map(CompletableFuture::join) | ||
.collect(toList()); | ||
@SuppressWarnings({"unchecked", "rawtypes"}) | ||
private <T> CompletableFuture<List<T>> sequence(List<CompletableFuture<T>> futures) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unfortunately the JDK doesn't provide this common operation out of the box 😞 |
||
CompletableFuture[] futuresArray = futures.toArray(new CompletableFuture[0]); | ||
return CompletableFuture.allOf(futuresArray).thenApply(aVoid -> { | ||
olim7t marked this conversation as resolved.
Show resolved
Hide resolved
|
||
List<T> result = new ArrayList<>(futures.size()); | ||
for (CompletableFuture future : futuresArray) { | ||
assert future.isDone(); // per the API contract of allOf() | ||
result.add((T) future.join()); | ||
} | ||
return result; | ||
}); | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
package graphql.kickstart.servlet; | ||
|
||
import com.fasterxml.jackson.core.JsonProcessingException; | ||
import graphql.GraphQLException; | ||
import graphql.kickstart.execution.GraphQLInvoker; | ||
import graphql.kickstart.execution.GraphQLQueryResult; | ||
|
@@ -9,6 +10,9 @@ | |
import graphql.kickstart.servlet.input.BatchInputPreProcessResult; | ||
import graphql.kickstart.servlet.input.BatchInputPreProcessor; | ||
import java.io.IOException; | ||
import java.io.UncheckedIOException; | ||
import java.util.concurrent.CompletableFuture; | ||
import javax.servlet.AsyncContext; | ||
import javax.servlet.http.HttpServletRequest; | ||
import javax.servlet.http.HttpServletResponse; | ||
import lombok.extern.slf4j.Slf4j; | ||
|
@@ -36,11 +40,11 @@ public void handle(HttpServletRequest request, HttpServletResponse response) thr | |
GraphQLInvocationInput invocationInput = invocationInputParser | ||
.getGraphQLInvocationInput(request, response); | ||
execute(invocationInput, request, response); | ||
} catch (GraphQLException e) { | ||
} catch (GraphQLException| JsonProcessingException e) { | ||
response.setStatus(STATUS_BAD_REQUEST); | ||
log.info("Bad request: cannot handle http request", e); | ||
throw e; | ||
} catch (Throwable t) { | ||
} catch (Exception t) { | ||
response.setStatus(500); | ||
log.error("Cannot handle http request", t); | ||
throw t; | ||
|
@@ -49,38 +53,65 @@ public void handle(HttpServletRequest request, HttpServletResponse response) thr | |
|
||
protected void execute(GraphQLInvocationInput invocationInput, HttpServletRequest request, | ||
HttpServletResponse response) throws IOException { | ||
GraphQLQueryResult queryResult = invoke(invocationInput, request, response); | ||
if (request.isAsyncSupported()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unrelated comment. as you are touching this already. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are those the only IOException instances that can happen here? Just making sure that we're not also catching others that would qualify as legitimate server errors. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Official method signature throws IOException. So it's hard to guarantee that. The use case right now is that servlet returns http 500 for bad requests that can't be deserialized. And that makes external attacks last longer. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added JsonProcessingException instead of IOException. |
||
AsyncContext asyncContext = request.isAsyncStarted() | ||
? request.getAsyncContext() | ||
: request.startAsync(request, response); | ||
asyncContext.setTimeout(configuration.getAsyncTimeout()); | ||
invoke(invocationInput, request, response) | ||
.thenAccept(result -> writeResultResponse(invocationInput, result, request, response)) | ||
.exceptionally(t -> writeErrorResponse(t, response)) | ||
.thenAccept(aVoid -> asyncContext.complete()); | ||
} else { | ||
try { | ||
GraphQLQueryResult result = invoke(invocationInput, request, response).join(); | ||
writeResultResponse(invocationInput, result, request, response); | ||
} catch (Exception t) { | ||
writeErrorResponse(t, response); | ||
} | ||
} | ||
} | ||
|
||
private void writeResultResponse(GraphQLInvocationInput invocationInput, GraphQLQueryResult queryResult, HttpServletRequest request, | ||
HttpServletResponse response) { | ||
QueryResponseWriter queryResponseWriter = createWriter(invocationInput, queryResult); | ||
queryResponseWriter.write(request, response); | ||
try { | ||
queryResponseWriter.write(request, response); | ||
} catch (IOException e) { | ||
throw new UncheckedIOException(e); | ||
} | ||
} | ||
|
||
protected QueryResponseWriter createWriter(GraphQLInvocationInput invocationInput, | ||
GraphQLQueryResult queryResult) { | ||
protected QueryResponseWriter createWriter(GraphQLInvocationInput invocationInput, GraphQLQueryResult queryResult) { | ||
return QueryResponseWriter.createWriter(queryResult, configuration.getObjectMapper(), | ||
configuration.getSubscriptionTimeout()); | ||
} | ||
|
||
private GraphQLQueryResult invoke(GraphQLInvocationInput invocationInput, | ||
HttpServletRequest request, | ||
private Void writeErrorResponse(Throwable t, HttpServletResponse response) { | ||
response.setStatus(STATUS_BAD_REQUEST); | ||
log.info("Bad GET request: path was not \"/schema.json\" or no query variable named \"query\" given", t); | ||
return null; | ||
} | ||
|
||
private CompletableFuture<GraphQLQueryResult> invoke(GraphQLInvocationInput invocationInput, HttpServletRequest request, | ||
HttpServletResponse response) { | ||
if (invocationInput instanceof GraphQLSingleInvocationInput) { | ||
return graphQLInvoker.query(invocationInput); | ||
return graphQLInvoker.queryAsync(invocationInput); | ||
} | ||
return invokeBatched((GraphQLBatchedInvocationInput) invocationInput, request, response); | ||
} | ||
|
||
private GraphQLQueryResult invokeBatched(GraphQLBatchedInvocationInput batchedInvocationInput, | ||
private CompletableFuture<GraphQLQueryResult> invokeBatched(GraphQLBatchedInvocationInput batchedInvocationInput, | ||
HttpServletRequest request, | ||
HttpServletResponse response) { | ||
BatchInputPreProcessor preprocessor = configuration.getBatchInputPreProcessor(); | ||
BatchInputPreProcessResult result = preprocessor | ||
.preProcessBatch(batchedInvocationInput, request, response); | ||
if (result.isExecutable()) { | ||
return graphQLInvoker.query(result.getBatchedInvocationInput()); | ||
return graphQLInvoker.queryAsync(result.getBatchedInvocationInput()); | ||
} | ||
|
||
return GraphQLQueryResult.createError(result.getStatusCode(), result.getStatusMessage()); | ||
return CompletableFuture.completedFuture(GraphQLQueryResult.createError(result.getStatusCode(), result.getStatusMessage())); | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I preserved the original signatures for binary compatibility.
This one is also still used by
AbstractGraphQLHttpServlet.executeQuery()
for JMX.