Merge remote-tracking branch 'origin/jetty-9.4.x' into jetty-10.0.x

Signed-off-by: Greg Wilkins <gregw@webtide.com>
This commit is contained in:
Greg Wilkins 2019-08-27 08:15:41 +10:00
commit af8587c108
43 changed files with 2843 additions and 1847 deletions

View File

@ -314,6 +314,20 @@ public class HttpStatus
} }
} }
public static boolean hasNoBody(int status)
{
switch (status)
{
case NO_CONTENT_204:
case NOT_MODIFIED_304:
case PARTIAL_CONTENT_206:
return true;
default:
return status < OK_200;
}
}
/** /**
* Simple test against an code to determine if it falls into the * Simple test against an code to determine if it falls into the
* <code>Informational</code> message category as defined in the <a * <code>Informational</code> message category as defined in the <a

View File

@ -0,0 +1,62 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.io;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import org.eclipse.jetty.util.BufferUtil;
/**
* Simple wrapper of a ByteBuffer as an OutputStream.
* The buffer does not grow and this class will throw an
* {@link java.nio.BufferOverflowException} if the buffer capacity is exceeded.
*/
public class ByteBufferOutputStream extends OutputStream
{
final ByteBuffer _buffer;
public ByteBufferOutputStream(ByteBuffer buffer)
{
_buffer = buffer;
}
public void close()
{
}
public void flush()
{
}
public void write(byte[] b)
{
write(b, 0, b.length);
}
public void write(byte[] b, int off, int len)
{
BufferUtil.append(_buffer, b, off, len);
}
public void write(int b)
{
BufferUtil.append(_buffer, (byte)b);
}
}

View File

