aboutsummaryrefslogtreecommitdiffstats
path: root/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpRequestDispatch.java
diff options
context:
space:
mode:
Diffstat (limited to 'container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpRequestDispatch.java')
-rw-r--r--container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpRequestDispatch.java243
1 files changed, 243 insertions, 0 deletions
diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpRequestDispatch.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpRequestDispatch.java
new file mode 100644
index 00000000000..05715b13d10
--- /dev/null
+++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpRequestDispatch.java
@@ -0,0 +1,243 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.server.jetty;
+
+import com.yahoo.container.logging.AccessLogEntry;
+import com.yahoo.jdisc.Metric.Context;
+import com.yahoo.jdisc.References;
+import com.yahoo.jdisc.ResourceReference;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.handler.BindingNotFoundException;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.OverloadException;
+import com.yahoo.jdisc.handler.RequestHandler;
+import com.yahoo.jdisc.http.ConnectorConfig;
+import com.yahoo.jdisc.http.HttpHeaders;
+import com.yahoo.jdisc.http.HttpRequest;
+import org.eclipse.jetty.io.EofException;
+import org.eclipse.jetty.server.HttpConnection;
+import org.eclipse.jetty.server.Request;
+
+import javax.servlet.AsyncContext;
+import javax.servlet.ServletInputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import static com.yahoo.jdisc.http.HttpHeaders.Values.APPLICATION_X_WWW_FORM_URLENCODED;
+import static com.yahoo.jdisc.http.server.jetty.HttpServletRequestUtils.getConnection;
+import static com.yahoo.jdisc.http.server.jetty.JDiscHttpServlet.getConnector;
+import static com.yahoo.yolean.Exceptions.throwUnchecked;
+
+/**
+ * @author Simon Thoresen Hult
+ * @author bjorncs
+ */
+class HttpRequestDispatch {
+
+ private static final Logger log = Logger.getLogger(HttpRequestDispatch.class.getName());
+
+ private final static String CHARSET_ANNOTATION = ";charset=";
+
+ private final JDiscContext jDiscContext;
+ private final AsyncContext async;
+ private final Request jettyRequest;
+
+ private final ServletResponseController servletResponseController;
+ private final RequestHandler requestHandler;
+ private final RequestMetricReporter metricReporter;
+
+ public HttpRequestDispatch(JDiscContext jDiscContext,
+ AccessLogEntry accessLogEntry,
+ Context metricContext,
+ HttpServletRequest servletRequest,
+ HttpServletResponse servletResponse) throws IOException {
+ this.jDiscContext = jDiscContext;
+
+ requestHandler = newRequestHandler(jDiscContext, accessLogEntry, servletRequest);
+
+ this.jettyRequest = (Request) servletRequest;
+ this.metricReporter = new RequestMetricReporter(jDiscContext.metric, metricContext, jettyRequest.getTimeStamp());
+ this.servletResponseController = new ServletResponseController(servletRequest,
+ servletResponse,
+ jDiscContext.janitor,
+ metricReporter,
+ jDiscContext.developerMode());
+ markConnectionAsNonPersistentIfThresholdReached(servletRequest);
+ this.async = servletRequest.startAsync();
+ async.setTimeout(0);
+ metricReporter.uriLength(jettyRequest.getOriginalURI().length());
+ }
+
+ public void dispatch() throws IOException {
+ ServletRequestReader servletRequestReader;
+ try {
+ servletRequestReader = handleRequest();
+ } catch (Throwable throwable) {
+ servletResponseController.trySendError(throwable);
+ servletResponseController.finishedFuture().whenComplete((result, exception) ->
+ completeRequestCallback.accept(null, throwable));
+ return;
+ }
+
+ try {
+ onError(servletRequestReader.finishedFuture, servletResponseController::trySendError);
+ onError(servletResponseController.finishedFuture(), servletRequestReader::onError);
+ CompletableFuture.allOf(servletRequestReader.finishedFuture, servletResponseController.finishedFuture())
+ .whenComplete(completeRequestCallback);
+ } catch (Throwable throwable) {
+ log.log(Level.WARNING, "Failed registering finished listeners.", throwable);
+ }
+ }
+
+ private final BiConsumer<Void, Throwable> completeRequestCallback;
+ {
+ AtomicBoolean completeRequestCalled = new AtomicBoolean(false);
+ HttpRequestDispatch parent = this; //used to avoid binding uninitialized variables
+
+ completeRequestCallback = (result, error) -> {
+ boolean alreadyCalled = completeRequestCalled.getAndSet(true);
+ if (alreadyCalled) {
+ AssertionError e = new AssertionError("completeRequest called more than once");
+ log.log(Level.WARNING, "Assertion failed.", e);
+ throw e;
+ }
+
+ boolean reportedError = false;
+
+ if (error != null) {
+ if (isErrorOfType(error, EofException.class, IOException.class)) {
+ log.log(Level.FINE,
+ error,
+ () -> "Network connection was unexpectedly terminated: " + parent.jettyRequest.getRequestURI());
+ parent.metricReporter.prematurelyClosed();
+ } else if (!isErrorOfType(error, OverloadException.class, BindingNotFoundException.class, RequestException.class)) {
+ log.log(Level.WARNING, "Request failed: " + parent.jettyRequest.getRequestURI(), error);
+ }
+ reportedError = true;
+ parent.metricReporter.failedResponse();
+ } else {
+ parent.metricReporter.successfulResponse();
+ }
+
+ try {
+ parent.async.complete();
+ log.finest(() -> "Request completed successfully: " + parent.jettyRequest.getRequestURI());
+ } catch (Throwable throwable) {
+ Level level = reportedError ? Level.FINE: Level.WARNING;
+ log.log(level, "Async.complete failed", throwable);
+ }
+ };
+ }
+
+ private static void markConnectionAsNonPersistentIfThresholdReached(HttpServletRequest request) {
+ ConnectorConfig connectorConfig = getConnector(request).connectorConfig();
+ int maxRequestsPerConnection = connectorConfig.maxRequestsPerConnection();
+ if (maxRequestsPerConnection > 0) {
+ HttpConnection connection = getConnection(request);
+ if (connection.getMessagesIn() >= maxRequestsPerConnection) {
+ connection.getGenerator().setPersistent(false);
+ }
+ }
+ double maxConnectionLifeInSeconds = connectorConfig.maxConnectionLife();
+ if (maxConnectionLifeInSeconds > 0) {
+ HttpConnection connection = getConnection(request);
+ Instant expireAt = Instant.ofEpochMilli((long)(connection.getCreatedTimeStamp() + maxConnectionLifeInSeconds * 1000));
+ if (Instant.now().isAfter(expireAt)) {
+ connection.getGenerator().setPersistent(false);
+ }
+ }
+ }
+
+ @SafeVarargs
+ @SuppressWarnings("varargs")
+ private static boolean isErrorOfType(Throwable throwable, Class<? extends Throwable>... handledTypes) {
+ return Arrays.stream(handledTypes)
+ .anyMatch(
+ exceptionType -> exceptionType.isInstance(throwable)
+ || throwable instanceof CompletionException && exceptionType.isInstance(throwable.getCause()));
+ }
+
+ @SuppressWarnings("try")
+ private ServletRequestReader handleRequest() throws IOException {
+ HttpRequest jdiscRequest = HttpRequestFactory.newJDiscRequest(jDiscContext.container, jettyRequest);
+ ContentChannel requestContentChannel;
+
+ try (ResourceReference ref = References.fromResource(jdiscRequest)) {
+ HttpRequestFactory.copyHeaders(jettyRequest, jdiscRequest);
+ requestContentChannel = requestHandler.handleRequest(jdiscRequest, servletResponseController.responseHandler);
+ }
+
+ ServletInputStream servletInputStream = jettyRequest.getInputStream();
+
+ ServletRequestReader servletRequestReader = new ServletRequestReader(servletInputStream,
+ requestContentChannel,
+ jDiscContext.janitor,
+ metricReporter);
+
+ servletInputStream.setReadListener(servletRequestReader);
+ return servletRequestReader;
+ }
+
+ private static void onError(CompletableFuture<?> future, Consumer<Throwable> errorHandler) {
+ future.whenComplete((result, exception) -> {
+ if (exception != null) {
+ errorHandler.accept(exception);
+ }
+ });
+ }
+
+ ContentChannel handleRequestFilterResponse(Response response) {
+ try {
+ jettyRequest.getInputStream().close();
+ ContentChannel responseContentChannel = servletResponseController.responseHandler.handleResponse(response);
+ servletResponseController.finishedFuture().whenComplete(completeRequestCallback);
+ return responseContentChannel;
+ } catch (IOException e) {
+ throw throwUnchecked(e);
+ }
+ }
+
+
+ private static RequestHandler newRequestHandler(JDiscContext context,
+ AccessLogEntry accessLogEntry,
+ HttpServletRequest servletRequest) {
+ RequestHandler requestHandler = wrapHandlerIfFormPost(
+ new FilteringRequestHandler(context.filterResolver, servletRequest),
+ servletRequest, context.serverConfig.removeRawPostBodyForWwwUrlEncodedPost());
+
+ return new AccessLoggingRequestHandler(requestHandler, accessLogEntry);
+ }
+
+ private static RequestHandler wrapHandlerIfFormPost(RequestHandler requestHandler,
+ HttpServletRequest servletRequest,
+ boolean removeBodyForFormPost) {
+ if (!servletRequest.getMethod().equals("POST")) {
+ return requestHandler;
+ }
+ String contentType = servletRequest.getHeader(HttpHeaders.Names.CONTENT_TYPE);
+ if (contentType == null) {
+ return requestHandler;
+ }
+ if (!contentType.startsWith(APPLICATION_X_WWW_FORM_URLENCODED)) {
+ return requestHandler;
+ }
+ return new FormPostRequestHandler(requestHandler, getCharsetName(contentType), removeBodyForFormPost);
+ }
+
+ private static String getCharsetName(String contentType) {
+ if (!contentType.startsWith(CHARSET_ANNOTATION, APPLICATION_X_WWW_FORM_URLENCODED.length())) {
+ return StandardCharsets.UTF_8.name();
+ }
+ return contentType.substring(APPLICATION_X_WWW_FORM_URLENCODED.length() + CHARSET_ANNOTATION.length());
+ }
+}