aboutsummaryrefslogtreecommitdiffstats
path: root/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletResponseController.java
diff options
context:
space:
mode:
Diffstat (limited to 'container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletResponseController.java')
-rw-r--r--container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletResponseController.java251
1 files changed, 251 insertions, 0 deletions
diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletResponseController.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletResponseController.java
new file mode 100644
index 00000000000..60b7878156f
--- /dev/null
+++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletResponseController.java
@@ -0,0 +1,251 @@
+// 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.jdisc.Response;
+import com.yahoo.jdisc.handler.BindingNotFoundException;
+import com.yahoo.jdisc.handler.CompletionHandler;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.ResponseHandler;
+import com.yahoo.jdisc.http.HttpHeaders;
+import com.yahoo.jdisc.http.HttpResponse;
+import com.yahoo.jdisc.service.BindingSetNotFoundException;
+import org.eclipse.jetty.http.MimeTypes;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.nio.ByteBuffer;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import static com.yahoo.jdisc.http.server.jetty.CompletionHandlerUtils.NOOP_COMPLETION_HANDLER;
+
+/**
+ * @author Tony Vaagenes
+ * @author bjorncs
+ */
+public class ServletResponseController {
+
+ private static Logger log = Logger.getLogger(ServletResponseController.class.getName());
+
+ /**
+ * The servlet spec does not require (Http)ServletResponse nor ServletOutputStream to be thread-safe. Therefore,
+ * we must provide our own synchronization, since we may attempt to access these objects simultaneously from
+ * different threads. (The typical cause of this is when one thread is writing a response while another thread
+ * throws an exception, causing the request to fail with an error response).
+ */
+ private final Object monitor = new Object();
+
+ //servletResponse must not be modified after the response has been committed.
+ private final HttpServletRequest servletRequest;
+ private final HttpServletResponse servletResponse;
+ private final boolean developerMode;
+ private final ErrorResponseContentCreator errorResponseContentCreator = new ErrorResponseContentCreator();
+
+ //all calls to the servletOutputStreamWriter must hold the monitor first to ensure visibility of servletResponse changes.
+ private final ServletOutputStreamWriter servletOutputStreamWriter;
+
+ // GuardedBy("monitor")
+ private boolean responseCommitted = false;
+
+ public ServletResponseController(
+ HttpServletRequest servletRequest,
+ HttpServletResponse servletResponse,
+ Executor executor,
+ RequestMetricReporter metricReporter,
+ boolean developerMode) throws IOException {
+
+ this.servletRequest = servletRequest;
+ this.servletResponse = servletResponse;
+ this.developerMode = developerMode;
+ this.servletOutputStreamWriter =
+ new ServletOutputStreamWriter(servletResponse.getOutputStream(), executor, metricReporter);
+ }
+
+
+ private static int getStatusCode(Throwable t) {
+ if (t instanceof BindingNotFoundException) {
+ return HttpServletResponse.SC_NOT_FOUND;
+ } else if (t instanceof BindingSetNotFoundException) {
+ return HttpServletResponse.SC_NOT_FOUND;
+ } else if (t instanceof RequestException) {
+ return ((RequestException)t).getResponseStatus();
+ } else {
+ return HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
+ }
+ }
+
+ private static String getReasonPhrase(Throwable t, boolean developerMode) {
+ if (developerMode) {
+ final StringWriter out = new StringWriter();
+ t.printStackTrace(new PrintWriter(out));
+ return out.toString();
+ } else if (t.getMessage() != null) {
+ return t.getMessage();
+ } else {
+ return t.toString();
+ }
+ }
+
+
+ public void trySendError(Throwable t) {
+ final boolean responseWasCommitted;
+ try {
+ synchronized (monitor) {
+ String reasonPhrase = getReasonPhrase(t, developerMode);
+ int statusCode = getStatusCode(t);
+ responseWasCommitted = responseCommitted;
+ if (!responseCommitted) {
+ responseCommitted = true;
+ sendErrorAsync(statusCode, reasonPhrase);
+ }
+ }
+ } catch (Throwable e) {
+ servletOutputStreamWriter.fail(t);
+ return;
+ }
+
+ //Must be evaluated after state transition for test purposes(See ConformanceTestException)
+ //Done outside the monitor since it causes a callback in tests.
+ if (responseWasCommitted) {
+ RuntimeException exceptionWithStackTrace = new RuntimeException(t);
+ log.log(Level.FINE, "Response already committed, can't change response code", exceptionWithStackTrace);
+ // TODO: should always have failed here, but that breaks test assumptions. Doing soft close instead.
+ //assert !Thread.holdsLock(monitor);
+ //servletOutputStreamWriter.fail(t);
+ servletOutputStreamWriter.close();
+ }
+
+ }
+
+ /**
+ * Async version of {@link org.eclipse.jetty.server.Response#sendError(int, String)}.
+ */
+ private void sendErrorAsync(int statusCode, String reasonPhrase) {
+ servletResponse.setHeader(HttpHeaders.Names.EXPIRES, null);
+ servletResponse.setHeader(HttpHeaders.Names.LAST_MODIFIED, null);
+ servletResponse.setHeader(HttpHeaders.Names.CACHE_CONTROL, null);
+ servletResponse.setHeader(HttpHeaders.Names.CONTENT_TYPE, null);
+ servletResponse.setHeader(HttpHeaders.Names.CONTENT_LENGTH, null);
+ setStatus(servletResponse, statusCode, Optional.of(reasonPhrase));
+
+ // If we are allowed to have a body
+ if (statusCode != HttpServletResponse.SC_NO_CONTENT &&
+ statusCode != HttpServletResponse.SC_NOT_MODIFIED &&
+ statusCode != HttpServletResponse.SC_PARTIAL_CONTENT &&
+ statusCode >= HttpServletResponse.SC_OK) {
+ servletResponse.setHeader(HttpHeaders.Names.CACHE_CONTROL, "must-revalidate,no-cache,no-store");
+ servletResponse.setContentType(MimeTypes.Type.TEXT_HTML_8859_1.toString());
+ byte[] errorContent = errorResponseContentCreator
+ .createErrorContent(servletRequest.getRequestURI(), statusCode, Optional.ofNullable(reasonPhrase));
+ servletResponse.setContentLength(errorContent.length);
+ servletOutputStreamWriter.sendErrorContentAndCloseAsync(ByteBuffer.wrap(errorContent));
+ } else {
+ servletResponse.setContentLength(0);
+ servletOutputStreamWriter.close();
+ }
+ }
+
+ /**
+ * When this future completes there will be no more calls against the servlet output stream or servlet response.
+ * The framework is still allowed to invoke us though.
+ *
+ * The future might complete in the servlet framework thread, user thread or executor thread.
+ */
+ public CompletableFuture<Void> finishedFuture() {
+ return servletOutputStreamWriter.finishedFuture;
+ }
+
+ private void setResponse(Response jdiscResponse) {
+ synchronized (monitor) {
+ servletRequest.setAttribute(HttpResponseStatisticsCollector.requestTypeAttribute, jdiscResponse.getRequestType());
+ if (responseCommitted) {
+ log.log(Level.FINE,
+ jdiscResponse.getError(),
+ () -> "Response already committed, can't change response code. " +
+ "From: " + servletResponse.getStatus() + ", To: " + jdiscResponse.getStatus());
+
+ //TODO: should throw an exception here, but this breaks unit tests.
+ //The failures will now instead happen when writing buffers.
+ servletOutputStreamWriter.close();
+ return;
+ }
+
+ setStatus_holdingLock(jdiscResponse, servletResponse);
+ setHeaders_holdingLock(jdiscResponse, servletResponse);
+ }
+ }
+
+ private static void setHeaders_holdingLock(Response jdiscResponse, HttpServletResponse servletResponse) {
+ for (final Map.Entry<String, String> entry : jdiscResponse.headers().entries()) {
+ servletResponse.addHeader(entry.getKey(), entry.getValue());
+ }
+
+ if (servletResponse.getContentType() == null) {
+ servletResponse.setContentType("text/plain;charset=utf-8");
+ }
+ }
+
+ private static void setStatus_holdingLock(Response jdiscResponse, HttpServletResponse servletResponse) {
+ if (jdiscResponse instanceof HttpResponse) {
+ setStatus(servletResponse, jdiscResponse.getStatus(), Optional.ofNullable(((HttpResponse) jdiscResponse).getMessage()));
+ } else {
+ setStatus(servletResponse, jdiscResponse.getStatus(), getErrorMessage(jdiscResponse));
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ private static void setStatus(HttpServletResponse response, int statusCode, Optional<String> reasonPhrase) {
+ if (reasonPhrase.isPresent()) {
+ // Sets the status line: a status code along with a custom message.
+ // Using a custom status message is deprecated in the Servlet API. No alternative exist.
+ response.setStatus(statusCode, reasonPhrase.get()); // DEPRECATED
+ } else {
+ response.setStatus(statusCode);
+ }
+ }
+
+ private static Optional<String> getErrorMessage(Response jdiscResponse) {
+ return Optional.ofNullable(jdiscResponse.getError()).flatMap(
+ error -> Optional.ofNullable(error.getMessage()));
+ }
+
+
+ private void commitResponse() {
+ synchronized (monitor) {
+ responseCommitted = true;
+ }
+ }
+
+ public final ResponseHandler responseHandler = new ResponseHandler() {
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ setResponse(response);
+ return responseContentChannel;
+ }
+ };
+
+ public final ContentChannel responseContentChannel = new ContentChannel() {
+ @Override
+ public void write(ByteBuffer buf, CompletionHandler handler) {
+ commitResponse();
+ servletOutputStreamWriter.writeBuffer(buf, handlerOrNoopHandler(handler));
+ }
+
+ @Override
+ public void close(CompletionHandler handler) {
+ commitResponse();
+ servletOutputStreamWriter.close(handlerOrNoopHandler(handler));
+ }
+
+ private CompletionHandler handlerOrNoopHandler(CompletionHandler handler) {
+ return handler != null ? handler : NOOP_COMPLETION_HANDLER;
+ }
+ };
+}