@ -693,6 +693,14 @@ public abstract class AbstractProxyServlet extends HttpServlet
catch (Exception e) catch (Exception e)
{ {
_log.ignore(e); _log.ignore(e);
try
{
proxyResponse.sendError(-1);
}
catch (Exception e2)
{
_log.ignore(e2);
}
} }
finally finally
{ {

View File

@ -22,6 +22,8 @@ import java.io.IOException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.annotation.Name; import org.eclipse.jetty.util.annotation.Name;
/** /**

View File

@ -44,6 +44,7 @@ import org.junit.jupiter.api.Test;
import static java.nio.charset.StandardCharsets.ISO_8859_1; import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.startsWith; import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
@ -322,7 +323,8 @@ public class SpecExampleConstraintTest
response = _connector.getResponse("POST /ctx/acme/wholesale/index.html HTTP/1.0\r\n" + response = _connector.getResponse("POST /ctx/acme/wholesale/index.html HTTP/1.0\r\n" +
"Authorization: Basic " + encodedChris + "\r\n" + "Authorization: Basic " + encodedChris + "\r\n" +
"\r\n"); "\r\n");
assertThat(response, startsWith("HTTP/1.1 403 ")); assertThat(response, startsWith("HTTP/1.1 403 Forbidden"));
assertThat(response, containsString("!Secure"));
//a user in role HOMEOWNER can do a GET //a user in role HOMEOWNER can do a GET
response = _connector.getResponse("GET /ctx/acme/retail/index.html HTTP/1.0\r\n" + response = _connector.getResponse("GET /ctx/acme/retail/index.html HTTP/1.0\r\n" +

View File

@ -18,6 +18,7 @@
package org.eclipse.jetty.security.authentication; package org.eclipse.jetty.security.authentication;
import java.io.IOException;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpFields;
@ -26,6 +27,7 @@ import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.server.Authentication; import org.eclipse.jetty.server.Authentication;
import org.eclipse.jetty.server.HttpChannel; import org.eclipse.jetty.server.HttpChannel;
import org.eclipse.jetty.server.HttpChannelState;
import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpOutput; import org.eclipse.jetty.server.HttpOutput;
import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Request;
@ -34,6 +36,8 @@ import org.eclipse.jetty.server.Server;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
public class SpnegoAuthenticatorTest public class SpnegoAuthenticatorTest
@ -56,20 +60,27 @@ public class SpnegoAuthenticatorTest
{ {
return null; return null;
} }
};
Request req = new Request(channel, null);
HttpOutput out = new HttpOutput(channel)
{
@Override @Override
public void close() protected HttpOutput newHttpOutput()
{ {
return new HttpOutput(this)
{
@Override
public void close() {}
@Override
public void flush() throws IOException {}
};
} }
}; };
Response res = new Response(channel, out); Request req = channel.getRequest();
Response res = channel.getResponse();
MetaData.Request metadata = new MetaData.Request(new HttpFields()); MetaData.Request metadata = new MetaData.Request(new HttpFields());
metadata.setURI(new HttpURI("http://localhost")); metadata.setURI(new HttpURI("http://localhost"));
req.setMetaData(metadata); req.setMetaData(metadata);
assertThat(channel.getState().handling(), is(HttpChannelState.Action.DISPATCH));
assertEquals(Authentication.SEND_CONTINUE, _authenticator.validateRequest(req, res, true)); assertEquals(Authentication.SEND_CONTINUE, _authenticator.validateRequest(req, res, true));
assertEquals(HttpHeader.NEGOTIATE.asString(), res.getHeader(HttpHeader.WWW_AUTHENTICATE.asString())); assertEquals(HttpHeader.NEGOTIATE.asString(), res.getHeader(HttpHeader.WWW_AUTHENTICATE.asString()));
assertEquals(HttpServletResponse.SC_UNAUTHORIZED, res.getStatus()); assertEquals(HttpServletResponse.SC_UNAUTHORIZED, res.getStatus());
@ -85,16 +96,22 @@ public class SpnegoAuthenticatorTest
{ {
return null; return null;
} }
};
Request req = new Request(channel, null);
HttpOutput out = new HttpOutput(channel)
{
@Override @Override
public void close() protected HttpOutput newHttpOutput()
{ {
return new HttpOutput(this)
{
@Override
public void close() {}
@Override
public void flush() throws IOException {}
};
} }
}; };
Response res = new Response(channel, out); Request req = channel.getRequest();
Response res = channel.getResponse();
HttpFields http_fields = new HttpFields(); HttpFields http_fields = new HttpFields();
// Create a bogus Authorization header. We don't care about the actual credentials. // Create a bogus Authorization header. We don't care about the actual credentials.
http_fields.add(HttpHeader.AUTHORIZATION, "Basic asdf"); http_fields.add(HttpHeader.AUTHORIZATION, "Basic asdf");
@ -102,6 +119,7 @@ public class SpnegoAuthenticatorTest
metadata.setURI(new HttpURI("http://localhost")); metadata.setURI(new HttpURI("http://localhost"));
req.setMetaData(metadata); req.setMetaData(metadata);
assertThat(channel.getState().handling(), is(HttpChannelState.Action.DISPATCH));
assertEquals(Authentication.SEND_CONTINUE, _authenticator.validateRequest(req, res, true)); assertEquals(Authentication.SEND_CONTINUE, _authenticator.validateRequest(req, res, true));
assertEquals(HttpHeader.NEGOTIATE.asString(), res.getHeader(HttpHeader.WWW_AUTHENTICATE.asString())); assertEquals(HttpHeader.NEGOTIATE.asString(), res.getHeader(HttpHeader.WWW_AUTHENTICATE.asString()));
assertEquals(HttpServletResponse.SC_UNAUTHORIZED, res.getStatus()); assertEquals(HttpServletResponse.SC_UNAUTHORIZED, res.getStatus());

View File

@ -160,7 +160,7 @@ public class AsyncContextEvent extends AsyncEvent implements Runnable
Scheduler.Task task = _timeoutTask; Scheduler.Task task = _timeoutTask;
_timeoutTask = null; _timeoutTask = null;
if (task != null) if (task != null)
_state.getHttpChannel().execute(() -> _state.onTimeout()); _state.timeout();
} }
public void addThrowable(Throwable e) public void addThrowable(Throwable e)

View File

@ -33,6 +33,7 @@ import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.BadMessageException; import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.DebugHandler;
import org.eclipse.jetty.util.Attributes; import org.eclipse.jetty.util.Attributes;
import org.eclipse.jetty.util.MultiMap; import org.eclipse.jetty.util.MultiMap;
import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Log;
@ -42,8 +43,6 @@ public class Dispatcher implements RequestDispatcher
{ {
private static final Logger LOG = Log.getLogger(Dispatcher.class); private static final Logger LOG = Log.getLogger(Dispatcher.class);
public static final String __ERROR_DISPATCH = "org.eclipse.jetty.server.Dispatcher.ERROR";
/** /**
* Dispatch include attribute names * Dispatch include attribute names
*/ */
@ -77,15 +76,7 @@ public class Dispatcher implements RequestDispatcher
public void error(ServletRequest request, ServletResponse response) throws ServletException, IOException public void error(ServletRequest request, ServletResponse response) throws ServletException, IOException
{ {
try forward(request, response, DispatcherType.ERROR);
{
request.setAttribute(__ERROR_DISPATCH, Boolean.TRUE);
forward(request, response, DispatcherType.ERROR);
}
finally
{
request.setAttribute(__ERROR_DISPATCH, null);
}
} }
@Override @Override

View File

@ -25,7 +25,6 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.Consumer; import java.util.function.Consumer;
@ -33,12 +32,12 @@ import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
import javax.servlet.DispatcherType; import javax.servlet.DispatcherType;
import javax.servlet.RequestDispatcher; import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import org.eclipse.jetty.http.BadMessageException; import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpGenerator; import org.eclipse.jetty.http.HttpGenerator;
import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.http.HttpVersion;
@ -51,6 +50,7 @@ import org.eclipse.jetty.io.QuietException;
import org.eclipse.jetty.server.HttpChannelState.Action; import org.eclipse.jetty.server.HttpChannelState.Action;
import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.ErrorHandler; import org.eclipse.jetty.server.handler.ErrorHandler;
import org.eclipse.jetty.server.handler.ErrorHandler.ErrorPageMapper;
import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.SharedBlockingCallback.Blocker; import org.eclipse.jetty.util.SharedBlockingCallback.Blocker;
@ -71,8 +71,6 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
{ {
private static final Logger LOG = Log.getLogger(HttpChannel.class); private static final Logger LOG = Log.getLogger(HttpChannel.class);
private final AtomicBoolean _committed = new AtomicBoolean();
private final AtomicBoolean _responseCompleted = new AtomicBoolean();
private final AtomicLong _requests = new AtomicLong(); private final AtomicLong _requests = new AtomicLong();
private final Connector _connector; private final Connector _connector;
private final Executor _executor; private final Executor _executor;
@ -121,6 +119,11 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
_state); _state);
} }
public boolean isSendError()
{
return _state.isSendError();
}
protected HttpInput newHttpInput(HttpChannelState state) protected HttpInput newHttpInput(HttpChannelState state)
{ {
return new HttpInput(state); return new HttpInput(state);
@ -284,8 +287,6 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
public void recycle() public void recycle()
{ {
_committed.set(false);
_responseCompleted.set(false);
_request.recycle(); _request.recycle();
_response.recycle(); _response.recycle();
_committedMetaData = null; _committedMetaData = null;
@ -320,7 +321,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
public boolean handle() public boolean handle()
{ {
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("{} handle {} ", this, _request.getHttpURI()); LOG.debug("handle {} {} ", _request.getHttpURI(), this);
HttpChannelState.Action action = _state.handling(); HttpChannelState.Action action = _state.handling();
@ -334,19 +335,18 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
try try
{ {
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("{} action {}", this, action); LOG.debug("action {} {}", action, this);
switch (action) switch (action)
{ {
case TERMINATED: case TERMINATED:
onCompleted();
break loop;
case WAIT: case WAIT:
// break loop without calling unhandle // break loop without calling unhandle
break loop; break loop;
case NOOP:
// do nothing other than call unhandle
break;
case DISPATCH: case DISPATCH:
{ {
if (!_request.hasMetaData()) if (!_request.hasMetaData())
@ -354,35 +354,17 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
_request.setHandled(false); _request.setHandled(false);
_response.getHttpOutput().reopen(); _response.getHttpOutput().reopen();
try dispatch(DispatcherType.REQUEST, () ->
{ {
_request.setDispatcherType(DispatcherType.REQUEST); for (HttpConfiguration.Customizer customizer : _configuration.getCustomizers())
notifyBeforeDispatch(_request);
List<HttpConfiguration.Customizer> customizers = _configuration.getCustomizers();
if (!customizers.isEmpty())
{ {
for (HttpConfiguration.Customizer customizer : customizers) customizer.customize(getConnector(), _configuration, _request);
{ if (_request.isHandled())
customizer.customize(getConnector(), _configuration, _request); return;
if (_request.isHandled())
break;
}
} }
getServer().handle(HttpChannel.this);
});
if (!_request.isHandled())
getServer().handle(this);
}
catch (Throwable x)
{
notifyDispatchFailure(_request, x);
throw x;
}
finally
{
notifyAfterDispatch(_request);
_request.setDispatcherType(null);
}
break; break;
} }
@ -391,70 +373,70 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
_request.setHandled(false); _request.setHandled(false);
_response.getHttpOutput().reopen(); _response.getHttpOutput().reopen();
try dispatch(DispatcherType.ASYNC,() -> getServer().handleAsync(this));
{
_request.setDispatcherType(DispatcherType.ASYNC);
notifyBeforeDispatch(_request);
getServer().handleAsync(this);
}
catch (Throwable x)
{
notifyDispatchFailure(_request, x);
throw x;
}
finally
{
notifyAfterDispatch(_request);
_request.setDispatcherType(null);
}
break; break;
} }
case ERROR_DISPATCH: case ASYNC_TIMEOUT:
_state.onTimeout();
break;
case SEND_ERROR:
{ {
try try
{ {
_response.reset(true); // Get ready to send an error response
Integer icode = (Integer)_request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
int code = icode != null ? icode : HttpStatus.INTERNAL_SERVER_ERROR_500;
_response.setStatus(code);
_request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, code);
_request.setHandled(false); _request.setHandled(false);
_response.resetContent();
_response.getHttpOutput().reopen(); _response.getHttpOutput().reopen();
try // the following is needed as you cannot trust the response code and reason
// as those could have been modified after calling sendError
Integer code = (Integer)_request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
_response.setStatus(code != null ? code : HttpStatus.INTERNAL_SERVER_ERROR_500);
ContextHandler.Context context = (ContextHandler.Context)_request.getAttribute(ErrorHandler.ERROR_CONTEXT);
ErrorHandler errorHandler = ErrorHandler.getErrorHandler(getServer(), context == null ? null : context.getContextHandler());
// If we can't have a body, then create a minimal error response.
if (HttpStatus.hasNoBody(_response.getStatus()) || errorHandler == null || !errorHandler.errorPageForMethod(_request.getMethod()))
{ {
_request.setDispatcherType(DispatcherType.ERROR); sendResponseAndComplete();
notifyBeforeDispatch(_request); break;
getServer().handle(this);
} }
catch (Throwable x)
// Look for an error page dispatcher
String errorPage = (errorHandler instanceof ErrorPageMapper) ? ((ErrorPageMapper)errorHandler).getErrorPage(_request) : null;
Dispatcher errorDispatcher = errorPage != null ? (Dispatcher)context.getRequestDispatcher(errorPage) : null;
if (errorDispatcher == null)
{ {
notifyDispatchFailure(_request, x); // Allow ErrorHandler to generate response
throw x; errorHandler.handle(null, _request, _request, _response);
_request.setHandled(true);
} }
finally else
{ {
notifyAfterDispatch(_request); // Do the error page dispatch
_request.setDispatcherType(null); dispatch(DispatcherType.ERROR,() -> errorDispatcher.error(_request, _response));
} }
} }
catch (Throwable x) catch (Throwable x)
{ {
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("Could not perform ERROR dispatch, aborting", x); LOG.debug("Could not perform ERROR dispatch, aborting", x);
Throwable failure = (Throwable)_request.getAttribute(RequestDispatcher.ERROR_EXCEPTION); if (_state.isResponseCommitted())
if (failure == null) abort(x);
{
minimalErrorResponse(x);
}
else else
{ {
if (x != failure) _response.resetContent();
failure.addSuppressed(x); sendResponseAndComplete();
minimalErrorResponse(failure);
} }
} }
finally
{
// clean up the context that was set in Response.sendError
_request.removeAttribute(ErrorHandler.ERROR_CONTEXT);
}
break; break;
} }
@ -463,6 +445,12 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
throw _state.getAsyncContextEvent().getThrowable(); throw _state.getAsyncContextEvent().getThrowable();
} }
case READ_REGISTER:
{
onAsyncWaitForContent();
break;
}
case READ_PRODUCE: case READ_PRODUCE:
{ {
_request.getHttpInput().asyncReadProduce(); _request.getHttpInput().asyncReadProduce();
@ -491,41 +479,32 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
case COMPLETE: case COMPLETE:
{ {
try if (!_response.isCommitted() && !_request.isHandled() && !_response.getHttpOutput().isClosed())
{ {
if (!_response.isCommitted() && !_request.isHandled()) _response.sendError(HttpStatus.NOT_FOUND_404);
{ break;
_response.sendError(HttpStatus.NOT_FOUND_404);
}
else
{
// RFC 7230, section 3.3.
int status = _response.getStatus();
boolean hasContent = !(_request.isHead() ||
HttpMethod.CONNECT.is(_request.getMethod()) && status == HttpStatus.OK_200 ||
HttpStatus.isInformational(status) ||
status == HttpStatus.NO_CONTENT_204 ||
status == HttpStatus.NOT_MODIFIED_304);
if (hasContent && !_response.isContentComplete(_response.getHttpOutput().getWritten()))
sendErrorOrAbort("Insufficient content written");
}
checkAndPrepareUpgrade();
_response.closeOutput();
}
finally
{
_request.setHandled(true);
_state.onComplete();
onCompleted();
} }
break loop; // RFC 7230, section 3.3.
if (!_request.isHead() && !_response.isContentComplete(_response.getHttpOutput().getWritten()))
sendErrorOrAbort("Insufficient content written");
// TODO Currently a blocking/aborting consumeAll is done in the handling of the TERMINATED
// TODO Action triggered by the completed callback below. It would be possible to modify the
// TODO callback to do a non-blocking consumeAll at this point and only call completed when
// TODO that is done.
checkAndPrepareUpgrade();
// Set a close callback on the HttpOutput to make it an async callback
_response.closeOutput(Callback.from(_state::completed));
break;
} }
default: default:
{ throw new IllegalStateException(this.toString());
throw new IllegalStateException("state=" + _state);
}
} }
} }
catch (Throwable failure) catch (Throwable failure)
@ -540,7 +519,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
} }
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("{} handle exit, result {}", this, action); LOG.debug("!handle {} {}", action, this);
boolean suspended = action == Action.WAIT; boolean suspended = action == Action.WAIT;
return !suspended; return !suspended;
@ -562,20 +541,23 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
} }
} }
protected void sendError(int code, String reason) private void dispatch(DispatcherType type, Dispatchable dispatchable) throws IOException, ServletException
{ {
try try
{ {
_response.sendError(code, reason); _request.setDispatcherType(type);
notifyBeforeDispatch(_request);
dispatchable.dispatch();
} }
catch (Throwable x) catch (Throwable x)
{ {
if (LOG.isDebugEnabled()) notifyDispatchFailure(_request, x);
LOG.debug("Could not send error " + code + " " + reason, x); throw x;
} }
finally finally
{ {
_state.errorComplete(); notifyAfterDispatch(_request);
_request.setDispatcherType(null);
} }
} }
@ -603,27 +585,19 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
{ {
// No stack trace unless there is debug turned on // No stack trace unless there is debug turned on
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug(_request.getRequestURI(), failure); LOG.warn("handleException " + _request.getRequestURI(), failure);
else else
LOG.warn("{} {}", _request.getRequestURI(), noStack.toString()); LOG.warn("handleException {} {}", _request.getRequestURI(), noStack.toString());
} }
else else
{ {
LOG.warn(_request.getRequestURI(), failure); LOG.warn(_request.getRequestURI(), failure);
} }
try if (isCommitted())
{ abort(failure);
else
_state.onError(failure); _state.onError(failure);
}
catch (Throwable e)
{
if (e != failure)
failure.addSuppressed(e);
LOG.warn("ERROR dispatch failed", failure);
// Try to send a minimal response.
minimalErrorResponse(failure);
}
} }
/** /**
@ -647,32 +621,19 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
return null; return null;
} }
private void minimalErrorResponse(Throwable failure) public void sendResponseAndComplete()
{ {
try try
{ {
int code = 500; _request.setHandled(true);
Integer status = (Integer)_request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE); _state.completing();
if (status != null) sendResponse(null, _response.getHttpOutput().getBuffer(), true, Callback.from(_state::completed));
{ {
code = status.intValue();
} }
else
{
Throwable cause = unwrap(failure, BadMessageException.class);
if (cause instanceof BadMessageException)
code = ((BadMessageException)cause).getCode();
}
_response.reset(true);
_response.setStatus(code);
_response.flushBuffer();
} }
catch (Throwable x) catch (Throwable x)
{ {
if (x != failure) abort(x);
failure.addSuppressed(x);
abort(failure);
} }
} }
@ -690,11 +651,11 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
public String toString() public String toString()
{ {
long timeStamp = _request.getTimeStamp(); long timeStamp = _request.getTimeStamp();
return String.format("%s@%x{r=%s,c=%b,c=%b/%b,a=%s,uri=%s,age=%d}", return String.format("%s@%x{s=%s,r=%s,c=%b/%b,a=%s,uri=%s,age=%d}",
getClass().getSimpleName(), getClass().getSimpleName(),
hashCode(), hashCode(),
_state,
_requests, _requests,
_committed.get(),
isRequestCompleted(), isRequestCompleted(),
isResponseCompleted(), isResponseCompleted(),
_state.getState(), _state.getState(),
@ -731,7 +692,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
public boolean onContent(HttpInput.Content content) public boolean onContent(HttpInput.Content content)
{ {
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("{} onContent {}", this, content); LOG.debug("onContent {} {}", this, content);
notifyRequestContent(_request, content.getByteBuffer()); notifyRequestContent(_request, content.getByteBuffer());
return _request.getHttpInput().addContent(content); return _request.getHttpInput().addContent(content);
} }
@ -739,7 +700,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
public boolean onContentComplete() public boolean onContentComplete()
{ {
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("{} onContentComplete", this); LOG.debug("onContentComplete {}", this);
notifyRequestContentEnd(_request); notifyRequestContentEnd(_request);
return false; return false;
} }
@ -747,7 +708,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
public void onTrailers(HttpFields trailers) public void onTrailers(HttpFields trailers)
{ {
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("{} onTrailers {}", this, trailers); LOG.debug("onTrailers {} {}", this, trailers);
_trailers = trailers; _trailers = trailers;
notifyRequestTrailers(_request); notifyRequestTrailers(_request);
} }
@ -755,7 +716,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
public boolean onRequestComplete() public boolean onRequestComplete()
{ {
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("{} onRequestComplete", this); LOG.debug("onRequestComplete {}", this);
boolean result = _request.getHttpInput().eof(); boolean result = _request.getHttpInput().eof();
notifyRequestEnd(_request); notifyRequestEnd(_request);
return result; return result;
@ -775,7 +736,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
public void onCompleted() public void onCompleted()
{ {
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("COMPLETE for {} written={}", getRequest().getRequestURI(), getBytesWritten()); LOG.debug("onCompleted for {} written={}", getRequest().getRequestURI(), getBytesWritten());
if (_requestLog != null) if (_requestLog != null)
_requestLog.log(_request, _response); _requestLog.log(_request, _response);
@ -798,7 +759,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
{ {
int status = failure.getCode(); int status = failure.getCode();
String message = failure.getReason(); String message = failure.getReason();
if (status < 400 || status > 599) if (status < HttpStatus.BAD_REQUEST_400 || status > 599)
failure = new BadMessageException(HttpStatus.BAD_REQUEST_400, message, failure); failure = new BadMessageException(HttpStatus.BAD_REQUEST_400, message, failure);
notifyRequestFailure(_request, failure); notifyRequestFailure(_request, failure);
@ -850,7 +811,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
protected boolean sendResponse(MetaData.Response response, ByteBuffer content, boolean complete, final Callback callback) protected boolean sendResponse(MetaData.Response response, ByteBuffer content, boolean complete, final Callback callback)
{ {
boolean committing = _committed.compareAndSet(false, true); boolean committing = _state.commitResponse();
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("sendResponse info={} content={} complete={} committing={} callback={}", LOG.debug("sendResponse info={} content={} complete={} committing={} callback={}",
@ -867,9 +828,11 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
response = _response.newResponseMetaData(); response = _response.newResponseMetaData();
commit(response); commit(response);
// Wrap the callback to process 1xx responses. // wrap callback to process 100 responses
Callback committed = HttpStatus.isInformational(response.getStatus()) final int status = response.getStatus();
? new Send100Callback(callback) : new SendCallback(callback, content, true, complete); final Callback committed = (status < HttpStatus.OK_200 && status >= HttpStatus.CONTINUE_100)
? new Send100Callback(callback)
: new SendCallback(callback, content, true, complete);
notifyResponseBegin(_request); notifyResponseBegin(_request);
@ -916,7 +879,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
public boolean isCommitted() public boolean isCommitted()
{ {
return _committed.get(); return _state.isResponseCommitted();
} }
/** /**
@ -932,7 +895,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
*/ */
public boolean isResponseCompleted() public boolean isResponseCompleted()
{ {
return _responseCompleted.get(); return _state.isResponseCompleted();
} }
public boolean isPersistent() public boolean isPersistent()
@ -995,8 +958,11 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
*/ */
public void abort(Throwable failure) public void abort(Throwable failure)
{ {
notifyResponseFailure(_request, failure); if (_state.abortResponse())
_transport.abort(failure); {
notifyResponseFailure(_request, failure);
_transport.abort(failure);
}
} }
public boolean isTunnellingSupported() public boolean isTunnellingSupported()
@ -1130,6 +1096,11 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
} }
} }
interface Dispatchable
{
void dispatch() throws IOException, ServletException;
}
/** /**
* <p>Listener for {@link HttpChannel} events.</p> * <p>Listener for {@link HttpChannel} events.</p>
* <p>HttpChannel will emit events for the various phases it goes through while * <p>HttpChannel will emit events for the various phases it goes through while
@ -1315,16 +1286,15 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
public void succeeded() public void succeeded()
{ {
_written += _length; _written += _length;
if (_complete)
_response.getHttpOutput().closed();
super.succeeded(); super.succeeded();
if (_commit) if (_commit)
notifyResponseCommit(_request); notifyResponseCommit(_request);
if (_length > 0) if (_length > 0)
notifyResponseContent(_request, _content); notifyResponseContent(_request, _content);
if (_complete) if (_complete && _state.completeResponse())
{
_responseCompleted.set(true);
notifyResponseEnd(_request); notifyResponseEnd(_request);
}
} }
@Override @Override
@ -1340,13 +1310,14 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
@Override @Override
public void succeeded() public void succeeded()
{ {
super.failed(x);
_response.getHttpOutput().closed(); _response.getHttpOutput().closed();
super.failed(x);
} }
@Override @Override
public void failed(Throwable th) public void failed(Throwable th)
{ {
_response.getHttpOutput().closed();
abort(x); abort(x);
super.failed(x); super.failed(x);
} }
@ -1370,7 +1341,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
@Override @Override
public void succeeded() public void succeeded()
{ {
if (_committed.compareAndSet(true, false)) if (_state.partialResponse())
super.succeeded(); super.succeeded();
else else
super.failed(new IllegalStateException()); super.failed(new IllegalStateException());

View File

@ -274,18 +274,8 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
} }
else if (filled < 0) else if (filled < 0)
{ {
switch (_channel.getState().getState()) if (_channel.getState().isIdle())
{ getEndPoint().shutdownOutput();
case COMPLETING:
case COMPLETED:
case IDLE:
case THROWN:
case ASYNC_ERROR:
getEndPoint().shutdownOutput();
break;
default:
break;
}
break; break;
} }
} }

View File

@ -274,7 +274,8 @@ public class HttpInput extends ServletInputStream implements Runnable
{ {
BadMessageException bad = new BadMessageException(HttpStatus.REQUEST_TIMEOUT_408, BadMessageException bad = new BadMessageException(HttpStatus.REQUEST_TIMEOUT_408,
String.format("Request content data rate < %d B/s", minRequestDataRate)); String.format("Request content data rate < %d B/s", minRequestDataRate));
_channelState.getHttpChannel().abort(bad); if (_channelState.isResponseCommitted())
_channelState.getHttpChannel().abort(bad);
throw bad; throw bad;
} }
} }

View File

@ -18,6 +18,7 @@
package org.eclipse.jetty.server; package org.eclipse.jetty.server;
import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@ -62,8 +63,31 @@ import org.eclipse.jetty.util.log.Logger;
public class HttpOutput extends ServletOutputStream implements Runnable public class HttpOutput extends ServletOutputStream implements Runnable
{ {
private static final String LSTRING_FILE = "javax.servlet.LocalStrings"; private static final String LSTRING_FILE = "javax.servlet.LocalStrings";
private static final Callback BLOCKING_CLOSE_CALLBACK = new Callback() {};
private static ResourceBundle lStrings = ResourceBundle.getBundle(LSTRING_FILE); private static ResourceBundle lStrings = ResourceBundle.getBundle(LSTRING_FILE);
/*
ACTION OPEN ASYNC READY PENDING UNREADY CLOSING CLOSED
--------------------------------------------------------------------------------------------------
setWriteListener() READY->owp ise ise ise ise ise ise
write() OPEN ise PENDING wpe wpe eof eof
flush() OPEN ise PENDING wpe wpe eof eof
close() CLOSING CLOSING CLOSING CLOSED CLOSED CLOSING CLOSED
isReady() OPEN:true READY:true READY:true UNREADY:false UNREADY:false CLOSED:true CLOSED:true
write completed - - - ASYNC READY->owp CLOSED -
*/
enum State
{
OPEN, // Open in blocking mode
ASYNC, // Open in async mode
READY, // isReady() has returned true
PENDING, // write operating in progress
UNREADY, // write operating in progress, isReady has returned false
ERROR, // An error has occured
CLOSING, // Asynchronous close in progress
CLOSED // Closed
}
/** /**
* The HttpOutput.Interceptor is a single intercept point for all * The HttpOutput.Interceptor is a single intercept point for all
* output written to the HttpOutput: via writer; via output stream; * output written to the HttpOutput: via writer; via output stream;
@ -129,6 +153,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable
private static Logger LOG = Log.getLogger(HttpOutput.class); private static Logger LOG = Log.getLogger(HttpOutput.class);
private static final ThreadLocal<CharsetEncoder> _encoder = new ThreadLocal<>(); private static final ThreadLocal<CharsetEncoder> _encoder = new ThreadLocal<>();
private final AtomicReference<State> _state = new AtomicReference<>(State.OPEN);
private final HttpChannel _channel; private final HttpChannel _channel;
private final SharedBlockingCallback _writeBlocker; private final SharedBlockingCallback _writeBlocker;
private Interceptor _interceptor; private Interceptor _interceptor;
@ -140,23 +165,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable
private int _commitSize; private int _commitSize;
private WriteListener _writeListener; private WriteListener _writeListener;
private volatile Throwable _onError; private volatile Throwable _onError;
private Callback _closeCallback;
/*
ACTION OPEN ASYNC READY PENDING UNREADY CLOSED
-------------------------------------------------------------------------------------------
setWriteListener() READY->owp ise ise ise ise ise
write() OPEN ise PENDING wpe wpe eof
flush() OPEN ise PENDING wpe wpe eof
close() CLOSED CLOSED CLOSED CLOSED CLOSED CLOSED
isReady() OPEN:true READY:true READY:true UNREADY:false UNREADY:false CLOSED:true
write completed - - - ASYNC READY->owp -
*/
private enum OutputState
{
OPEN, ASYNC, READY, PENDING, UNREADY, ERROR, CLOSED
}
private final AtomicReference<OutputState> _state = new AtomicReference<>(OutputState.OPEN);
public HttpOutput(HttpChannel channel) public HttpOutput(HttpChannel channel)
{ {
@ -200,7 +209,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable
public void reopen() public void reopen()
{ {
_state.set(OutputState.OPEN); _state.set(State.OPEN);
} }
private boolean isLastContentToWrite(int len) private boolean isLastContentToWrite(int len)
@ -225,16 +234,67 @@ public class HttpOutput extends ServletOutputStream implements Runnable
_channel.abort(failure); _channel.abort(failure);
} }
@Override public void closedBySendError()
public void close()
{ {
while (true) while (true)
{ {
OutputState state = _state.get(); State state = _state.get();
switch (state) switch (state)
{ {
case OPEN:
case READY:
case ASYNC:
if (!_state.compareAndSet(state, State.CLOSED))
continue;
return;
default:
throw new IllegalStateException(state.toString());
}
}
}
public void close(Closeable wrapper, Callback callback)
{
_closeCallback = callback;
try
{
if (wrapper != null)
wrapper.close();
if (!isClosed())
close();
}
catch (Throwable th)
{
closed();
if (_closeCallback == null)
LOG.ignore(th);
else
callback.failed(th);
}
finally
{
if (_closeCallback != null)
callback.succeeded();
_closeCallback = null;
}
}
@Override
public void close()
{
Callback closeCallback = _closeCallback == null ? BLOCKING_CLOSE_CALLBACK : _closeCallback;
while (true)
{
State state = _state.get();
switch (state)
{
case CLOSING:
case CLOSED: case CLOSED:
{ {
_closeCallback = null;
closeCallback.succeeded();
return; return;
} }
case ASYNC: case ASYNC:
@ -242,11 +302,10 @@ public class HttpOutput extends ServletOutputStream implements Runnable
// A close call implies a write operation, thus in asynchronous mode // A close call implies a write operation, thus in asynchronous mode
// a call to isReady() that returned true should have been made. // a call to isReady() that returned true should have been made.
// However it is desirable to allow a close at any time, specially if // However it is desirable to allow a close at any time, specially if
// complete is called. Thus we simulate a call to isReady here, assuming // complete is called. Thus we simulate a call to isReady here, by
// that we can transition to READY. // trying to move to READY state. Either way we continue.
if (!_state.compareAndSet(state, OutputState.READY)) _state.compareAndSet(state, State.READY);
continue; continue;
break;
} }
case UNREADY: case UNREADY:
case PENDING: case PENDING:
@ -257,34 +316,45 @@ public class HttpOutput extends ServletOutputStream implements Runnable
// complete is called. Because the prior write has not yet completed // complete is called. Because the prior write has not yet completed
// and/or isReady has not been called, this close is allowed, but will // and/or isReady has not been called, this close is allowed, but will
// abort the response. // abort the response.
if (!_state.compareAndSet(state, OutputState.CLOSED)) if (!_state.compareAndSet(state, State.CLOSED))
continue; continue;
IOException ex = new IOException("Closed while Pending/Unready"); IOException ex = new IOException("Closed while Pending/Unready");
LOG.warn(ex.toString()); LOG.warn(ex.toString());
LOG.debug(ex); LOG.debug(ex);
abort(ex); abort(ex);
_closeCallback = null;
closeCallback.failed(ex);
return; return;
} }
default: default:
{ {
if (!_state.compareAndSet(state, OutputState.CLOSED)) if (!_state.compareAndSet(state, State.CLOSING))
continue; continue;
// Do a normal close by writing the aggregate buffer or an empty buffer. If we are // Do a normal close by writing the aggregate buffer or an empty buffer. If we are
// not including, then indicate this is the last write. // not including, then indicate this is the last write.
try try
{ {
write(BufferUtil.hasContent(_aggregate) ? _aggregate : BufferUtil.EMPTY_BUFFER, !_channel.getResponse().isIncluding()); ByteBuffer content = BufferUtil.hasContent(_aggregate) ? _aggregate : BufferUtil.EMPTY_BUFFER;
if (closeCallback == BLOCKING_CLOSE_CALLBACK)
{
// Do a blocking close
write(content, !_channel.getResponse().isIncluding());
_closeCallback = null;
closeCallback.succeeded();
}
else
{
_closeCallback = null;
write(content, !_channel.getResponse().isIncluding(), closeCallback);
}
} }
catch (IOException x) catch (IOException x)
{ {
LOG.ignore(x); // Ignore it, it's been already logged in write(). LOG.ignore(x); // Ignore it, it's been already logged in write().
_closeCallback = null;
closeCallback.failed(x);
} }
finally
{
releaseBuffer();
}
// Return even if an exception is thrown by write().
return; return;
} }
} }
@ -295,11 +365,11 @@ public class HttpOutput extends ServletOutputStream implements Runnable
* Called to indicate that the last write has been performed. * Called to indicate that the last write has been performed.
* It updates the state and performs cleanup operations. * It updates the state and performs cleanup operations.
*/ */
void closed() public void closed()
{ {
while (true) while (true)
{ {
OutputState state = _state.get(); State state = _state.get();
switch (state) switch (state)
{ {
case CLOSED: case CLOSED:
@ -308,15 +378,16 @@ public class HttpOutput extends ServletOutputStream implements Runnable
} }
case UNREADY: case UNREADY:
{ {
if (_state.compareAndSet(state, OutputState.ERROR)) if (_state.compareAndSet(state, State.ERROR))
_writeListener.onError(_onError == null ? new EofException("Async closed") : _onError); _writeListener.onError(_onError == null ? new EofException("Async closed") : _onError);
break; break;
} }
default: default:
{ {
if (!_state.compareAndSet(state, OutputState.CLOSED)) if (!_state.compareAndSet(state, State.CLOSED))
break; break;
// Just make sure write and output stream really are closed
try try
{ {
_channel.getResponse().closeOutput(); _channel.getResponse().closeOutput();
@ -338,6 +409,18 @@ public class HttpOutput extends ServletOutputStream implements Runnable
} }
} }
public ByteBuffer getBuffer()
{
return _aggregate;
}
public ByteBuffer acquireBuffer()
{
if (_aggregate == null)
_aggregate = _channel.getByteBufferPool().acquire(getBufferSize(), _interceptor.isOptimizedForDirectBuffers());
return _aggregate;
}
private void releaseBuffer() private void releaseBuffer()
{ {
if (_aggregate != null) if (_aggregate != null)
@ -349,7 +432,14 @@ public class HttpOutput extends ServletOutputStream implements Runnable
public boolean isClosed() public boolean isClosed()
{ {
return _state.get() == OutputState.CLOSED; switch (_state.get())
{
case CLOSING:
case CLOSED:
return true;
default:
return false;
}
} }
public boolean isAsync() public boolean isAsync()
@ -371,7 +461,8 @@ public class HttpOutput extends ServletOutputStream implements Runnable
{ {
while (true) while (true)
{ {
switch (_state.get()) State state = _state.get();
switch (state)
{ {
case OPEN: case OPEN:
write(BufferUtil.hasContent(_aggregate) ? _aggregate : BufferUtil.EMPTY_BUFFER, false); write(BufferUtil.hasContent(_aggregate) ? _aggregate : BufferUtil.EMPTY_BUFFER, false);
@ -381,25 +472,24 @@ public class HttpOutput extends ServletOutputStream implements Runnable
throw new IllegalStateException("isReady() not called"); throw new IllegalStateException("isReady() not called");
case READY: case READY:
if (!_state.compareAndSet(OutputState.READY, OutputState.PENDING)) if (!_state.compareAndSet(state, State.PENDING))
continue; continue;
new AsyncFlush().iterate(); new AsyncFlush().iterate();
return; return;
case PENDING:
return;
case UNREADY: case UNREADY:
throw new WritePendingException(); throw new WritePendingException();
case ERROR: case ERROR:
throw new EofException(_onError); throw new EofException(_onError);
case PENDING:
case CLOSING:
case CLOSED: case CLOSED:
return; return;
default: default:
throw new IllegalStateException(); throw new IllegalStateException(state.toString());
} }
} }
} }
@ -441,7 +531,8 @@ public class HttpOutput extends ServletOutputStream implements Runnable
// Async or Blocking ? // Async or Blocking ?
while (true) while (true)
{ {
switch (_state.get()) State state = _state.get();
switch (state)
{ {
case OPEN: case OPEN:
// process blocking below // process blocking below
@ -451,15 +542,14 @@ public class HttpOutput extends ServletOutputStream implements Runnable
throw new IllegalStateException("isReady() not called"); throw new IllegalStateException("isReady() not called");
case READY: case READY:
if (!_state.compareAndSet(OutputState.READY, OutputState.PENDING)) if (!_state.compareAndSet(state, State.PENDING))
continue; continue;
// Should we aggregate? // Should we aggregate?
boolean last = isLastContentToWrite(len); boolean last = isLastContentToWrite(len);
if (!last && len <= _commitSize) if (!last && len <= _commitSize)
{ {
if (_aggregate == null) acquireBuffer();
_aggregate = _channel.getByteBufferPool().acquire(getBufferSize(), _interceptor.isOptimizedForDirectBuffers());
// YES - fill the aggregate with content from the buffer // YES - fill the aggregate with content from the buffer
int filled = BufferUtil.fill(_aggregate, b, off, len); int filled = BufferUtil.fill(_aggregate, b, off, len);
@ -467,8 +557,8 @@ public class HttpOutput extends ServletOutputStream implements Runnable
// return if we are not complete, not full and filled all the content // return if we are not complete, not full and filled all the content
if (filled == len && !BufferUtil.isFull(_aggregate)) if (filled == len && !BufferUtil.isFull(_aggregate))
{ {
if (!_state.compareAndSet(OutputState.PENDING, OutputState.ASYNC)) if (!_state.compareAndSet(State.PENDING, State.ASYNC))
throw new IllegalStateException(); throw new IllegalStateException(_state.get().toString());
return; return;
} }
@ -488,11 +578,12 @@ public class HttpOutput extends ServletOutputStream implements Runnable
case ERROR: case ERROR:
throw new EofException(_onError); throw new EofException(_onError);
case CLOSING:
case CLOSED: case CLOSED:
throw new EofException("Closed"); throw new EofException("Closed");
default: default:
throw new IllegalStateException(); throw new IllegalStateException(state.toString());
} }
break; break;
} }
@ -504,8 +595,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable
boolean last = isLastContentToWrite(len); boolean last = isLastContentToWrite(len);
if (!last && len <= _commitSize) if (!last && len <= _commitSize)
{ {
if (_aggregate == null) acquireBuffer();
_aggregate = _channel.getByteBufferPool().acquire(capacity, _interceptor.isOptimizedForDirectBuffers());
// YES - fill the aggregate with content from the buffer // YES - fill the aggregate with content from the buffer
int filled = BufferUtil.fill(_aggregate, b, off, len); int filled = BufferUtil.fill(_aggregate, b, off, len);
@ -554,9 +644,6 @@ public class HttpOutput extends ServletOutputStream implements Runnable
{ {
write(BufferUtil.EMPTY_BUFFER, true); write(BufferUtil.EMPTY_BUFFER, true);
} }
if (last)
closed();
} }
public void write(ByteBuffer buffer) throws IOException public void write(ByteBuffer buffer) throws IOException
@ -566,7 +653,8 @@ public class HttpOutput extends ServletOutputStream implements Runnable
// Async or Blocking ? // Async or Blocking ?
while (true) while (true)
{ {
switch (_state.get()) State state = _state.get();
switch (state)
{ {
case OPEN: case OPEN:
// process blocking below // process blocking below
@ -576,7 +664,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable
throw new IllegalStateException("isReady() not called"); throw new IllegalStateException("isReady() not called");
case READY: case READY:
if (!_state.compareAndSet(OutputState.READY, OutputState.PENDING)) if (!_state.compareAndSet(state, State.PENDING))
continue; continue;
// Do the asynchronous writing from the callback // Do the asynchronous writing from the callback
@ -591,11 +679,12 @@ public class HttpOutput extends ServletOutputStream implements Runnable
case ERROR: case ERROR:
throw new EofException(_onError); throw new EofException(_onError);
case CLOSING:
case CLOSED: case CLOSED:
throw new EofException("Closed"); throw new EofException("Closed");
default: default:
throw new IllegalStateException(); throw new IllegalStateException(state.toString());
} }
break; break;
} }
@ -613,9 +702,6 @@ public class HttpOutput extends ServletOutputStream implements Runnable
write(buffer, last); write(buffer, last);
else if (last) else if (last)
write(BufferUtil.EMPTY_BUFFER, true); write(BufferUtil.EMPTY_BUFFER, true);
if (last)
closed();
} }
@Override @Override
@ -630,34 +716,28 @@ public class HttpOutput extends ServletOutputStream implements Runnable
switch (_state.get()) switch (_state.get())
{ {
case OPEN: case OPEN:
if (_aggregate == null) acquireBuffer();
_aggregate = _channel.getByteBufferPool().acquire(getBufferSize(), _interceptor.isOptimizedForDirectBuffers());
BufferUtil.append(_aggregate, (byte)b); BufferUtil.append(_aggregate, (byte)b);
// Check if all written or full // Check if all written or full
if (complete || BufferUtil.isFull(_aggregate)) if (complete || BufferUtil.isFull(_aggregate))
{
write(_aggregate, complete); write(_aggregate, complete);
if (complete)
closed();
}
break; break;
case ASYNC: case ASYNC:
throw new IllegalStateException("isReady() not called"); throw new IllegalStateException("isReady() not called");
case READY: case READY:
if (!_state.compareAndSet(OutputState.READY, OutputState.PENDING)) if (!_state.compareAndSet(State.READY, State.PENDING))
continue; continue;
if (_aggregate == null) acquireBuffer();
_aggregate = _channel.getByteBufferPool().acquire(getBufferSize(), _interceptor.isOptimizedForDirectBuffers());
BufferUtil.append(_aggregate, (byte)b); BufferUtil.append(_aggregate, (byte)b);
// Check if all written or full // Check if all written or full
if (!complete && !BufferUtil.isFull(_aggregate)) if (!complete && !BufferUtil.isFull(_aggregate))
{ {
if (!_state.compareAndSet(OutputState.PENDING, OutputState.ASYNC)) if (!_state.compareAndSet(State.PENDING, State.ASYNC))
throw new IllegalStateException(); throw new IllegalStateException();
return; return;
} }
@ -673,6 +753,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable
case ERROR: case ERROR:
throw new EofException(_onError); throw new EofException(_onError);
case CLOSING:
case CLOSED: case CLOSED:
throw new EofException("Closed"); throw new EofException("Closed");
@ -810,7 +891,6 @@ public class HttpOutput extends ServletOutputStream implements Runnable
_written += content.remaining(); _written += content.remaining();
write(content, true); write(content, true);
closed();
} }
/** /**
@ -966,7 +1046,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable
switch (_state.get()) switch (_state.get())
{ {
case OPEN: case OPEN:
if (!_state.compareAndSet(OutputState.OPEN, OutputState.PENDING)) if (!_state.compareAndSet(State.OPEN, State.PENDING))
continue; continue;
break; break;
@ -974,6 +1054,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable
callback.failed(new EofException(_onError)); callback.failed(new EofException(_onError));
return; return;
case CLOSING:
case CLOSED: case CLOSED:
callback.failed(new EofException("Closed")); callback.failed(new EofException("Closed"));
return; return;
@ -1073,6 +1154,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable
_onError = null; _onError = null;
_firstByteTimeStamp = -1; _firstByteTimeStamp = -1;
_flushed = 0; _flushed = 0;
_closeCallback = null;
reopen(); reopen();
} }
@ -1082,7 +1164,6 @@ public class HttpOutput extends ServletOutputStream implements Runnable
if (BufferUtil.hasContent(_aggregate)) if (BufferUtil.hasContent(_aggregate))
BufferUtil.clear(_aggregate); BufferUtil.clear(_aggregate);
_written = 0; _written = 0;
reopen();
} }
@Override @Override
@ -1091,7 +1172,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable
if (!_channel.getState().isAsync()) if (!_channel.getState().isAsync())
throw new IllegalStateException("!ASYNC"); throw new IllegalStateException("!ASYNC");
if (_state.compareAndSet(OutputState.OPEN, OutputState.READY)) if (_state.compareAndSet(State.OPEN, State.READY))
{ {
_writeListener = writeListener; _writeListener = writeListener;
if (_channel.getState().onWritePossible()) if (_channel.getState().onWritePossible())
@ -1109,30 +1190,25 @@ public class HttpOutput extends ServletOutputStream implements Runnable
switch (_state.get()) switch (_state.get())
{ {
case OPEN: case OPEN:
case READY:
case ERROR:
case CLOSING:
case CLOSED:
return true; return true;
case ASYNC: case ASYNC:
if (!_state.compareAndSet(OutputState.ASYNC, OutputState.READY)) if (!_state.compareAndSet(State.ASYNC, State.READY))
continue; continue;
return true; return true;
case READY:
return true;
case PENDING: case PENDING:
if (!_state.compareAndSet(OutputState.PENDING, OutputState.UNREADY)) if (!_state.compareAndSet(State.PENDING, State.UNREADY))
continue; continue;
return false; return false;
case UNREADY: case UNREADY:
return false; return false;
case ERROR:
return true;
case CLOSED:
return true;
default: default:
throw new IllegalStateException(); throw new IllegalStateException();
} }
@ -1144,12 +1220,13 @@ public class HttpOutput extends ServletOutputStream implements Runnable
{ {
while (true) while (true)
{ {
OutputState state = _state.get(); State state = _state.get();
if (_onError != null) if (_onError != null)
{ {
switch (state) switch (state)
{ {
case CLOSING:
case CLOSED: case CLOSED:
case ERROR: case ERROR:
{ {
@ -1158,7 +1235,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable
} }
default: default:
{ {
if (_state.compareAndSet(state, OutputState.ERROR)) if (_state.compareAndSet(state, State.ERROR))
{ {
Throwable th = _onError; Throwable th = _onError;
_onError = null; _onError = null;
@ -1234,16 +1311,16 @@ public class HttpOutput extends ServletOutputStream implements Runnable
{ {
while (true) while (true)
{ {
OutputState last = _state.get(); State last = _state.get();
switch (last) switch (last)
{ {
case PENDING: case PENDING:
if (!_state.compareAndSet(OutputState.PENDING, OutputState.ASYNC)) if (!_state.compareAndSet(State.PENDING, State.ASYNC))
continue; continue;
break; break;
case UNREADY: case UNREADY:
if (!_state.compareAndSet(OutputState.UNREADY, OutputState.READY)) if (!_state.compareAndSet(State.UNREADY, State.READY))
continue; continue;
if (_last) if (_last)
closed(); closed();

View File

@ -212,6 +212,7 @@ public class Request implements HttpServletRequest
private String _contentType; private String _contentType;
private String _characterEncoding; private String _characterEncoding;
private ContextHandler.Context _context; private ContextHandler.Context _context;
private ContextHandler.Context _errorContext;
private Cookies _cookies; private Cookies _cookies;
private DispatcherType _dispatcherType; private DispatcherType _dispatcherType;
private int _inputState = INPUT_NONE; private int _inputState = INPUT_NONE;
@ -759,6 +760,22 @@ public class Request implements HttpServletRequest
return _context; return _context;
} }
/**
* @return The current {@link Context context} used for this error handling for this request. If the request is asynchronous,
* then it is the context that called async. Otherwise it is the last non-null context passed to #setContext
*/
public Context getErrorContext()
{
if (isAsyncStarted())
{
ContextHandler handler = _channel.getState().getContextHandler();
if (handler != null)
return handler.getServletContext();
}
return _errorContext;
}
/* /*
* @see javax.servlet.http.HttpServletRequest#getContextPath() * @see javax.servlet.http.HttpServletRequest#getContextPath()
*/ */
@ -1983,6 +2000,7 @@ public class Request implements HttpServletRequest
else else
{ {
_context = context; _context = context;
_errorContext = context;
} }
} }

View File

@ -18,19 +18,19 @@
package org.eclipse.jetty.server; package org.eclipse.jetty.server;
import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.nio.channels.IllegalSelectorException; import java.nio.channels.IllegalSelectorException;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.List; import java.util.Iterator;
import java.util.ListIterator; import java.util.ListIterator;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier; import java.util.function.Supplier;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletOutputStream; import javax.servlet.ServletOutputStream;
import javax.servlet.ServletResponse; import javax.servlet.ServletResponse;
import javax.servlet.ServletResponseWrapper; import javax.servlet.ServletResponseWrapper;
@ -58,9 +58,8 @@ import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.PreEncodedHttpField; import org.eclipse.jetty.http.PreEncodedHttpField;
import org.eclipse.jetty.io.RuntimeIOException; import org.eclipse.jetty.io.RuntimeIOException;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.ErrorHandler;
import org.eclipse.jetty.server.session.SessionHandler; import org.eclipse.jetty.server.session.SessionHandler;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Log;
@ -379,66 +378,35 @@ public class Response implements HttpServletResponse
sendError(sc, null); sendError(sc, null);
} }
/**
* Send an error response.
* <p>In addition to the servlet standard handling, this method supports some additional codes:</p>
* <dl>
* <dt>102</dt><dd>Send a partial PROCESSING response and allow additional responses</dd>
* <dt>-1</dt><dd>Abort the HttpChannel and close the connection/stream</dd>
* </dl>
* @param code The error code
* @param message The message
* @throws IOException If an IO problem occurred sending the error response.
*/
@Override @Override
public void sendError(int code, String message) throws IOException public void sendError(int code, String message) throws IOException
{ {
if (isIncluding()) if (isIncluding())
return; return;
if (isCommitted())
{
if (LOG.isDebugEnabled())
LOG.debug("Aborting on sendError on committed response {} {}", code, message);
code = -1;
}
else
resetBuffer();
switch (code) switch (code)
{ {
case -1: case -1:
_channel.abort(new IOException()); _channel.abort(new IOException(message));
return; break;
case 102: case HttpStatus.PROCESSING_102:
sendProcessing(); sendProcessing();
return; break;
default: default:
_channel.getState().sendError(code, message);
break; break;
} }
_outputType = OutputType.NONE;
setContentType(null);
setCharacterEncoding(null);
setHeader(HttpHeader.EXPIRES, null);
setHeader(HttpHeader.LAST_MODIFIED, null);
setHeader(HttpHeader.CACHE_CONTROL, null);
setHeader(HttpHeader.CONTENT_TYPE, null);
setHeader(HttpHeader.CONTENT_LENGTH, null);
setStatus(code);
Request request = _channel.getRequest();
Throwable cause = (Throwable)request.getAttribute(Dispatcher.ERROR_EXCEPTION);
_reason = HttpStatus.getMessage(code);
if (message == null)
message = cause == null ? _reason : cause.toString();
// If we are allowed to have a body, then produce the error page.
if (code != SC_NO_CONTENT && code != SC_NOT_MODIFIED &&
code != SC_PARTIAL_CONTENT && code >= SC_OK)
{
ContextHandler.Context context = request.getContext();
ContextHandler contextHandler = context == null ? _channel.getState().getContextHandler() : context.getContextHandler();
request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, code);
request.setAttribute(RequestDispatcher.ERROR_MESSAGE, message);
request.setAttribute(RequestDispatcher.ERROR_REQUEST_URI, request.getRequestURI());
request.setAttribute(RequestDispatcher.ERROR_SERVLET_NAME, request.getServletName());
ErrorHandler errorHandler = ErrorHandler.getErrorHandler(_channel.getServer(), contextHandler);
if (errorHandler != null)
errorHandler.handle(null, request, request, this);
}
if (!request.isAsyncStarted())
closeOutput();
} }
/** /**
@ -650,8 +618,11 @@ public class Response implements HttpServletResponse
throw new IllegalArgumentException(); throw new IllegalArgumentException();
if (!isIncluding()) if (!isIncluding())
{ {
// Null the reason only if the status is different. This allows
// a specific reason to be sent with setStatusWithReason followed by sendError.
if (_status != sc)
_reason = null;
_status = sc; _status = sc;
_reason = null;
} }
} }
@ -714,6 +685,11 @@ public class Response implements HttpServletResponse
return _outputType == OutputType.STREAM; return _outputType == OutputType.STREAM;
} }
public boolean isWritingOrStreaming()
{
return isWriting() || isStreaming();
}
@Override @Override
public PrintWriter getWriter() throws IOException public PrintWriter getWriter() throws IOException
{ {
@ -822,21 +798,15 @@ public class Response implements HttpServletResponse
public void closeOutput() throws IOException public void closeOutput() throws IOException
{ {
switch (_outputType) if (_outputType == OutputType.WRITER)
{ _writer.close();
case WRITER: if (!_out.isClosed())
_writer.close(); _out.close();
if (!_out.isClosed()) }
_out.close();
break; public void closeOutput(Callback callback)
case STREAM: {
if (!_out.isClosed()) _out.close((_outputType == OutputType.WRITER) ? _writer : _out, callback);
getOutputStream().close();
break;
default:
if (!_out.isClosed())
_out.close();
}
} }
public long getLongContentLength() public long getLongContentLength()
@ -1029,19 +999,20 @@ public class Response implements HttpServletResponse
@Override @Override
public void reset() public void reset()
{ {
reset(false);
}
public void reset(boolean preserveCookies)
{
resetForForward();
_status = 200; _status = 200;
_reason = null; _reason = null;
_out.resetBuffer();
_outputType = OutputType.NONE;
_contentLength = -1; _contentLength = -1;
_contentType = null;
_mimeType = null;
_characterEncoding = null;
_encodingFrom = EncodingFrom.NOT_SET;
List<HttpField> cookies = preserveCookies ? _fields.getFields(HttpHeader.SET_COOKIE) : null; // Clear all response headers
_fields.clear(); _fields.clear();
// recreate necessary connection related fields
for (String value : _channel.getRequest().getHttpFields().getCSV(HttpHeader.CONNECTION, false)) for (String value : _channel.getRequest().getHttpFields().getCSV(HttpHeader.CONNECTION, false))
{ {
HttpHeaderValue cb = HttpHeaderValue.CACHE.get(value); HttpHeaderValue cb = HttpHeaderValue.CACHE.get(value);
@ -1064,21 +1035,57 @@ public class Response implements HttpServletResponse
} }
} }
if (preserveCookies) // recreate session cookies
cookies.forEach(_fields::add); Request request = getHttpChannel().getRequest();
else HttpSession session = request.getSession(false);
if (session != null && session.isNew())
{ {
Request request = getHttpChannel().getRequest(); SessionHandler sh = request.getSessionHandler();
HttpSession session = request.getSession(false); if (sh != null)
if (session != null && session.isNew())
{ {
SessionHandler sh = request.getSessionHandler(); HttpCookie c = sh.getSessionCookie(session, request.getContextPath(), request.isSecure());
if (sh != null) if (c != null)
{ addCookie(c);
HttpCookie c = sh.getSessionCookie(session, request.getContextPath(), request.isSecure()); }
if (c != null) }
addCookie(c); }
}
public void resetContent()
{
_out.resetBuffer();
_outputType = OutputType.NONE;
_contentLength = -1;
_contentType = null;
_mimeType = null;
_characterEncoding = null;
_encodingFrom = EncodingFrom.NOT_SET;
// remove the content related response headers and keep all others
for (Iterator<HttpField> i = getHttpFields().iterator(); i.hasNext(); )
{
HttpField field = i.next();
if (field.getHeader() == null)
continue;
switch (field.getHeader())
{
case CONTENT_TYPE:
case CONTENT_LENGTH:
case CONTENT_ENCODING:
case CONTENT_LANGUAGE:
case CONTENT_RANGE:
case CONTENT_MD5:
case CONTENT_LOCATION:
case TRANSFER_ENCODING:
case CACHE_CONTROL:
case LAST_MODIFIED:
case EXPIRES:
case ETAG:
case DATE:
case VARY:
i.remove();
continue;
default:
} }
} }
} }
@ -1093,6 +1100,7 @@ public class Response implements HttpServletResponse
public void resetBuffer() public void resetBuffer()
{ {
_out.resetBuffer(); _out.resetBuffer();
_out.reopen();
} }
@Override @Override
@ -1143,6 +1151,9 @@ public class Response implements HttpServletResponse
@Override @Override
public boolean isCommitted() public boolean isCommitted()
{ {
// If we are in sendError state, we pretend to be committed
if (_channel.isSendError())
return true;
return _channel.isCommitted(); return _channel.isCommitted();
} }

View File

@ -514,10 +514,16 @@ public class Server extends HandlerWrapper implements Attributes
if (HttpMethod.OPTIONS.is(request.getMethod()) || "*".equals(target)) if (HttpMethod.OPTIONS.is(request.getMethod()) || "*".equals(target))
{ {
if (!HttpMethod.OPTIONS.is(request.getMethod())) if (!HttpMethod.OPTIONS.is(request.getMethod()))
{
request.setHandled(true);
response.sendError(HttpStatus.BAD_REQUEST_400); response.sendError(HttpStatus.BAD_REQUEST_400);
handleOptions(request, response); }
if (!request.isHandled()) else
handle(target, request, request, response); {
handleOptions(request, response);
if (!request.isHandled())
handle(target, request, request, response);
}
} }
else else
handle(target, request, request, response); handle(target, request, request, response);

View File

@ -864,7 +864,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
* insert additional handling (Eg configuration) before the call to super.doStart by this method will start contained handlers. * insert additional handling (Eg configuration) before the call to super.doStart by this method will start contained handlers.
* *
* @throws Exception if unable to start the context * @throws Exception if unable to start the context
* @see org.eclipse.jetty.server.handler.ContextHandler.Context * @see ContextHandler.Context
*/ */
protected void startContext() throws Exception protected void startContext() throws Exception
{ {
@ -1103,7 +1103,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
case UNAVAILABLE: case UNAVAILABLE:
baseRequest.setHandled(true); baseRequest.setHandled(true);
response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE); response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
return true; return false;
default: default:
if ((DispatcherType.REQUEST.equals(dispatch) && baseRequest.isHandled())) if ((DispatcherType.REQUEST.equals(dispatch) && baseRequest.isHandled()))
return false; return false;
@ -1138,8 +1138,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
if (oldContext != _scontext) if (oldContext != _scontext)
{ {
// check the target. // check the target.
if (DispatcherType.REQUEST.equals(dispatch) || DispatcherType.ASYNC.equals(dispatch) || if (DispatcherType.REQUEST.equals(dispatch) || DispatcherType.ASYNC.equals(dispatch))
DispatcherType.ERROR.equals(dispatch) && baseRequest.getHttpChannelState().isAsync())
{ {
if (_compactPath) if (_compactPath)
target = URIUtil.compactPath(target); target = URIUtil.compactPath(target);
@ -1276,29 +1275,11 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
if (new_context) if (new_context)
requestInitialized(baseRequest, request); requestInitialized(baseRequest, request);
switch (dispatch) if (dispatch == DispatcherType.REQUEST && isProtectedTarget(target))
{ {
case REQUEST: baseRequest.setHandled(true);
if (isProtectedTarget(target)) response.sendError(HttpServletResponse.SC_NOT_FOUND);
{ return;
response.sendError(HttpServletResponse.SC_NOT_FOUND);
baseRequest.setHandled(true);
return;
}
break;
case ERROR:
// If this is already a dispatch to an error page, proceed normally
if (Boolean.TRUE.equals(baseRequest.getAttribute(Dispatcher.__ERROR_DISPATCH)))
break;
// We can just call doError here. If there is no error page, then one will
// be generated. If there is an error page, then a RequestDispatcher will be
// used to route the request through appropriate filters etc.
doError(target, baseRequest, request, response);
return;
default:
break;
} }
nextHandle(target, baseRequest, request, response); nextHandle(target, baseRequest, request, response);

View File

@ -19,28 +19,29 @@
package org.eclipse.jetty.server.handler; package org.eclipse.jetty.server.handler;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer; import java.io.Writer;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.List; import java.util.List;
import javax.servlet.RequestDispatcher; import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.QuotedQualityCSV; import org.eclipse.jetty.http.QuotedQualityCSV;
import org.eclipse.jetty.io.ByteBufferOutputStream;
import org.eclipse.jetty.server.Dispatcher; import org.eclipse.jetty.server.Dispatcher;
import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.QuotedStringTokenizer;
import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.log.Logger;
@ -54,10 +55,13 @@ import org.eclipse.jetty.util.log.Logger;
*/ */
public class ErrorHandler extends AbstractHandler public class ErrorHandler extends AbstractHandler
{ {
// TODO This classes API needs to be majorly refactored/cleanup in jetty-10
private static final Logger LOG = Log.getLogger(ErrorHandler.class); private static final Logger LOG = Log.getLogger(ErrorHandler.class);
public static final String ERROR_PAGE = "org.eclipse.jetty.server.error_page"; public static final String ERROR_PAGE = "org.eclipse.jetty.server.error_page";
public static final String ERROR_CONTEXT = "org.eclipse.jetty.server.error_context";
boolean _showStacks = true; boolean _showStacks = true;
boolean _disableStacks = false;
boolean _showMessageInTitle = true; boolean _showMessageInTitle = true;
String _cacheControl = "must-revalidate,no-cache,no-store"; String _cacheControl = "must-revalidate,no-cache,no-store";
@ -65,6 +69,19 @@ public class ErrorHandler extends AbstractHandler
{ {
} }
public boolean errorPageForMethod(String method)
{
switch (method)
{
case "GET":
case "POST":
case "HEAD":
return true;
default:
return false;
}
}
/* /*
* @see org.eclipse.jetty.server.server.Handler#handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, int) * @see org.eclipse.jetty.server.server.Handler#handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, int)
*/ */
@ -77,71 +94,13 @@ public class ErrorHandler extends AbstractHandler
@Override @Override
public void doError(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException public void doError(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
{ {
String method = request.getMethod(); String cacheControl = getCacheControl();
if (!HttpMethod.GET.is(method) && !HttpMethod.POST.is(method) && !HttpMethod.HEAD.is(method)) if (cacheControl != null)
{ response.setHeader(HttpHeader.CACHE_CONTROL.asString(), cacheControl);
baseRequest.setHandled(true);
return;
}
if (this instanceof ErrorPageMapper) String message = (String)request.getAttribute(Dispatcher.ERROR_MESSAGE);
{
String errorPage = ((ErrorPageMapper)this).getErrorPage(request);
if (errorPage != null)
{
String oldErrorPage = (String)request.getAttribute(ERROR_PAGE);
ContextHandler.Context context = baseRequest.getContext();
if (context == null)
context = ContextHandler.getCurrentContext();
if (context == null)
{
LOG.warn("No ServletContext for error page {}", errorPage);
}
else if (oldErrorPage != null && oldErrorPage.equals(errorPage))
{
LOG.warn("Error page loop {}", errorPage);
}
else
{
request.setAttribute(ERROR_PAGE, errorPage);
Dispatcher dispatcher = (Dispatcher)context.getRequestDispatcher(errorPage);
try
{
if (LOG.isDebugEnabled())
LOG.debug("error page dispatch {}->{}", errorPage, dispatcher);
if (dispatcher != null)
{
dispatcher.error(request, response);
return;
}
LOG.warn("No error page found " + errorPage);
}
catch (ServletException e)
{
LOG.warn(Log.EXCEPTION, e);
return;
}
}
}
else
{
if (LOG.isDebugEnabled())
{
LOG.debug("No Error Page mapping for request({} {}) (using default)", request.getMethod(), request.getRequestURI());
}
}
}
if (_cacheControl != null)
response.setHeader(HttpHeader.CACHE_CONTROL.asString(), _cacheControl);
String message = (String)request.getAttribute(RequestDispatcher.ERROR_MESSAGE);
if (message == null) if (message == null)
message = baseRequest.getResponse().getReason(); message = baseRequest.getResponse().getReason();
if (message == null)
message = HttpStatus.getMessage(response.getStatus());
generateAcceptableResponse(baseRequest, request, response, response.getStatus(), message); generateAcceptableResponse(baseRequest, request, response, response.getStatus(), message);
} }
@ -151,7 +110,7 @@ public class ErrorHandler extends AbstractHandler
* acceptable to the user-agent. The Accept header is evaluated in * acceptable to the user-agent. The Accept header is evaluated in
* quality order and the method * quality order and the method
* {@link #generateAcceptableResponse(Request, HttpServletRequest, HttpServletResponse, int, String, String)} * {@link #generateAcceptableResponse(Request, HttpServletRequest, HttpServletResponse, int, String, String)}
* is called for each mimetype until {@link Request#isHandled()} is true.</p> * is called for each mimetype until the response is written to or committed.</p>
* *
* @param baseRequest The base request * @param baseRequest The base request
* @param request The servlet request (may be wrapped) * @param request The servlet request (may be wrapped)
@ -174,48 +133,10 @@ public class ErrorHandler extends AbstractHandler
for (String mimeType : acceptable) for (String mimeType : acceptable)
{ {
generateAcceptableResponse(baseRequest, request, response, code, message, mimeType); generateAcceptableResponse(baseRequest, request, response, code, message, mimeType);
if (response.isCommitted() || baseRequest.getResponse().isWriting() || baseRequest.getResponse().isStreaming()) if (response.isCommitted() || baseRequest.getResponse().isWritingOrStreaming())
break; break;
} }
} }
baseRequest.getResponse().closeOutput();
}
/**
* Generate an acceptable error response for a mime type.
* <p>This method is called for each mime type in the users agent's
* <code>Accept</code> header, until {@link Request#isHandled()} is true and a
* response of the appropriate type is generated.
*
* @param baseRequest The base request
* @param request The servlet request (may be wrapped)
* @param response The response (may be wrapped)
* @param code the http error code
* @param message the http error message
* @param mimeType The mimetype to generate (may be *&#47;*or other wildcard)
* @throws IOException if a response cannot be generated
*/
protected void generateAcceptableResponse(Request baseRequest, HttpServletRequest request, HttpServletResponse response, int code, String message, String mimeType)
throws IOException
{
switch (mimeType)
{
case "text/html":
case "text/*":
case "*/*":
{
baseRequest.setHandled(true);
Writer writer = getAcceptableWriter(baseRequest, request, response);
if (writer != null)
{
response.setContentType(MimeTypes.Type.TEXT_HTML.asString());
handleErrorPage(request, writer, code, message);
}
break;
}
default:
break;
}
} }
/** /**
@ -236,6 +157,7 @@ public class ErrorHandler extends AbstractHandler
* @return A {@link Writer} if there is a known acceptable charset or null * @return A {@link Writer} if there is a known acceptable charset or null
* @throws IOException if a Writer cannot be returned * @throws IOException if a Writer cannot be returned
*/ */
@Deprecated
protected Writer getAcceptableWriter(Request baseRequest, HttpServletRequest request, HttpServletResponse response) protected Writer getAcceptableWriter(Request baseRequest, HttpServletRequest request, HttpServletResponse response)
throws IOException throws IOException
{ {
@ -264,6 +186,139 @@ public class ErrorHandler extends AbstractHandler
return null; return null;
} }
/**
* Generate an acceptable error response for a mime type.
* <p>This method is called for each mime type in the users agent's
* <code>Accept</code> header, until {@link Request#isHandled()} is true and a
* response of the appropriate type is generated.
* </p>
* <p>The default implementation handles "text/html", "text/*" and "*&#47;*".
* The method can be overridden to handle other types. Implementations must
* immediate produce a response and may not be async.
* </p>
*
* @param baseRequest The base request
* @param request The servlet request (may be wrapped)
* @param response The response (may be wrapped)
* @param code the http error code
* @param message the http error message
* @param contentType The mimetype to generate (may be *&#47;*or other wildcard)
* @throws IOException if a response cannot be generated
*/
protected void generateAcceptableResponse(Request baseRequest, HttpServletRequest request, HttpServletResponse response, int code, String message, String contentType)
throws IOException
{
// We can generate an acceptable contentType, but can we generate an acceptable charset?
// TODO refactor this in jetty-10 to be done in the other calling loop
Charset charset = null;
List<String> acceptable = baseRequest.getHttpFields().getQualityCSV(HttpHeader.ACCEPT_CHARSET);
if (!acceptable.isEmpty())
{
for (String name : acceptable)
{
if ("*".equals(name))
{
charset = StandardCharsets.UTF_8;
break;
}
try
{
charset = Charset.forName(name);
}
catch (Exception e)
{
LOG.ignore(e);
}
}
if (charset == null)
return;
}
MimeTypes.Type type;
switch (contentType)
{
case "text/html":
case "text/*":
case "*/*":
type = MimeTypes.Type.TEXT_HTML;
if (charset == null)
charset = StandardCharsets.ISO_8859_1;
break;
case "text/json":
case "application/json":
type = MimeTypes.Type.TEXT_JSON;
if (charset == null)
charset = StandardCharsets.UTF_8;
break;
case "text/plain":
type = MimeTypes.Type.TEXT_PLAIN;
if (charset == null)
charset = StandardCharsets.ISO_8859_1;
break;
default:
return;
}
// write into the response aggregate buffer and flush it asynchronously.
while (true)
{
try
{
// TODO currently the writer used here is of fixed size, so a large
// TODO error page may cause a BufferOverflow. In which case we try
// TODO again with stacks disabled. If it still overflows, it is
// TODO written without a body.
ByteBuffer buffer = baseRequest.getResponse().getHttpOutput().acquireBuffer();
ByteBufferOutputStream out = new ByteBufferOutputStream(buffer);
PrintWriter writer = new PrintWriter(new OutputStreamWriter(out, charset));
switch (type)
{
case TEXT_HTML:
response.setContentType(MimeTypes.Type.TEXT_HTML.asString());
response.setCharacterEncoding(charset.name());
handleErrorPage(request, writer, code, message);
break;
case TEXT_JSON:
response.setContentType(contentType);
writeErrorJson(request, writer, code, message);
break;
case TEXT_PLAIN:
response.setContentType(MimeTypes.Type.TEXT_PLAIN.asString());
response.setCharacterEncoding(charset.name());
writeErrorPlain(request, writer, code, message);
break;
default:
throw new IllegalStateException();
}
writer.flush();
break;
}
catch (BufferOverflowException e)
{
LOG.warn("Error page too large: {} {} {}", code, message, request);
if (LOG.isDebugEnabled())
LOG.warn(e);
baseRequest.getResponse().resetContent();
if (!_disableStacks)
{
LOG.info("Disabling showsStacks for " + this);
_disableStacks = true;
continue;
}
break;
}
}
// Do an asynchronous completion.
baseRequest.getHttpChannel().sendResponseAndComplete();
}
protected void handleErrorPage(HttpServletRequest request, Writer writer, int code, String message) protected void handleErrorPage(HttpServletRequest request, Writer writer, int code, String message)
throws IOException throws IOException
{ {
@ -288,12 +343,13 @@ public class ErrorHandler extends AbstractHandler
{ {
writer.write("<meta http-equiv=\"Content-Type\" content=\"text/html;charset=utf-8\"/>\n"); writer.write("<meta http-equiv=\"Content-Type\" content=\"text/html;charset=utf-8\"/>\n");
writer.write("<title>Error "); writer.write("<title>Error ");
writer.write(Integer.toString(code)); // TODO this code is duplicated in writeErrorPageMessage
String status = Integer.toString(code);
if (_showMessageInTitle) writer.write(status);
if (message != null && !message.equals(status))
{ {
writer.write(' '); writer.write(' ');
write(writer, message); writer.write(StringUtil.sanitizeXmlString(message));
} }
writer.write("</title>\n"); writer.write("</title>\n");
} }
@ -304,7 +360,7 @@ public class ErrorHandler extends AbstractHandler
String uri = request.getRequestURI(); String uri = request.getRequestURI();
writeErrorPageMessage(request, writer, code, message, uri); writeErrorPageMessage(request, writer, code, message, uri);
if (showStacks) if (showStacks && !_disableStacks)
writeErrorPageStacks(request, writer); writeErrorPageStacks(request, writer);
Request.getBaseRequest(request).getHttpChannel().getHttpConfiguration() Request.getBaseRequest(request).getHttpChannel().getHttpConfiguration()
@ -315,29 +371,97 @@ public class ErrorHandler extends AbstractHandler
throws IOException throws IOException
{ {
writer.write("<h2>HTTP ERROR "); writer.write("<h2>HTTP ERROR ");
String status = Integer.toString(code);
writer.write(status);
if (message != null && !message.equals(status))
{
writer.write(' ');
writer.write(StringUtil.sanitizeXmlString(message));
}
writer.write("</h2>\n");
writer.write("<table>\n");
htmlRow(writer, "URI", uri);
htmlRow(writer, "STATUS", status);
htmlRow(writer, "MESSAGE", message);
htmlRow(writer, "SERVLET", request.getAttribute(Dispatcher.ERROR_SERVLET_NAME));
Throwable cause = (Throwable)request.getAttribute(Dispatcher.ERROR_EXCEPTION);
while (cause != null)
{
htmlRow(writer, "CAUSED BY", cause);
cause = cause.getCause();
}
writer.write("</table>\n");
}
private void htmlRow(Writer writer, String tag, Object value)
throws IOException
{
writer.write("<tr><th>");
writer.write(tag);
writer.write(":</th><td>");
if (value == null)
writer.write("-");
else
writer.write(StringUtil.sanitizeXmlString(value.toString()));
writer.write("</td></tr>\n");
}
private void writeErrorPlain(HttpServletRequest request, PrintWriter writer, int code, String message)
{
writer.write("HTTP ERROR ");
writer.write(Integer.toString(code)); writer.write(Integer.toString(code));
writer.write("</h2>\n<p>Problem accessing "); writer.write(' ');
write(writer, uri); writer.write(StringUtil.sanitizeXmlString(message));
writer.write(". Reason:\n<pre> "); writer.write("\n");
write(writer, message); writer.printf("URI: %s%n", request.getRequestURI());
writer.write("</pre></p>"); writer.printf("STATUS: %s%n", code);
writer.printf("MESSAGE: %s%n", message);
writer.printf("SERVLET: %s%n", request.getAttribute(Dispatcher.ERROR_SERVLET_NAME));
Throwable cause = (Throwable)request.getAttribute(Dispatcher.ERROR_EXCEPTION);
while (cause != null)
{
writer.printf("CAUSED BY %s%n", cause);
if (_showStacks && !_disableStacks)
cause.printStackTrace(writer);
cause = cause.getCause();
}
}
private void writeErrorJson(HttpServletRequest request, PrintWriter writer, int code, String message)
{
writer
.append("{\n")
.append(" url: \"").append(request.getRequestURI()).append("\",\n")
.append(" status: \"").append(Integer.toString(code)).append("\",\n")
.append(" message: ").append(QuotedStringTokenizer.quote(message)).append(",\n");
Object servlet = request.getAttribute(Dispatcher.ERROR_SERVLET_NAME);
if (servlet != null)
writer.append("servlet: \"").append(servlet.toString()).append("\",\n");
Throwable cause = (Throwable)request.getAttribute(Dispatcher.ERROR_EXCEPTION);
int c = 0;
while (cause != null)
{
writer.append(" cause").append(Integer.toString(c++)).append(": ")
.append(QuotedStringTokenizer.quote(cause.toString())).append(",\n");
cause = cause.getCause();
}
writer.append("}");
} }
protected void writeErrorPageStacks(HttpServletRequest request, Writer writer) protected void writeErrorPageStacks(HttpServletRequest request, Writer writer)
throws IOException throws IOException
{ {
Throwable th = (Throwable)request.getAttribute(RequestDispatcher.ERROR_EXCEPTION); Throwable th = (Throwable)request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
while (th != null) if (_showStacks && th != null)
{ {
writer.write("<h3>Caused by:</h3><pre>"); PrintWriter pw = writer instanceof PrintWriter ? (PrintWriter)writer : new PrintWriter(writer);
StringWriter sw = new StringWriter(); pw.write("<pre>");
PrintWriter pw = new PrintWriter(sw); while (th != null)
th.printStackTrace(pw); {
pw.flush(); th.printStackTrace(pw);
write(writer, sw.getBuffer().toString()); th = th.getCause();
}
writer.write("</pre>\n"); writer.write("</pre>\n");
th = th.getCause();
} }
} }

View File

@ -190,8 +190,9 @@ public class ShutdownHandler extends HandlerWrapper
connector.shutdown(); connector.shutdown();
} }
response.sendError(200, "Connectors closed, commencing full shutdown");
baseRequest.setHandled(true); baseRequest.setHandled(true);
response.setStatus(200);
response.flushBuffer();
final Server server = getServer(); final Server server = getServer();
new Thread() new Thread()

View File

@ -85,10 +85,10 @@ public abstract class AbstractHttpTest
HttpTester.parseResponse(input, response); HttpTester.parseResponse(input, response);
if (httpVersion.is("HTTP/1.1") && if (httpVersion.is("HTTP/1.1") &&
response.isComplete() && response.isComplete() &&
response.get("content-length") == null && response.get("content-length") == null &&
response.get("transfer-encoding") == null && response.get("transfer-encoding") == null &&
!__noBodyCodes.contains(response.getStatus())) !__noBodyCodes.contains(response.getStatus()))
assertThat("If HTTP/1.1 response doesn't contain transfer-encoding or content-length headers, " + assertThat("If HTTP/1.1 response doesn't contain transfer-encoding or content-length headers, " +
"it should contain connection:close", response.get("connection"), is("close")); "it should contain connection:close", response.get("connection"), is("close"));
return response; return response;

View File

@ -0,0 +1,221 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.server;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Exchanger;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream;
import org.eclipse.jetty.http.HttpCompliance;
import org.eclipse.jetty.http.tools.HttpTester;
import org.eclipse.jetty.io.ChannelEndPoint;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.io.ManagedSelector;
import org.eclipse.jetty.io.SocketChannelEndPoint;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.thread.Scheduler;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
/**
* Extended Server Tester.
*/
public class AsyncCompletionTest extends HttpServerTestFixture
{
private static final Exchanger<DelayedCallback> X = new Exchanger<>();
private static final AtomicBoolean COMPLETE = new AtomicBoolean();
private static class DelayedCallback extends Callback.Nested
{
private CompletableFuture<Void> _delay = new CompletableFuture<>();
public DelayedCallback(Callback callback)
{
super(callback);
}
@Override
public void succeeded()
{
_delay.complete(null);
}
@Override
public void failed(Throwable x)
{
_delay.completeExceptionally(x);
}
public void proceed()
{
try
{
_delay.get(10, TimeUnit.SECONDS);
getCallback().succeeded();
}
catch(Throwable th)
{
th.printStackTrace();
getCallback().failed(th);
}
}
}
@BeforeEach
public void init() throws Exception
{
COMPLETE.set(false);
startServer(new ServerConnector(_server, new HttpConnectionFactory()
{
@Override
public Connection newConnection(Connector connector, EndPoint endPoint)
{
return configure(new ExtendedHttpConnection(getHttpConfiguration(), connector, endPoint), connector, endPoint);
}
})
{
@Override
protected ChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) throws IOException
{
return new ExtendedEndPoint(channel, selectSet, key, getScheduler());
}
});
}
private static class ExtendedEndPoint extends SocketChannelEndPoint
{
public ExtendedEndPoint(SocketChannel channel, ManagedSelector selector, SelectionKey key, Scheduler scheduler)
{
super(channel, selector, key, scheduler);
}
@Override
public void write(Callback callback, ByteBuffer... buffers) throws IllegalStateException
{
DelayedCallback delay = new DelayedCallback(callback);
super.write(delay, buffers);
try
{
X.exchange(delay);
}
catch (InterruptedException e)
{
throw new RuntimeException(e);
}
}
}
private static class ExtendedHttpConnection extends HttpConnection
{
public ExtendedHttpConnection(HttpConfiguration config, Connector connector, EndPoint endPoint)
{
super(config, connector, endPoint, false);
}
@Override
public void onCompleted()
{
COMPLETE.compareAndSet(false,true);
super.onCompleted();
}
}
// Tests from here use these parameters
public static Stream<Arguments> tests()
{
List<Object[]> tests = new ArrayList<>();
tests.add(new Object[]{new HelloWorldHandler(), 200, "Hello world"});
tests.add(new Object[]{new SendErrorHandler(499,"Test async sendError"), 499, "Test async sendError"});
return tests.stream().map(Arguments::of);
}
@ParameterizedTest
@MethodSource("tests")
public void testAsyncCompletion(Handler handler, int status, String message) throws Exception
{
configureServer(handler);
int base = _threadPool.getBusyThreads();
try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
{
OutputStream os = client.getOutputStream();
// write the request
os.write("GET / HTTP/1.0\r\n\r\n".getBytes(StandardCharsets.ISO_8859_1));
os.flush();
// The write should happen but the callback is delayed
HttpTester.Response response = HttpTester.parseResponse(client.getInputStream());
assertThat(response, Matchers.notNullValue());
assertThat(response.getStatus(), is(status));
String content = response.getContent();
assertThat(content, containsString(message));
// Check that a thread is held busy in write
assertThat(_threadPool.getBusyThreads(), Matchers.greaterThan(base));
// Getting the Delayed callback will free the thread
DelayedCallback delay = X.exchange(null, 10, TimeUnit.SECONDS);
// wait for threads to return to base level
long end = System.nanoTime() + TimeUnit.SECONDS.toNanos(10);
while(_threadPool.getBusyThreads() != base)
{
if (System.nanoTime() > end)
throw new TimeoutException();
Thread.sleep(10);
}
// We are now asynchronously waiting!
assertThat(COMPLETE.get(), is(false));
// proceed with the completion
delay.proceed();
while(!COMPLETE.get())
{
if (System.nanoTime() > end)
throw new TimeoutException();
Thread.sleep(10);
}
}
}
}

View File

@ -30,7 +30,6 @@ import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.tools.HttpTester; import org.eclipse.jetty.http.tools.HttpTester;
import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.server.handler.ErrorHandler;
import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -53,43 +52,6 @@ public class ErrorHandlerTest
server = new Server(); server = new Server();
connector = new LocalConnector(server); connector = new LocalConnector(server);
server.addConnector(connector); server.addConnector(connector);
server.addBean(new ErrorHandler()
{
@Override
protected void generateAcceptableResponse(
Request baseRequest,
HttpServletRequest request,
HttpServletResponse response,
int code,
String message,
String mimeType) throws IOException
{
switch (mimeType)
{
case "text/json":
case "application/json":
{
baseRequest.setHandled(true);
response.setContentType(mimeType);
response.getWriter()
.append("{")
.append("code: \"").append(Integer.toString(code)).append("\",")
.append("message: \"").append(message).append('"')
.append("}");
break;
}
case "text/plain":
{
baseRequest.setHandled(true);
response.setContentType("text/plain");
response.getOutputStream().print(response.getContentType());
break;
}
default:
super.generateAcceptableResponse(baseRequest, request, response, code, message, mimeType);
}
}
});
server.setHandler(new AbstractHandler() server.setHandler(new AbstractHandler()
{ {

View File

@ -190,6 +190,10 @@ public class HttpInputAsyncStateTest
__history.add("COMPLETE"); __history.add("COMPLETE");
break; break;
case READ_REGISTER:
_state.getHttpChannel().onAsyncWaitForContent();
break;
default: default:
fail("Bad Action: " + action); fail("Bad Action: " + action);
} }

View File

@ -1,133 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.server;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Stream;
import javax.servlet.AsyncContext;
import javax.servlet.DispatcherType;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.tools.HttpTester;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
//TODO: reset buffer tests
//TODO: add protocol specific tests for connection: close and/or chunking
public class HttpManyWaysToAsyncCommitBadBehaviourTest extends AbstractHttpTest
{
private final String contextAttribute = getClass().getName() + ".asyncContext";
public static Stream<Arguments> httpVersions()
{
// boolean dispatch - if true we dispatch, otherwise we complete
final boolean DISPATCH = true;
final boolean COMPLETE = false;
List<Arguments> ret = new ArrayList<>();
ret.add(Arguments.of(HttpVersion.HTTP_1_0, DISPATCH));
ret.add(Arguments.of(HttpVersion.HTTP_1_0, COMPLETE));
ret.add(Arguments.of(HttpVersion.HTTP_1_1, DISPATCH));
ret.add(Arguments.of(HttpVersion.HTTP_1_1, COMPLETE));
return ret.stream();
}
@ParameterizedTest
@MethodSource("httpVersions")
public void testHandlerSetsHandledAndWritesSomeContent(HttpVersion httpVersion, boolean dispatch) throws Exception
{
server.setHandler(new SetHandledWriteSomeDataHandler(false, dispatch));
server.start();
HttpTester.Response response = executeRequest(httpVersion);
assertThat("response code is 500", response.getStatus(), is(500));
}
private class SetHandledWriteSomeDataHandler extends ThrowExceptionOnDemandHandler
{
private final boolean dispatch;
private SetHandledWriteSomeDataHandler(boolean throwException, boolean dispatch)
{
super(throwException);
this.dispatch = dispatch;
}
@Override
public void doNonErrorHandle(String target, Request baseRequest, final HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
final CyclicBarrier resumeBarrier = new CyclicBarrier(1);
if (baseRequest.getDispatcherType() == DispatcherType.ERROR)
{
response.sendError(500);
return;
}
if (request.getAttribute(contextAttribute) == null)
{
final AsyncContext asyncContext = baseRequest.startAsync();
new Thread(new Runnable()
{
@Override
public void run()
{
try
{
asyncContext.getResponse().getWriter().write("foobar");
if (dispatch)
asyncContext.dispatch();
else
asyncContext.complete();
resumeBarrier.await(5, TimeUnit.SECONDS);
}
catch (IOException | TimeoutException | InterruptedException | BrokenBarrierException e)
{
e.printStackTrace();
}
}
}).run();
}
try
{
resumeBarrier.await(5, TimeUnit.SECONDS);
}
catch (InterruptedException | BrokenBarrierException | TimeoutException e)
{
e.printStackTrace();
}
throw new TestCommitException();
}
}
}

View File

@ -65,13 +65,22 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
public abstract class HttpServerTestBase extends HttpServerTestFixture public abstract class HttpServerTestBase extends HttpServerTestFixture
{ {
private static final String REQUEST1_HEADER = "POST / HTTP/1.0\n" + "Host: localhost\n" + "Content-Type: text/xml; charset=utf-8\n" + "Connection: close\n" + "Content-Length: "; private static final String REQUEST1_HEADER = "POST / HTTP/1.0\n" +
"Host: localhost\n" +
"Content-Type: text/xml; charset=utf-8\n" +
"Connection: close\n" +
"Content-Length: ";
private static final String REQUEST1_CONTENT = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + private static final String REQUEST1_CONTENT = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<nimbus xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" + " xsi:noNamespaceSchemaLocation=\"nimbus.xsd\" version=\"1.0\">\n" + "<nimbus xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" +
"</nimbus>"; " xsi:noNamespaceSchemaLocation=\"nimbus.xsd\" version=\"1.0\">\n" +
"</nimbus>";
private static final String REQUEST1 = REQUEST1_HEADER + REQUEST1_CONTENT.getBytes().length + "\n\n" + REQUEST1_CONTENT; private static final String REQUEST1 = REQUEST1_HEADER + REQUEST1_CONTENT.getBytes().length + "\n\n" + REQUEST1_CONTENT;
private static final String RESPONSE1 = "HTTP/1.1 200 OK\n" + "Content-Length: 13\n" + "Server: Jetty(" + Server.getVersion() + ")\n" + "\n" + "Hello world\n"; private static final String RESPONSE1 = "HTTP/1.1 200 OK\n" +
"Content-Length: 13\n" +
"Server: Jetty(" + Server.getVersion() + ")\n" +
"\n" +
"Hello world\n";
// Break the request up into three pieces, splitting the header. // Break the request up into three pieces, splitting the header.
private static final String FRAGMENT1 = REQUEST1.substring(0, 16); private static final String FRAGMENT1 = REQUEST1.substring(0, 16);
@ -104,7 +113,7 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
" <jobId>73</jobId>\n" + " <jobId>73</jobId>\n" +
" </getJobDetails>\n" + " </getJobDetails>\n" +
" </request>\n" + " </request>\n" +
"</nimbus>\n"; "</nimbus>\n";
protected static final String RESPONSE2 = protected static final String RESPONSE2 =
"HTTP/1.1 200 OK\n" + "HTTP/1.1 200 OK\n" +
"Content-Type: text/xml;charset=iso-8859-1\n" + "Content-Type: text/xml;charset=iso-8859-1\n" +
@ -143,9 +152,9 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
OutputStream os = client.getOutputStream(); OutputStream os = client.getOutputStream();
os.write(("OPTIONS * HTTP/1.1\r\n" + os.write(("OPTIONS * HTTP/1.1\r\n" +
"Host: " + _serverURI.getHost() + "\r\n" + "Host: " + _serverURI.getHost() + "\r\n" +
"Connection: close\r\n" + "Connection: close\r\n" +
"\r\n").getBytes(StandardCharsets.ISO_8859_1)); "\r\n").getBytes(StandardCharsets.ISO_8859_1));
os.flush(); os.flush();
// Read the response. // Read the response.
@ -154,15 +163,20 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
assertThat(response, Matchers.containsString("HTTP/1.1 200 OK")); assertThat(response, Matchers.containsString("HTTP/1.1 200 OK"));
assertThat(response, Matchers.containsString("Allow: GET")); assertThat(response, Matchers.containsString("Allow: GET"));
} }
}
@Test
public void testGETStar() throws Exception
{
configureServer(new OptionsHandler());
try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort())) try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
{ {
OutputStream os = client.getOutputStream(); OutputStream os = client.getOutputStream();
os.write(("GET * HTTP/1.1\r\n" + os.write(("GET * HTTP/1.1\r\n" +
"Host: " + _serverURI.getHost() + "\r\n" + "Host: " + _serverURI.getHost() + "\r\n" +
"Connection: close\r\n" + "Connection: close\r\n" +
"\r\n").getBytes(StandardCharsets.ISO_8859_1)); "\r\n").getBytes(StandardCharsets.ISO_8859_1));
os.flush(); os.flush();
// Read the response. // Read the response.
@ -434,13 +448,13 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort())) try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
{ {
OutputStream os = client.getOutputStream(); OutputStream os = client.getOutputStream();
os.write(("GET /R2 HTTP/1.1\r\n" + os.write(("GET /R2 HTTP/1.1\r\n" +
"Host: localhost\r\n" + "Host: localhost\r\n" +
"Transfer-Encoding: chunked\r\n" + "Transfer-Encoding: chunked\r\n" +
"Content-Type: text/plain\r\n" + "Content-Type: text/plain\r\n" +
"Connection: close\r\n" + "Connection: close\r\n" +
"\r\n").getBytes()); "\r\n").getBytes());
os.flush(); os.flush();
Thread.sleep(1000); Thread.sleep(1000);
os.write(("5").getBytes()); os.write(("5").getBytes());
@ -450,7 +464,6 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
Thread.sleep(1000); Thread.sleep(1000);
os.write(("ABCDE\r\n" + os.write(("ABCDE\r\n" +
"0;\r\n\r\n").getBytes()); "0;\r\n\r\n").getBytes());
os.flush(); os.flush();
// Read the response. // Read the response.
@ -467,6 +480,7 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort())) try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
{ {
OutputStream os = client.getOutputStream(); OutputStream os = client.getOutputStream();
//@checkstyle-disable-check : IllegalTokenText
os.write(("GET /R2 HTTP/1.1\r\n" + os.write(("GET /R2 HTTP/1.1\r\n" +
"Host: localhost\r\n" + "Host: localhost\r\n" +
"Content-Length: 5\r\n" + "Content-Length: 5\r\n" +
@ -475,6 +489,7 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
"\r\n" + "\r\n" +
"ABCDE\r\n" + "ABCDE\r\n" +
"\r\n" "\r\n"
//@checkstyle-enable-check : IllegalTokenText
).getBytes()); ).getBytes());
os.flush(); os.flush();
@ -893,7 +908,7 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
if (line.length() == 0) if (line.length() == 0)
break; break;
int len = line.length(); int len = line.length();
assertEquals(Integer.parseInt(chunk, 16), len); assertEquals(Integer.valueOf(chunk, 16).intValue(), len);
if (max < len) if (max < len)
max = len; max = len;
} }
@ -1547,12 +1562,11 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
{ {
try try
{ {
byte[] bytes = ( byte[] bytes = ("GET / HTTP/1.1\r\n" +
"GET / HTTP/1.1\r\n" + "Host: localhost\r\n" +
"Host: localhost\r\n" + "Content-Length: " + cl + "\r\n" +
"Content-Length: " + cl + "\r\n" + "\r\n" +
"\r\n" + content).getBytes(StandardCharsets.ISO_8859_1);
content).getBytes(StandardCharsets.ISO_8859_1);
for (int i = 0; i < REQS; i++) for (int i = 0; i < REQS; i++)
{ {

View File

@ -40,7 +40,8 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
public class HttpServerTestFixture public class HttpServerTestFixture
{ // Useful constants {
// Useful constants
protected static final long PAUSE = 10L; protected static final long PAUSE = 10L;
protected static final int LOOPS = 50; protected static final int LOOPS = 50;
@ -185,6 +186,31 @@ public class HttpServerTestFixture
} }
} }
protected static class SendErrorHandler extends AbstractHandler
{
private final int code;
private final String message;
public SendErrorHandler()
{
this(500, null);
}
public SendErrorHandler(int code, String message)
{
this.code = code;
this.message = message;
}
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
baseRequest.setHandled(true);
response.sendError(code, message);
}
}
protected static class ReadExactHandler extends AbstractHandler.ErrorDispatchHandler protected static class ReadExactHandler extends AbstractHandler.ErrorDispatchHandler
{ {
private int expected; private int expected;

View File

@ -32,6 +32,8 @@ import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.server.handler.HandlerWrapper; import org.eclipse.jetty.server.handler.HandlerWrapper;
import org.eclipse.jetty.server.session.SessionHandler; import org.eclipse.jetty.server.session.SessionHandler;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.hamcrest.Matchers; import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@ -42,6 +44,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
public class LocalAsyncContextTest public class LocalAsyncContextTest
{ {
public static final Logger LOG = Log.getLogger(LocalAsyncContextTest.class);
protected Server _server; protected Server _server;
protected SuspendHandler _handler; protected SuspendHandler _handler;
protected Connector _connector; protected Connector _connector;
@ -232,6 +235,7 @@ public class LocalAsyncContextTest
private synchronized String process(String content) throws Exception private synchronized String process(String content) throws Exception
{ {
LOG.debug("TEST process: {}", content);
reset(); reset();
String request = "GET / HTTP/1.1\r\n" + String request = "GET / HTTP/1.1\r\n" +
"Host: localhost\r\n" + "Host: localhost\r\n" +
@ -305,6 +309,7 @@ public class LocalAsyncContextTest
@Override @Override
public void handle(String target, final Request baseRequest, final HttpServletRequest request, final HttpServletResponse response) throws IOException, ServletException public void handle(String target, final Request baseRequest, final HttpServletRequest request, final HttpServletResponse response) throws IOException, ServletException
{ {
LOG.debug("handle {} {}", baseRequest.getDispatcherType(), baseRequest);
if (DispatcherType.REQUEST.equals(baseRequest.getDispatcherType())) if (DispatcherType.REQUEST.equals(baseRequest.getDispatcherType()))
{ {
if (_read > 0) if (_read > 0)

View File

@ -35,6 +35,8 @@ import java.util.Enumeration;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.stream.Stream;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletOutputStream; import javax.servlet.ServletOutputStream;
import javax.servlet.http.Cookie; import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@ -70,6 +72,8 @@ import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
@ -662,70 +666,41 @@ public class ResponseTest
assertEquals("foo/bar; other=pq charset=utf-8 other=xyz;charset=utf-16", response.getContentType()); assertEquals("foo/bar; other=pq charset=utf-8 other=xyz;charset=utf-16", response.getContentType());
} }
@Test public static Stream<Object[]> sendErrorTestCodes()
public void testStatusCodes() throws Exception
{ {
Response response = getResponse(); List<Object[]> data = new ArrayList<>();
data.add(new Object[]{404, null, "Not Found"});
response.sendError(404); data.add(new Object[]{500, "Database Error", "Database Error"});
assertEquals(404, response.getStatus()); data.add(new Object[]{406, "Super Nanny", "Super Nanny"});
assertEquals("Not Found", response.getReason()); return data.stream();
response = getResponse();
response.sendError(500, "Database Error");
assertEquals(500, response.getStatus());
assertEquals("Server Error", response.getReason());
assertThat(BufferUtil.toString(_content), containsString("Database Error"));
assertEquals("must-revalidate,no-cache,no-store", response.getHeader(HttpHeader.CACHE_CONTROL.asString()));
response = getResponse();
response.setStatus(200);
assertEquals(200, response.getStatus());
assertNull(response.getReason());
response = getResponse();
response.sendError(406, "Super Nanny");
assertEquals(406, response.getStatus());
assertEquals(HttpStatus.Code.NOT_ACCEPTABLE.getMessage(), response.getReason());
assertThat(BufferUtil.toString(_content), containsString("Super Nanny"));
assertEquals("must-revalidate,no-cache,no-store", response.getHeader(HttpHeader.CACHE_CONTROL.asString()));
} }
@Test @ParameterizedTest
public void testStatusCodesNoErrorHandler() throws Exception @MethodSource(value = "sendErrorTestCodes")
public void testStatusCodes(int code, String message, String expectedMessage) throws Exception
{
Response response = getResponse();
assertThat(response.getHttpChannel().getState().handling(), is(HttpChannelState.Action.DISPATCH));
if (message == null)
response.sendError(code);
else
response.sendError(code, message);
assertTrue(response.getHttpOutput().isClosed());
assertEquals(code, response.getStatus());
assertEquals(null, response.getReason());
assertEquals(expectedMessage, response.getHttpChannel().getRequest().getAttribute(RequestDispatcher.ERROR_MESSAGE));
assertThat(response.getHttpChannel().getState().unhandle(), is(HttpChannelState.Action.SEND_ERROR));
assertThat(response.getHttpChannel().getState().unhandle(), is(HttpChannelState.Action.COMPLETE));
}
@ParameterizedTest
@MethodSource(value = "sendErrorTestCodes")
public void testStatusCodesNoErrorHandler(int code, String message, String expectedMessage) throws Exception
{ {
_server.removeBean(_server.getBean(ErrorHandler.class)); _server.removeBean(_server.getBean(ErrorHandler.class));
Response response = getResponse(); testStatusCodes(code, message, expectedMessage);
response.sendError(404);
assertEquals(404, response.getStatus());
assertEquals("Not Found", response.getReason());
response = getResponse();
response.sendError(500, "Database Error");
assertEquals(500, response.getStatus());
assertEquals("Server Error", response.getReason());
assertThat(BufferUtil.toString(_content), is(""));
assertThat(response.getHeader(HttpHeader.CACHE_CONTROL.asString()), Matchers.nullValue());
response = getResponse();
response.setStatus(200);
assertEquals(200, response.getStatus());
assertNull(response.getReason());
response = getResponse();
response.sendError(406, "Super Nanny");
assertEquals(406, response.getStatus());
assertEquals(HttpStatus.Code.NOT_ACCEPTABLE.getMessage(), response.getReason());
assertThat(BufferUtil.toString(_content), is(""));
assertThat(response.getHeader(HttpHeader.CACHE_CONTROL.asString()), Matchers.nullValue());
} }
@Test @Test
@ -900,7 +875,7 @@ public class ResponseTest
assertFalse(response.isCommitted()); assertFalse(response.isCommitted());
assertFalse(writer.checkError()); assertFalse(writer.checkError());
writer.print(""); writer.print("");
assertFalse(writer.checkError()); // assertFalse(writer.checkError()); TODO check this
assertTrue(response.isCommitted()); assertTrue(response.isCommitted());
} }
@ -1034,7 +1009,7 @@ public class ResponseTest
} }
@Test @Test
public void testCookiesWithReset() public void testResetContent() throws Exception
{ {
Response response = getResponse(); Response response = getResponse();
@ -1050,9 +1025,27 @@ public class ResponseTest
cookie2.setPath("/path"); cookie2.setPath("/path");
response.addCookie(cookie2); response.addCookie(cookie2);
//keep the cookies response.setContentType("some/type");
response.reset(true); response.setContentLength(3);
response.setHeader(HttpHeader.EXPIRES,"never");
response.setHeader("SomeHeader", "SomeValue");
response.getOutputStream();
// reset the content
response.resetContent();
// check content is nulled
assertThat(response.getContentType(), nullValue());
assertThat(response.getContentLength(), is(-1L));
assertThat(response.getHeader(HttpHeader.EXPIRES.asString()), nullValue());
response.getWriter();
// check arbitrary header still set
assertThat(response.getHeader("SomeHeader"), is("SomeValue"));
// check cookies are still there
Enumeration<String> set = response.getHttpFields().getValues("Set-Cookie"); Enumeration<String> set = response.getHttpFields().getValues("Set-Cookie");
assertNotNull(set); assertNotNull(set);

View File

@ -36,6 +36,7 @@ import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.server.CustomRequestLog; import org.eclipse.jetty.server.CustomRequestLog;
import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpChannel; import org.eclipse.jetty.server.HttpChannel;
import org.eclipse.jetty.server.HttpChannelState;
import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.LocalConnector;
import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.RequestLog; import org.eclipse.jetty.server.RequestLog;
@ -43,6 +44,7 @@ import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.BlockingArrayQueue; import org.eclipse.jetty.util.BlockingArrayQueue;
import org.eclipse.jetty.util.component.AbstractLifeCycle; import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.StacklessLogging; import org.eclipse.jetty.util.log.StacklessLogging;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
@ -387,7 +389,7 @@ public class NcsaRequestLogTest
data.add(new Object[]{logType, new IOExceptionPartialHandler(), "/ioex", "\"GET /ioex HTTP/1.0\" 200"}); data.add(new Object[]{logType, new IOExceptionPartialHandler(), "/ioex", "\"GET /ioex HTTP/1.0\" 200"});
data.add(new Object[]{logType, new RuntimeExceptionHandler(), "/rtex", "\"GET /rtex HTTP/1.0\" 500"}); data.add(new Object[]{logType, new RuntimeExceptionHandler(), "/rtex", "\"GET /rtex HTTP/1.0\" 500"});
data.add(new Object[]{logType, new BadMessageHandler(), "/bad", "\"GET /bad HTTP/1.0\" 499"}); data.add(new Object[]{logType, new BadMessageHandler(), "/bad", "\"GET /bad HTTP/1.0\" 499"});
data.add(new Object[]{logType, new AbortHandler(), "/bad", "\"GET /bad HTTP/1.0\" 488"}); data.add(new Object[]{logType, new AbortHandler(), "/bad", "\"GET /bad HTTP/1.0\" 500"});
data.add(new Object[]{logType, new AbortPartialHandler(), "/bad", "\"GET /bad HTTP/1.0\" 200"}); data.add(new Object[]{logType, new AbortPartialHandler(), "/bad", "\"GET /bad HTTP/1.0\" 200"});
}); });
@ -514,7 +516,9 @@ public class NcsaRequestLogTest
startServer(); startServer();
makeRequest(requestPath); makeRequest(requestPath);
expectedLogEntry = "\"GET " + requestPath + " HTTP/1.0\" 200"; // If we abort, we can't write a 200 error page
if (!(testHandler instanceof AbortHandler))
expectedLogEntry = expectedLogEntry.replaceFirst(" [1-9][0-9][0-9]", " 200");
assertRequestLog(expectedLogEntry, _log); assertRequestLog(expectedLogEntry, _log);
} }
@ -574,6 +578,10 @@ public class NcsaRequestLogTest
{ {
try try
{ {
while (baseRequest.getHttpChannel().getState().getState() != HttpChannelState.State.WAITING)
{
Thread.sleep(10);
}
baseRequest.setHandled(false); baseRequest.setHandled(false);
testHandler.handle(target, baseRequest, request, response); testHandler.handle(target, baseRequest, request, response);
if (!baseRequest.isHandled()) if (!baseRequest.isHandled())
@ -581,18 +589,21 @@ public class NcsaRequestLogTest
} }
catch (BadMessageException bad) catch (BadMessageException bad)
{ {
response.sendError(bad.getCode()); response.sendError(bad.getCode(), bad.getReason());
} }
catch (Exception e) catch (Exception e)
{ {
response.sendError(500); response.sendError(500, e.toString());
} }
} }
catch (Throwable th) catch (IOException | IllegalStateException th)
{ {
throw new RuntimeException(th); Log.getLog().ignore(th);
}
finally
{
ac.complete();
} }
ac.complete();
}); });
} }
} }

View File

@ -266,6 +266,7 @@ public class SecuredRedirectHandlerTest
{ {
if (!"/".equals(target)) if (!"/".equals(target))
{ {
baseRequest.setHandled(true);
response.sendError(404); response.sendError(404);
return; return;
} }

View File

@ -20,7 +20,6 @@ package org.eclipse.jetty.server.ssl;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.Socket; import java.net.Socket;
@ -43,6 +42,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.tools.HttpTester;
import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.io.ssl.SslConnection; import org.eclipse.jetty.io.ssl.SslConnection;
@ -55,8 +55,8 @@ import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SocketCustomizationListener; import org.eclipse.jetty.server.SocketCustomizationListener;
import org.eclipse.jetty.server.SslConnectionFactory; import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.server.handler.ErrorHandler;
import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.Utf8StringBuilder;
import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.hamcrest.Matchers; import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
@ -65,9 +65,8 @@ import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.startsWith; import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class SniSslConnectionFactoryTest public class SniSslConnectionFactoryTest
{ {
@ -81,23 +80,23 @@ public class SniSslConnectionFactoryTest
{ {
_server = new Server(); _server = new Server();
HttpConfiguration http_config = new HttpConfiguration(); HttpConfiguration httpConfig = new HttpConfiguration();
http_config.setSecureScheme("https"); httpConfig.setSecureScheme("https");
http_config.setSecurePort(8443); httpConfig.setSecurePort(8443);
http_config.setOutputBufferSize(32768); httpConfig.setOutputBufferSize(32768);
_httpsConfiguration = new HttpConfiguration(http_config); _httpsConfiguration = new HttpConfiguration(httpConfig);
SecureRequestCustomizer src = new SecureRequestCustomizer(); SecureRequestCustomizer src = new SecureRequestCustomizer();
src.setSniHostCheck(true); src.setSniHostCheck(true);
_httpsConfiguration.addCustomizer(src); _httpsConfiguration.addCustomizer(src);
_httpsConfiguration.addCustomizer((connector, httpConfig, request) -> _httpsConfiguration.addCustomizer((connector, hc, request) ->
{ {
EndPoint endp = request.getHttpChannel().getEndPoint(); EndPoint endp = request.getHttpChannel().getEndPoint();
if (endp instanceof SslConnection.DecryptedEndPoint) if (endp instanceof SslConnection.DecryptedEndPoint)
{ {
try try
{ {
SslConnection.DecryptedEndPoint ssl_endp = (SslConnection.DecryptedEndPoint)endp; SslConnection.DecryptedEndPoint sslEndp = (SslConnection.DecryptedEndPoint)endp;
SslConnection sslConnection = ssl_endp.getSslConnection(); SslConnection sslConnection = sslEndp.getSslConnection();
SSLEngine sslEngine = sslConnection.getSSLEngine(); SSLEngine sslEngine = sslConnection.getSSLEngine();
SSLSession session = sslEngine.getSession(); SSLSession session = sslEngine.getSession();
for (Certificate c : session.getLocalCertificates()) for (Certificate c : session.getLocalCertificates())
@ -224,6 +223,7 @@ public class SniSslConnectionFactoryTest
public void testSameConnectionRequestsForManyDomains() throws Exception public void testSameConnectionRequestsForManyDomains() throws Exception
{ {
start("src/test/resources/keystore_sni.p12"); start("src/test/resources/keystore_sni.p12");
_server.setErrorHandler(new ErrorHandler());
SslContextFactory clientContextFactory = new SslContextFactory.Client(true); SslContextFactory clientContextFactory = new SslContextFactory.Client(true);
clientContextFactory.start(); clientContextFactory.start();
@ -246,8 +246,8 @@ public class SniSslConnectionFactoryTest
output.flush(); output.flush();
InputStream input = sslSocket.getInputStream(); InputStream input = sslSocket.getInputStream();
String response = response(input); HttpTester.Response response = HttpTester.parseResponse(input);
assertTrue(response.startsWith("HTTP/1.1 200 ")); assertThat(response.getStatus(), is(200));
// Same socket, send a request for a different domain but same alias. // Same socket, send a request for a different domain but same alias.
request = request =
@ -256,9 +256,8 @@ public class SniSslConnectionFactoryTest
"\r\n"; "\r\n";
output.write(request.getBytes(StandardCharsets.UTF_8)); output.write(request.getBytes(StandardCharsets.UTF_8));
output.flush(); output.flush();
response = HttpTester.parseResponse(input);
response = response(input); assertThat(response.getStatus(), is(200));
assertTrue(response.startsWith("HTTP/1.1 200 "));
// Same socket, send a request for a different domain but different alias. // Same socket, send a request for a different domain but different alias.
request = request =
@ -268,10 +267,9 @@ public class SniSslConnectionFactoryTest
output.write(request.getBytes(StandardCharsets.UTF_8)); output.write(request.getBytes(StandardCharsets.UTF_8));
output.flush(); output.flush();
response = response(input); response = HttpTester.parseResponse(input);
String body = IO.toString(input); assertThat(response.getStatus(), is(400));
assertThat(response, startsWith("HTTP/1.1 400 ")); assertThat(response.getContent(), containsString("Host does not match SNI"));
assertThat(body, containsString("Host does not match SNI"));
} }
finally finally
{ {
@ -304,8 +302,8 @@ public class SniSslConnectionFactoryTest
output.flush(); output.flush();
InputStream input = sslSocket.getInputStream(); InputStream input = sslSocket.getInputStream();
String response = response(input); HttpTester.Response response = HttpTester.parseResponse(input);
assertTrue(response.startsWith("HTTP/1.1 200 ")); assertThat(response.getStatus(), is(200));
// Now, on the same socket, send a request for a different valid domain. // Now, on the same socket, send a request for a different valid domain.
request = request =
@ -315,8 +313,8 @@ public class SniSslConnectionFactoryTest
output.write(request.getBytes(StandardCharsets.UTF_8)); output.write(request.getBytes(StandardCharsets.UTF_8));
output.flush(); output.flush();
response = response(input); response = HttpTester.parseResponse(input);
assertTrue(response.startsWith("HTTP/1.1 200 ")); assertThat(response.getStatus(), is(200));
// Now make a request for an invalid domain for this connection. // Now make a request for an invalid domain for this connection.
request = request =
@ -326,10 +324,9 @@ public class SniSslConnectionFactoryTest
output.write(request.getBytes(StandardCharsets.UTF_8)); output.write(request.getBytes(StandardCharsets.UTF_8));
output.flush(); output.flush();
response = response(input); response = HttpTester.parseResponse(input);
assertTrue(response.startsWith("HTTP/1.1 400 ")); assertThat(response.getStatus(), is(400));
String body = IO.toString(input); assertThat(response.getContent(), containsString("Host does not match SNI"));
assertThat(body, Matchers.containsString("Host does not match SNI"));
} }
finally finally
{ {
@ -337,22 +334,6 @@ public class SniSslConnectionFactoryTest
} }
} }
private String response(InputStream input) throws IOException
{
Utf8StringBuilder buffer = new Utf8StringBuilder();
int crlfs = 0;
while (true)
{
int read = input.read();
assertTrue(read >= 0);
buffer.append((byte)read);
crlfs = (read == '\r' || read == '\n') ? crlfs + 1 : 0;
if (crlfs == 4)
break;
}
return buffer.toString();
}
private String getResponse(String host, String cn) throws Exception private String getResponse(String host, String cn) throws Exception
{ {
String response = getResponse(host, host, cn); String response = getResponse(host, host, cn);

View File

@ -50,6 +50,7 @@ import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
/** /**
@ -478,7 +479,7 @@ public class AsyncContextTest
assertThat("error servlet", responseBody, containsString("ERROR: /error")); assertThat("error servlet", responseBody, containsString("ERROR: /error"));
assertThat("error servlet", responseBody, containsString("PathInfo= /500")); assertThat("error servlet", responseBody, containsString("PathInfo= /500"));
assertThat("error servlet", responseBody, containsString("EXCEPTION: java.lang.RuntimeException: TEST")); assertThat("error servlet", responseBody, not(containsString("EXCEPTION: ")));
} }
private class DispatchingRunnable implements Runnable private class DispatchingRunnable implements Runnable
@ -552,7 +553,7 @@ public class AsyncContextTest
@Override @Override
public void onTimeout(AsyncEvent event) throws IOException public void onTimeout(AsyncEvent event) throws IOException
{ {
throw new RuntimeException("TEST"); throw new RuntimeException("BAD EXPIRE");
} }
@Override @Override

View File

@ -32,6 +32,7 @@ import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.tools.HttpTester; import org.eclipse.jetty.http.tools.HttpTester;
import org.eclipse.jetty.io.QuietException;
import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.LocalConnector;
import org.eclipse.jetty.server.QuietServletException; import org.eclipse.jetty.server.QuietServletException;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
@ -42,6 +43,7 @@ import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
public class AsyncListenerTest public class AsyncListenerTest
@ -140,7 +142,7 @@ public class AsyncListenerTest
test_StartAsync_Throw_OnError(event -> test_StartAsync_Throw_OnError(event ->
{ {
HttpServletResponse response = (HttpServletResponse)event.getAsyncContext().getResponse(); HttpServletResponse response = (HttpServletResponse)event.getAsyncContext().getResponse();
response.sendError(HttpStatus.BAD_GATEWAY_502); response.sendError(HttpStatus.BAD_GATEWAY_502, "Message!!!");
}); });
String httpResponse = connector.getResponse( String httpResponse = connector.getResponse(
"GET /ctx/path HTTP/1.1\r\n" + "GET /ctx/path HTTP/1.1\r\n" +
@ -148,7 +150,8 @@ public class AsyncListenerTest
"Connection: close\r\n" + "Connection: close\r\n" +
"\r\n"); "\r\n");
assertThat(httpResponse, containsString("HTTP/1.1 502 ")); assertThat(httpResponse, containsString("HTTP/1.1 502 "));
assertThat(httpResponse, containsString(TestRuntimeException.class.getName())); assertThat(httpResponse, containsString("Message!!!"));
assertThat(httpResponse, not(containsString(TestRuntimeException.class.getName())));
} }
@Test @Test
@ -191,7 +194,7 @@ public class AsyncListenerTest
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{ {
AsyncContext asyncContext = request.startAsync(); AsyncContext asyncContext = request.startAsync();
asyncContext.setTimeout(0); asyncContext.setTimeout(10000);
asyncContext.addListener(new AsyncListenerAdapter() asyncContext.addListener(new AsyncListenerAdapter()
{ {
@Override @Override
@ -268,7 +271,8 @@ public class AsyncListenerTest
"Connection: close\r\n" + "Connection: close\r\n" +
"\r\n"); "\r\n");
assertThat(httpResponse, containsString("HTTP/1.1 500 ")); assertThat(httpResponse, containsString("HTTP/1.1 500 "));
assertThat(httpResponse, containsString(TestRuntimeException.class.getName())); assertThat(httpResponse, containsString("AsyncContext timeout"));
assertThat(httpResponse, not(containsString(TestRuntimeException.class.getName())));
} }
@Test @Test
@ -292,6 +296,7 @@ public class AsyncListenerTest
{ {
HttpServletResponse response = (HttpServletResponse)event.getAsyncContext().getResponse(); HttpServletResponse response = (HttpServletResponse)event.getAsyncContext().getResponse();
response.sendError(HttpStatus.BAD_GATEWAY_502); response.sendError(HttpStatus.BAD_GATEWAY_502);
event.getAsyncContext().complete();
}); });
String httpResponse = connector.getResponse( String httpResponse = connector.getResponse(
"GET / HTTP/1.1\r\n" + "GET / HTTP/1.1\r\n" +
@ -384,7 +389,7 @@ public class AsyncListenerTest
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{ {
AsyncContext asyncContext = request.startAsync(); AsyncContext asyncContext = request.startAsync();
asyncContext.setTimeout(0); asyncContext.setTimeout(10000);
asyncContext.addListener(new AsyncListenerAdapter() asyncContext.addListener(new AsyncListenerAdapter()
{ {
@Override @Override
@ -447,7 +452,7 @@ public class AsyncListenerTest
} }
// Unique named RuntimeException to help during debugging / assertions. // Unique named RuntimeException to help during debugging / assertions.
public static class TestRuntimeException extends RuntimeException public static class TestRuntimeException extends RuntimeException implements QuietException
{ {
} }

View File

@ -52,6 +52,7 @@ import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.eclipse.jetty.util.thread.QueuedThreadPool;
@ -305,6 +306,7 @@ public class AsyncServletIOTest
request.append(s).append("w=").append(w); request.append(s).append("w=").append(w);
s = '&'; s = '&';
} }
LOG.debug("process {} {}", request.toString(), BufferUtil.toDetailString(BufferUtil.toBuffer(content)));
request.append(" HTTP/1.1\r\n") request.append(" HTTP/1.1\r\n")
.append("Host: localhost\r\n") .append("Host: localhost\r\n")
@ -816,13 +818,15 @@ public class AsyncServletIOTest
// wait until server is ready // wait until server is ready
_servletStolenAsyncRead.ready.await(); _servletStolenAsyncRead.ready.await();
final CountDownLatch wait = new CountDownLatch(1); final CountDownLatch wait = new CountDownLatch(1);
final CountDownLatch held = new CountDownLatch(1);
// Stop any dispatches until we want them // Stop any dispatches until we want them
UnaryOperator<Runnable> old = _wQTP.wrapper.getAndSet(r -> UnaryOperator<Runnable> old = _wQTP.wrapper.getAndSet(r ->
() -> () ->
{ {
try try
{ {
held.countDown();
wait.await(); wait.await();
r.run(); r.run();
} }
@ -836,7 +840,9 @@ public class AsyncServletIOTest
// We are an unrelated thread, let's mess with the input stream // We are an unrelated thread, let's mess with the input stream
ServletInputStream sin = _servletStolenAsyncRead.listener.in; ServletInputStream sin = _servletStolenAsyncRead.listener.in;
sin.setReadListener(_servletStolenAsyncRead.listener); sin.setReadListener(_servletStolenAsyncRead.listener);
// thread should be dispatched to handle, but held by our wQTP wait. // thread should be dispatched to handle, but held by our wQTP wait.
assertTrue(held.await(10, TimeUnit.SECONDS));
// Let's steal our read // Let's steal our read
assertTrue(sin.isReady()); assertTrue(sin.isReady());

View File

@ -265,7 +265,6 @@ public class AsyncServletTest
"start", "start",
"onTimeout", "onTimeout",
"error", "error",
"onError",
"ERROR /ctx/error/custom", "ERROR /ctx/error/custom",
"!initial", "!initial",
"onComplete")); "onComplete"));
@ -273,44 +272,6 @@ public class AsyncServletTest
assertContains("ERROR DISPATCH", response); assertContains("ERROR DISPATCH", response);
} }
@Test
public void testStartOnTimeoutErrorComplete() throws Exception
{
String response = process("start=200&timeout=error&error=complete", null);
assertThat(response, startsWith("HTTP/1.1 200 OK"));
assertThat(__history, contains(
"REQUEST /ctx/path/info",
"initial",
"start",
"onTimeout",
"error",
"onError",
"complete",
"onComplete"));
assertContains("COMPLETED", response);
}
@Test
public void testStartOnTimeoutErrorDispatch() throws Exception
{
String response = process("start=200&timeout=error&error=dispatch", null);
assertThat(response, startsWith("HTTP/1.1 200 OK"));
assertThat(__history, contains(
"REQUEST /ctx/path/info",
"initial",
"start",
"onTimeout",
"error",
"onError",
"dispatch",
"ASYNC /ctx/path/info",
"!initial",
"onComplete"));
assertContains("DISPATCHED", response);
}
@Test @Test
public void testStartOnTimeoutComplete() throws Exception public void testStartOnTimeoutComplete() throws Exception
{ {
@ -526,8 +487,10 @@ public class AsyncServletTest
"onStartAsync", "onStartAsync",
"start", "start",
"onTimeout", "onTimeout",
"ERROR /ctx/path/error",
"!initial",
"onComplete")); // Error Page Loop! "onComplete")); // Error Page Loop!
assertContains("HTTP ERROR 500", response); assertContains("AsyncContext timeout", response);
} }
@Test @Test

View File

@ -85,7 +85,8 @@ public class CustomRequestLogTest
_connector.getResponse("GET /context/servlet/info HTTP/1.0\n\n"); _connector.getResponse("GET /context/servlet/info HTTP/1.0\n\n");
String log = _entries.poll(5, TimeUnit.SECONDS); String log = _entries.poll(5, TimeUnit.SECONDS);
assertThat(log, is("Filename: " + _tmpDir + File.separator + "servlet" + File.separator + "info")); String expected = new File(_tmpDir + File.separator + "servlet" + File.separator + "info").getCanonicalPath();
assertThat(log, is("Filename: " + expected));
} }
@Test @Test

View File

@ -20,8 +20,22 @@ package org.eclipse.jetty.servlet;
import java.io.IOException; import java.io.IOException;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.EnumSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import javax.servlet.AsyncContext;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.Servlet; import javax.servlet.Servlet;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
@ -29,8 +43,12 @@ import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.BadMessageException; import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.server.Dispatcher; import org.eclipse.jetty.server.Dispatcher;
import org.eclipse.jetty.server.HttpChannel; import org.eclipse.jetty.server.HttpChannel;
import org.eclipse.jetty.server.HttpChannelState;
import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.LocalConnector;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.HandlerWrapper;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.StacklessLogging; import org.eclipse.jetty.util.log.StacklessLogging;
import org.hamcrest.Matchers; import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
@ -40,43 +58,71 @@ import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class ErrorPageTest public class ErrorPageTest
{ {
private Server _server; private Server _server;
private LocalConnector _connector; private LocalConnector _connector;
private StacklessLogging _stackless; private StacklessLogging _stackless;
private static CountDownLatch __asyncSendErrorCompleted;
private ErrorPageErrorHandler _errorPageErrorHandler;
@BeforeEach @BeforeEach
public void init() throws Exception public void init() throws Exception
{ {
_server = new Server(); _server = new Server();
_connector = new LocalConnector(_server); _connector = new LocalConnector(_server);
_server.addConnector(_connector);
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS); ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS);
_server.addConnector(_connector);
_server.setHandler(context); _server.setHandler(context);
context.setContextPath("/"); context.setContextPath("/");
context.addFilter(SingleDispatchFilter.class, "/*", EnumSet.allOf(DispatcherType.class));
context.addServlet(DefaultServlet.class, "/"); context.addServlet(DefaultServlet.class, "/");
context.addServlet(FailServlet.class, "/fail/*"); context.addServlet(FailServlet.class, "/fail/*");
context.addServlet(FailClosedServlet.class, "/fail-closed/*"); context.addServlet(FailClosedServlet.class, "/fail-closed/*");
context.addServlet(ErrorServlet.class, "/error/*"); context.addServlet(ErrorServlet.class, "/error/*");
context.addServlet(AppServlet.class, "/app/*"); context.addServlet(AppServlet.class, "/app/*");
context.addServlet(LongerAppServlet.class, "/longer.app/*"); context.addServlet(LongerAppServlet.class, "/longer.app/*");
context.addServlet(SyncSendErrorServlet.class, "/sync/*");
context.addServlet(AsyncSendErrorServlet.class, "/async/*");
context.addServlet(NotEnoughServlet.class, "/notenough/*");
context.addServlet(DeleteServlet.class, "/delete/*");
context.addServlet(ErrorAndStatusServlet.class, "/error-and-status/*");
ErrorPageErrorHandler error = new ErrorPageErrorHandler(); HandlerWrapper noopHandler = new HandlerWrapper()
context.setErrorHandler(error); {
error.addErrorPage(599, "/error/599"); @Override
error.addErrorPage(400, "/error/400"); public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
if (target.startsWith("/noop"))
return;
else
super.handle(target, baseRequest, request, response);
}
};
context.insertHandler(noopHandler);
_errorPageErrorHandler = new ErrorPageErrorHandler();
context.setErrorHandler(_errorPageErrorHandler);
_errorPageErrorHandler.addErrorPage(595, "/error/595");
_errorPageErrorHandler.addErrorPage(597, "/sync");
_errorPageErrorHandler.addErrorPage(599, "/error/599");
_errorPageErrorHandler.addErrorPage(400, "/error/400");
// error.addErrorPage(500,"/error/500"); // error.addErrorPage(500,"/error/500");
error.addErrorPage(IllegalStateException.class.getCanonicalName(), "/error/TestException"); _errorPageErrorHandler.addErrorPage(IllegalStateException.class.getCanonicalName(), "/error/TestException");
error.addErrorPage(BadMessageException.class, "/error/BadMessageException"); _errorPageErrorHandler.addErrorPage(BadMessageException.class, "/error/BadMessageException");
error.addErrorPage(ErrorPageErrorHandler.GLOBAL_ERROR_PAGE, "/error/GlobalErrorPage"); _errorPageErrorHandler.addErrorPage(ErrorPageErrorHandler.GLOBAL_ERROR_PAGE, "/error/GlobalErrorPage");
_server.start(); _server.start();
_stackless = new StacklessLogging(ServletHandler.class); _stackless = new StacklessLogging(ServletHandler.class);
__asyncSendErrorCompleted = new CountDownLatch(1);
} }
@AfterEach @AfterEach
@ -87,6 +133,101 @@ public class ErrorPageTest
_server.join(); _server.join();
} }
@Test
void testErrorOverridesStatus() throws Exception
{
String response = _connector.getResponse("GET /error-and-status/anything HTTP/1.0\r\n\r\n");
assertThat(response, Matchers.containsString("HTTP/1.1 594 594"));
assertThat(response, Matchers.containsString("ERROR_PAGE: /GlobalErrorPage"));
assertThat(response, Matchers.containsString("ERROR_MESSAGE: custom get error"));
assertThat(response, Matchers.containsString("ERROR_CODE: 594"));
assertThat(response, Matchers.containsString("ERROR_EXCEPTION: null"));
assertThat(response, Matchers.containsString("ERROR_EXCEPTION_TYPE: null"));
assertThat(response, Matchers.containsString("ERROR_SERVLET: org.eclipse.jetty.servlet.ErrorPageTest$ErrorAndStatusServlet-"));
assertThat(response, Matchers.containsString("ERROR_REQUEST_URI: /error-and-status/anything"));
}
@Test
void testHttp204CannotHaveBody() throws Exception
{
String response = _connector.getResponse("GET /fail/code?code=204 HTTP/1.0\r\n\r\n");
assertThat(response, Matchers.containsString("HTTP/1.1 204 No Content"));
assertThat(response, not(Matchers.containsString("DISPATCH: ")));
assertThat(response, not(Matchers.containsString("ERROR_PAGE: ")));
assertThat(response, not(Matchers.containsString("ERROR_CODE: ")));
assertThat(response, not(Matchers.containsString("ERROR_EXCEPTION: ")));
assertThat(response, not(Matchers.containsString("ERROR_EXCEPTION_TYPE: ")));
assertThat(response, not(Matchers.containsString("ERROR_SERVLET: ")));
assertThat(response, not(Matchers.containsString("ERROR_REQUEST_URI: ")));
}
@Test
void testDeleteCannotHaveBody() throws Exception
{
String response = _connector.getResponse("DELETE /delete/anything HTTP/1.0\r\n\r\n");
assertThat(response, Matchers.containsString("HTTP/1.1 595 595"));
assertThat(response, not(Matchers.containsString("DISPATCH: ")));
assertThat(response, not(Matchers.containsString("ERROR_PAGE: ")));
assertThat(response, not(Matchers.containsString("ERROR_MESSAGE: ")));
assertThat(response, not(Matchers.containsString("ERROR_CODE: ")));
assertThat(response, not(Matchers.containsString("ERROR_EXCEPTION: ")));
assertThat(response, not(Matchers.containsString("ERROR_EXCEPTION_TYPE: ")));
assertThat(response, not(Matchers.containsString("ERROR_SERVLET: ")));
assertThat(response, not(Matchers.containsString("ERROR_REQUEST_URI: ")));
assertThat(response, not(containsString("This shouldn't be seen")));
}
@Test
void testGenerateAcceptableResponse_noAcceptHeader() throws Exception
{
// no global error page here
_errorPageErrorHandler.getErrorPages().remove(ErrorPageErrorHandler.GLOBAL_ERROR_PAGE);
String response = _connector.getResponse("GET /fail/code?code=598 HTTP/1.0\r\n\r\n");
assertThat(response, Matchers.containsString("HTTP/1.1 598 598"));
assertThat(response, Matchers.containsString("<title>Error 598"));
assertThat(response, Matchers.containsString("<h2>HTTP ERROR 598"));
assertThat(response, Matchers.containsString("/fail/code"));
}
@Test
void testGenerateAcceptableResponse_htmlAcceptHeader() throws Exception
{
// no global error page here
_errorPageErrorHandler.getErrorPages().remove(ErrorPageErrorHandler.GLOBAL_ERROR_PAGE);
// even when text/html is not the 1st content type, a html error page should still be generated
String response = _connector.getResponse("GET /fail/code?code=598 HTTP/1.0\r\n" +
"Accept: application/bytes,text/html\r\n\r\n");
assertThat(response, Matchers.containsString("HTTP/1.1 598 598"));
assertThat(response, Matchers.containsString("<title>Error 598"));
assertThat(response, Matchers.containsString("<h2>HTTP ERROR 598"));
assertThat(response, Matchers.containsString("/fail/code"));
}
@Test
void testGenerateAcceptableResponse_noHtmlAcceptHeader() throws Exception
{
// no global error page here
_errorPageErrorHandler.getErrorPages().remove(ErrorPageErrorHandler.GLOBAL_ERROR_PAGE);
String response = _connector.getResponse("GET /fail/code?code=598 HTTP/1.0\r\n" +
"Accept: application/bytes\r\n\r\n");
assertThat(response, Matchers.containsString("HTTP/1.1 598 598"));
assertThat(response, not(Matchers.containsString("<title>Error 598")));
assertThat(response, not(Matchers.containsString("<h2>HTTP ERROR 598")));
assertThat(response, not(Matchers.containsString("/fail/code")));
}
@Test
void testNestedSendErrorDoesNotLoop() throws Exception
{
String response = _connector.getResponse("GET /fail/code?code=597 HTTP/1.0\r\n\r\n");
assertThat(response, Matchers.containsString("HTTP/1.1 597 597"));
assertThat(response, not(Matchers.containsString("time this error page is being accessed")));
}
@Test @Test
public void testSendErrorClosedResponse() throws Exception public void testSendErrorClosedResponse() throws Exception
{ {
@ -167,7 +308,7 @@ public class ErrorPageTest
try (StacklessLogging ignore = new StacklessLogging(Dispatcher.class)) try (StacklessLogging ignore = new StacklessLogging(Dispatcher.class))
{ {
String response = _connector.getResponse("GET /app?baa=%88%A4 HTTP/1.0\r\n\r\n"); String response = _connector.getResponse("GET /app?baa=%88%A4 HTTP/1.0\r\n\r\n");
assertThat(response, Matchers.containsString("HTTP/1.1 400 ")); assertThat(response, Matchers.containsString("HTTP/1.1 400 Bad Request"));
assertThat(response, Matchers.containsString("ERROR_PAGE: /BadMessageException")); assertThat(response, Matchers.containsString("ERROR_PAGE: /BadMessageException"));
assertThat(response, Matchers.containsString("ERROR_MESSAGE: Bad query encoding")); assertThat(response, Matchers.containsString("ERROR_MESSAGE: Bad query encoding"));
assertThat(response, Matchers.containsString("ERROR_CODE: 400")); assertThat(response, Matchers.containsString("ERROR_CODE: 400"));
@ -179,6 +320,94 @@ public class ErrorPageTest
} }
} }
@Test
public void testAsyncErrorPageDSC() throws Exception
{
try (StacklessLogging ignore = new StacklessLogging(Dispatcher.class))
{
String response = _connector.getResponse("GET /async/info?mode=DSC HTTP/1.0\r\n\r\n");
assertThat(response, Matchers.containsString("HTTP/1.1 599 599"));
assertThat(response, Matchers.containsString("ERROR_PAGE: /599"));
assertThat(response, Matchers.containsString("ERROR_CODE: 599"));
assertThat(response, Matchers.containsString("ERROR_EXCEPTION: null"));
assertThat(response, Matchers.containsString("ERROR_EXCEPTION_TYPE: null"));
assertThat(response, Matchers.containsString("ERROR_SERVLET: org.eclipse.jetty.servlet.ErrorPageTest$AsyncSendErrorServlet-"));
assertThat(response, Matchers.containsString("ERROR_REQUEST_URI: /async/info"));
assertTrue(__asyncSendErrorCompleted.await(10, TimeUnit.SECONDS));
}
}
@Test
public void testAsyncErrorPageSDC() throws Exception
{
try (StacklessLogging ignore = new StacklessLogging(Dispatcher.class))
{
String response = _connector.getResponse("GET /async/info?mode=SDC HTTP/1.0\r\n\r\n");
assertThat(response, Matchers.containsString("HTTP/1.1 599 599"));
assertThat(response, Matchers.containsString("ERROR_PAGE: /599"));
assertThat(response, Matchers.containsString("ERROR_CODE: 599"));
assertThat(response, Matchers.containsString("ERROR_EXCEPTION: null"));
assertThat(response, Matchers.containsString("ERROR_EXCEPTION_TYPE: null"));
assertThat(response, Matchers.containsString("ERROR_SERVLET: org.eclipse.jetty.servlet.ErrorPageTest$AsyncSendErrorServlet-"));
assertThat(response, Matchers.containsString("ERROR_REQUEST_URI: /async/info"));
assertTrue(__asyncSendErrorCompleted.await(10, TimeUnit.SECONDS));
}
}
@Test
public void testAsyncErrorPageSCD() throws Exception
{
try (StacklessLogging ignore = new StacklessLogging(Dispatcher.class))
{
String response = _connector.getResponse("GET /async/info?mode=SCD HTTP/1.0\r\n\r\n");
assertThat(response, Matchers.containsString("HTTP/1.1 599 599"));
assertThat(response, Matchers.containsString("ERROR_PAGE: /599"));
assertThat(response, Matchers.containsString("ERROR_CODE: 599"));
assertThat(response, Matchers.containsString("ERROR_EXCEPTION: null"));
assertThat(response, Matchers.containsString("ERROR_EXCEPTION_TYPE: null"));
assertThat(response, Matchers.containsString("ERROR_SERVLET: org.eclipse.jetty.servlet.ErrorPageTest$AsyncSendErrorServlet-"));
assertThat(response, Matchers.containsString("ERROR_REQUEST_URI: /async/info"));
assertTrue(__asyncSendErrorCompleted.await(10, TimeUnit.SECONDS));
}
}
@Test
public void testNoop() throws Exception
{
String response = _connector.getResponse("GET /noop/info HTTP/1.0\r\n\r\n");
assertThat(response, Matchers.containsString("HTTP/1.1 404 Not Found"));
assertThat(response, Matchers.containsString("DISPATCH: ERROR"));
assertThat(response, Matchers.containsString("ERROR_PAGE: /GlobalErrorPage"));
assertThat(response, Matchers.containsString("ERROR_CODE: 404"));
assertThat(response, Matchers.containsString("ERROR_EXCEPTION: null"));
assertThat(response, Matchers.containsString("ERROR_EXCEPTION_TYPE: null"));
assertThat(response, Matchers.containsString("ERROR_SERVLET: org.eclipse.jetty.servlet.DefaultServlet-"));
assertThat(response, Matchers.containsString("ERROR_REQUEST_URI: /noop/info"));
}
@Test
public void testNotEnough() throws Exception
{
String response = _connector.getResponse("GET /notenough/info HTTP/1.0\r\n\r\n");
assertThat(response, Matchers.containsString("HTTP/1.1 500 Server Error"));
assertThat(response, Matchers.containsString("DISPATCH: ERROR"));
assertThat(response, Matchers.containsString("ERROR_PAGE: /GlobalErrorPage"));
assertThat(response, Matchers.containsString("ERROR_CODE: 500"));
assertThat(response, Matchers.containsString("ERROR_EXCEPTION: null"));
assertThat(response, Matchers.containsString("ERROR_EXCEPTION_TYPE: null"));
assertThat(response, Matchers.containsString("ERROR_SERVLET: org.eclipse.jetty.servlet.ErrorPageTest$NotEnoughServlet-"));
assertThat(response, Matchers.containsString("ERROR_REQUEST_URI: /notenough/info"));
}
@Test
public void testNotEnoughCommitted() throws Exception
{
String response = _connector.getResponse("GET /notenough/info?commit=true HTTP/1.0\r\n\r\n");
assertThat(response, Matchers.containsString("HTTP/1.1 200 OK"));
assertThat(response, Matchers.containsString("Content-Length: 1000"));
assertThat(response, Matchers.endsWith("SomeBytes"));
}
public static class AppServlet extends HttpServlet implements Servlet public static class AppServlet extends HttpServlet implements Servlet
{ {
@Override @Override
@ -198,6 +427,112 @@ public class ErrorPageTest
} }
} }
public static class SyncSendErrorServlet extends HttpServlet implements Servlet
{
public static final AtomicInteger COUNTER = new AtomicInteger();
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
int count = COUNTER.incrementAndGet();
PrintWriter writer = response.getWriter();
writer.println("this is the " + count + " time this error page is being accessed");
response.sendError(597, "loop #" + count);
}
}
public static class AsyncSendErrorServlet extends HttpServlet implements Servlet
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
try
{
final CountDownLatch hold = new CountDownLatch(1);
final String mode = request.getParameter("mode");
switch(mode)
{
case "DSC":
case "SDC":
case "SCD":
break;
default:
throw new IllegalStateException(mode);
}
final boolean lateComplete = "true".equals(request.getParameter("latecomplete"));
AsyncContext async = request.startAsync();
async.start(() ->
{
try
{
switch(mode)
{
case "SDC":
response.sendError(599);
break;
case "SCD":
response.sendError(599);
async.complete();
break;
default:
break;
}
// Complete after original servlet
hold.countDown();
// Wait until request async waiting
while (Request.getBaseRequest(request).getHttpChannelState().getState() == HttpChannelState.State.HANDLING)
{
try
{
Thread.sleep(10);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
try
{
switch (mode)
{
case "DSC":
response.sendError(599);
async.complete();
break;
case "SDC":
async.complete();
break;
default:
break;
}
}
catch(IllegalStateException e)
{
Log.getLog().ignore(e);
}
finally
{
__asyncSendErrorCompleted.countDown();
}
}
catch (IOException e)
{
Log.getLog().warn(e);
}
});
hold.await();
}
catch (InterruptedException e)
{
throw new ServletException(e);
}
}
}
public static class FailServlet extends HttpServlet implements Servlet public static class FailServlet extends HttpServlet implements Servlet
{ {
@Override @Override
@ -225,16 +560,51 @@ public class ErrorPageTest
} }
catch (Throwable ignore) catch (Throwable ignore)
{ {
// no opEchoSocket Log.getLog().ignore(ignore);
} }
} }
} }
public static class ErrorAndStatusServlet extends HttpServlet implements Servlet
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
response.sendError(594, "custom get error");
response.setStatus(200);
}
}
public static class DeleteServlet extends HttpServlet implements Servlet
{
@Override
protected void doDelete(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
response.getWriter().append("This shouldn't be seen");
response.sendError(595, "custom delete");
}
}
public static class NotEnoughServlet extends HttpServlet implements Servlet
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
response.setContentLength(1000);
response.getOutputStream().write("SomeBytes".getBytes(StandardCharsets.UTF_8));
if (Boolean.parseBoolean(request.getParameter("commit")))
response.flushBuffer();
}
}
public static class ErrorServlet extends HttpServlet implements Servlet public static class ErrorServlet extends HttpServlet implements Servlet
{ {
@Override @Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{ {
if (request.getDispatcherType() != DispatcherType.ERROR && request.getDispatcherType() != DispatcherType.ASYNC)
throw new IllegalStateException("Bad Dispatcher Type " + request.getDispatcherType());
PrintWriter writer = response.getWriter(); PrintWriter writer = response.getWriter();
writer.println("DISPATCH: " + request.getDispatcherType().name()); writer.println("DISPATCH: " + request.getDispatcherType().name());
writer.println("ERROR_PAGE: " + request.getPathInfo()); writer.println("ERROR_PAGE: " + request.getPathInfo());
@ -247,4 +617,55 @@ public class ErrorPageTest
writer.println("getParameterMap()= " + request.getParameterMap()); writer.println("getParameterMap()= " + request.getParameterMap());
} }
} }
public static class SingleDispatchFilter implements Filter
{
ConcurrentMap<Integer, Thread> dispatches = new ConcurrentHashMap<>();
@Override
public void init(FilterConfig filterConfig) throws ServletException
{
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
{
final Integer key = request.hashCode();
Thread current = Thread.currentThread();
final Thread existing = dispatches.putIfAbsent(key, current);
if (existing != null && existing != current)
{
System.err.println("DOUBLE DISPATCH OF REQUEST!!!!!!!!!!!!!!!!!!");
System.err.println("Thread " + existing + " :");
for (StackTraceElement element : existing.getStackTrace())
{
System.err.println("\tat " + element);
}
IllegalStateException ex = new IllegalStateException();
ex.printStackTrace();
response.flushBuffer();
throw ex;
}
try
{
chain.doFilter(request, response);
}
finally
{
if (existing == null)
{
if (!dispatches.remove(key, current))
throw new IllegalStateException();
}
}
}
@Override
public void destroy()
{
}
}
} }

View File

@ -153,7 +153,10 @@ public class GzipHandlerTest
response.setHeader("ETag", __contentETag); response.setHeader("ETag", __contentETag);
String ifnm = req.getHeader("If-None-Match"); String ifnm = req.getHeader("If-None-Match");
if (ifnm != null && ifnm.equals(__contentETag)) if (ifnm != null && ifnm.equals(__contentETag))
response.sendError(304); {
response.setStatus(304);
response.flushBuffer();
}
else else
{ {
PrintWriter writer = response.getWriter(); PrintWriter writer = response.getWriter();

View File

@ -501,7 +501,16 @@ public class DoSFilter implements Filter
{ {
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("Timing out {}", request); LOG.debug("Timing out {}", request);
response.sendError(HttpStatus.SERVICE_UNAVAILABLE_503); try
{
response.sendError(HttpStatus.SERVICE_UNAVAILABLE_503);
}
catch (IllegalStateException ise)
{
LOG.ignore(ise);
// abort instead
response.sendError(-1);
}
} }
catch (Throwable x) catch (Throwable x)
{ {

View File

@ -470,6 +470,7 @@ public class BufferUtil
* *
* @param to Buffer is flush mode * @param to Buffer is flush mode
* @param b byte to append * @param b byte to append
* @throws BufferOverflowException if unable to append buffer due to space limits
*/ */
public static void append(ByteBuffer to, byte b) public static void append(ByteBuffer to, byte b)
{ {
@ -1114,20 +1115,20 @@ public class BufferUtil
for (int i = 0; i < buffer.position(); i++) for (int i = 0; i < buffer.position(); i++)
{ {
appendContentChar(buf, buffer.get(i)); appendContentChar(buf, buffer.get(i));
if (i == 16 && buffer.position() > 32) if (i == 8 && buffer.position() > 16)
{ {
buf.append("..."); buf.append("...");
i = buffer.position() - 16; i = buffer.position() - 8;
} }
} }
buf.append("<<<"); buf.append("<<<");
for (int i = buffer.position(); i < buffer.limit(); i++) for (int i = buffer.position(); i < buffer.limit(); i++)
{ {
appendContentChar(buf, buffer.get(i)); appendContentChar(buf, buffer.get(i));
if (i == buffer.position() + 16 && buffer.limit() > buffer.position() + 32) if (i == buffer.position() + 24 && buffer.limit() > buffer.position() + 48)
{ {
buf.append("..."); buf.append("...");
i = buffer.limit() - 16; i = buffer.limit() - 24;
} }
} }
buf.append(">>>"); buf.append(">>>");
@ -1136,10 +1137,10 @@ public class BufferUtil
for (int i = limit; i < buffer.capacity(); i++) for (int i = limit; i < buffer.capacity(); i++)
{ {
appendContentChar(buf, buffer.get(i)); appendContentChar(buf, buffer.get(i));
if (i == limit + 16 && buffer.capacity() > limit + 32) if (i == limit + 8 && buffer.capacity() > limit + 16)
{ {
buf.append("..."); buf.append("...");
i = buffer.capacity() - 16; i = buffer.capacity() - 8;
} }
} }
buffer.limit(limit); buffer.limit(limit);

View File

@ -171,10 +171,11 @@ public class QueuedThreadPool extends ContainerLifeCycle implements SizedThreadP
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("Stopping {}", this); LOG.debug("Stopping {}", this);
super.doStop();
removeBean(_tryExecutor); removeBean(_tryExecutor);
_tryExecutor = TryExecutor.NO_TRY; _tryExecutor = TryExecutor.NO_TRY;
super.doStop();
// Signal the Runner threads that we are stopping // Signal the Runner threads that we are stopping
int threads = _counts.getAndSetHi(Integer.MIN_VALUE); int threads = _counts.getAndSetHi(Integer.MIN_VALUE);

View File

@ -108,8 +108,8 @@ public class BadAppTests extends AbstractDistributionTest
startHttpClient(); startHttpClient();
ContentResponse response = client.GET("http://localhost:" + port + "/badapp/"); ContentResponse response = client.GET("http://localhost:" + port + "/badapp/");
assertEquals(HttpStatus.SERVICE_UNAVAILABLE_503, response.getStatus()); assertEquals(HttpStatus.SERVICE_UNAVAILABLE_503, response.getStatus());
assertThat(response.getContentAsString(), containsString("Unavailable")); assertThat(response.getContentAsString(), containsString("<h2>HTTP ERROR 503 Service Unavailable</h2>"));
assertThat(response.getContentAsString(), containsString("Problem accessing /badapp/")); assertThat(response.getContentAsString(), containsString("<tr><th>URI:</th><td>/badapp/</td></tr>"));
} }
} }
} }
@ -148,8 +148,8 @@ public class BadAppTests extends AbstractDistributionTest
startHttpClient(); startHttpClient();
ContentResponse response = client.GET("http://localhost:" + port + "/badapp/"); ContentResponse response = client.GET("http://localhost:" + port + "/badapp/");
assertEquals(HttpStatus.SERVICE_UNAVAILABLE_503, response.getStatus()); assertEquals(HttpStatus.SERVICE_UNAVAILABLE_503, response.getStatus());
assertThat(response.getContentAsString(), containsString("Unavailable")); assertThat(response.getContentAsString(), containsString("<h2>HTTP ERROR 503 Service Unavailable</h2>"));
assertThat(response.getContentAsString(), containsString("Problem accessing /badapp/")); assertThat(response.getContentAsString(), containsString("<tr><th>URI:</th><td>/badapp/</td></tr>"));
} }
} }
} }