summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBjørn Christian Seime <bjorncs@verizonmedia.com>2021-12-13 13:49:56 +0100
committerBjørn Christian Seime <bjorncs@verizonmedia.com>2021-12-13 17:24:19 +0100
commitf72d020192432bc0c5cda68f25168b035e66f5cc (patch)
treef04f190bd12c8ab9b8cda8aafc04f1d73b9f1ce3
parent8cc6b48c160ee2a91714452b78123183b63a19b1 (diff)
Handle write race between handler response and error response
Check if an error response is written before committing response on initial content channel write.
-rw-r--r--container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletResponseController.java173
1 files changed, 105 insertions, 68 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
index ffa31a9e8de..14e6ee4f5e2 100644
--- 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
@@ -32,27 +32,32 @@ import static com.yahoo.jdisc.http.server.jetty.CompletionHandlerUtils.NOOP_COMP
*/
class ServletResponseController {
+ private enum State {
+ WAITING_FOR_RESPONSE,
+ ACCEPTED_RESPONSE_FROM_HANDLER,
+ COMMITTED_RESPONSE_FROM_HANDLER,
+ COMPLETED_WITH_RESPONSE_FROM_HANDLER,
+ COMPLETED_WITH_ERROR_RESPONSE
+ }
+
private static final 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).
+ * Only a single thread must modify {@link HttpServletRequest}/{@link HttpServletResponse} at a time,
+ * and it must only be performed when the response is committed.
+ * The response cannot be modified once response content is being written.
*/
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 out;
// GuardedBy("monitor")
- private boolean responseCommitted = false;
+ private State state = State.WAITING_FOR_RESPONSE;
+ private Response handlerResponse;
ServletResponseController(
HttpServletRequest servletRequest,
@@ -71,7 +76,24 @@ class ServletResponseController {
void trySendErrorResponse(Throwable t) {
synchronized (monitor) {
try {
- sendErrorResponseIfUncommitted(t);
+ switch (state) {
+ case WAITING_FOR_RESPONSE:
+ case ACCEPTED_RESPONSE_FROM_HANDLER:
+ state = State.COMPLETED_WITH_ERROR_RESPONSE;
+ break;
+ case COMMITTED_RESPONSE_FROM_HANDLER:
+ case COMPLETED_WITH_RESPONSE_FROM_HANDLER:
+ if (log.isLoggable(Level.FINE)) {
+ RuntimeException exceptionWithStackTrace = new RuntimeException(t);
+ log.log(Level.FINE, "Response already committed, can't change response code", exceptionWithStackTrace);
+ }
+ return;
+ case COMPLETED_WITH_ERROR_RESPONSE:
+ return;
+ default:
+ throw new IllegalStateException();
+ }
+ writeErrorResponse(t);
} catch (Throwable suppressed) {
t.addSuppressed(suppressed);
} finally {
@@ -93,34 +115,28 @@ class ServletResponseController {
ResponseHandler responseHandler() { return responseHandler; }
- private void sendErrorResponseIfUncommitted(Throwable t) {
- if (!responseCommitted) {
- responseCommitted = true;
- 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);
- String reasonPhrase = getReasonPhrase(t, developerMode);
- int statusCode = getStatusCode(t);
- setStatus(servletResponse, statusCode, 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, reasonPhrase);
- servletResponse.setContentLength(errorContent.length);
- out.writeBuffer(ByteBuffer.wrap(errorContent), NOOP_COMPLETION_HANDLER);
- } else {
- servletResponse.setContentLength(0);
- }
+ private void writeErrorResponse(Throwable t) {
+ 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);
+ String reasonPhrase = getReasonPhrase(t, developerMode);
+ int statusCode = getStatusCode(t);
+ setStatus(servletResponse, statusCode, 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, reasonPhrase);
+ servletResponse.setContentLength(errorContent.length);
+ out.writeBuffer(ByteBuffer.wrap(errorContent), NOOP_COMPLETION_HANDLER);
} else {
- RuntimeException exceptionWithStackTrace = new RuntimeException(t);
- log.log(Level.FINE, "Response already committed, can't change response code", exceptionWithStackTrace);
+ servletResponse.setContentLength(0);
}
}
@@ -151,34 +167,27 @@ class ServletResponseController {
}
}
- private void setResponse(Response jdiscResponse) {
+ private void acceptResponseFromHandler(Response response) {
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.
- out.close();
- return;
- }
-
- if (jdiscResponse instanceof HttpResponse) {
- setStatus(servletResponse, jdiscResponse.getStatus(), ((HttpResponse) jdiscResponse).getMessage());
- } else {
- String message = Optional.ofNullable(jdiscResponse.getError())
- .flatMap(error -> Optional.ofNullable(error.getMessage()))
- .orElse(null);
- setStatus(servletResponse, jdiscResponse.getStatus(), message);
- }
- 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");
+ switch (state) {
+ case WAITING_FOR_RESPONSE:
+ case ACCEPTED_RESPONSE_FROM_HANDLER: // Allow multiple invocations to ResponseHandler.handleResponse()
+ handlerResponse = response;
+ state = State.ACCEPTED_RESPONSE_FROM_HANDLER;
+ servletRequest.setAttribute(
+ HttpResponseStatisticsCollector.requestTypeAttribute, handlerResponse.getRequestType());
+ return;
+ case COMMITTED_RESPONSE_FROM_HANDLER:
+ case COMPLETED_WITH_RESPONSE_FROM_HANDLER:
+ String message = "Response already committed, can't change response code. " +
+ "From: " + servletResponse.getStatus() + ", To: " + response.getStatus();
+ log.log(Level.FINE, message, response.getError());
+ throw new IllegalStateException(message);
+ case COMPLETED_WITH_ERROR_RESPONSE:
+ log.log(Level.FINE, "Error response already written");
+ return; // Silently ignore response from handler when request was failed out
+ default:
+ throw new IllegalStateException();
}
}
}
@@ -195,16 +204,44 @@ class ServletResponseController {
}
- private void ensureCommitted() {
+ private void commitResponseFromHandlerIfUncommitted(boolean close) {
synchronized (monitor) {
- responseCommitted = true;
+ switch (state) {
+ case ACCEPTED_RESPONSE_FROM_HANDLER:
+ state = close ? State.COMPLETED_WITH_RESPONSE_FROM_HANDLER : State.COMMITTED_RESPONSE_FROM_HANDLER;
+ break;
+ case WAITING_FOR_RESPONSE:
+ throw new IllegalStateException("No response provided");
+ case COMMITTED_RESPONSE_FROM_HANDLER:
+ case COMPLETED_WITH_RESPONSE_FROM_HANDLER:
+ return;
+ case COMPLETED_WITH_ERROR_RESPONSE:
+ log.fine("An error response is already committed - failure will be handled by ServletOutputStreamWriter");
+ return;
+ default:
+ throw new IllegalStateException();
+ }
+ if (handlerResponse instanceof HttpResponse) {
+ setStatus(servletResponse, handlerResponse.getStatus(), ((HttpResponse) handlerResponse).getMessage());
+ } else {
+ String message = Optional.ofNullable(handlerResponse.getError())
+ .flatMap(error -> Optional.ofNullable(error.getMessage()))
+ .orElse(null);
+ setStatus(servletResponse, handlerResponse.getStatus(), message);
+ }
+ for (final Map.Entry<String, String> entry : handlerResponse.headers().entries()) {
+ servletResponse.addHeader(entry.getKey(), entry.getValue());
+ }
+ if (servletResponse.getContentType() == null) {
+ servletResponse.setContentType("text/plain;charset=utf-8");
+ }
}
}
private final ResponseHandler responseHandler = new ResponseHandler() {
@Override
public ContentChannel handleResponse(Response response) {
- setResponse(response);
+ acceptResponseFromHandler(response);
return responseContentChannel;
}
};
@@ -212,13 +249,13 @@ class ServletResponseController {
private final ContentChannel responseContentChannel = new ContentChannel() {
@Override
public void write(ByteBuffer buf, CompletionHandler handler) {
- ensureCommitted();
+ commitResponseFromHandlerIfUncommitted(false);
out.writeBuffer(buf, handlerOrNoopHandler(handler));
}
@Override
public void close(CompletionHandler handler) {
- ensureCommitted();
+ commitResponseFromHandlerIfUncommitted(true);
out.close(handlerOrNoopHandler(handler));
}