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
* <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)
{
_log.ignore(e);
try
{
proxyResponse.sendError(-1);
}
catch (Exception e2)
{
_log.ignore(e2);
}
}
finally
{

View File

@ -22,6 +22,8 @@ import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.util.StringUtil;
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 org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertEquals;
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" +
"Authorization: Basic " + encodedChris + "\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
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;
import java.io.IOException;
import javax.servlet.http.HttpServletResponse;
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.server.Authentication;
import org.eclipse.jetty.server.HttpChannel;
import org.eclipse.jetty.server.HttpChannelState;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpOutput;
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.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class SpnegoAuthenticatorTest
@ -56,20 +60,27 @@ public class SpnegoAuthenticatorTest
{
return null;
}
};
Request req = new Request(channel, null);
HttpOutput out = new HttpOutput(channel)
{
@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.setURI(new HttpURI("http://localhost"));
req.setMetaData(metadata);
assertThat(channel.getState().handling(), is(HttpChannelState.Action.DISPATCH));
assertEquals(Authentication.SEND_CONTINUE, _authenticator.validateRequest(req, res, true));
assertEquals(HttpHeader.NEGOTIATE.asString(), res.getHeader(HttpHeader.WWW_AUTHENTICATE.asString()));
assertEquals(HttpServletResponse.SC_UNAUTHORIZED, res.getStatus());
@ -85,16 +96,22 @@ public class SpnegoAuthenticatorTest
{
return null;
}
};
Request req = new Request(channel, null);
HttpOutput out = new HttpOutput(channel)
{
@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();
// Create a bogus Authorization header. We don't care about the actual credentials.
http_fields.add(HttpHeader.AUTHORIZATION, "Basic asdf");
@ -102,6 +119,7 @@ public class SpnegoAuthenticatorTest
metadata.setURI(new HttpURI("http://localhost"));
req.setMetaData(metadata);
assertThat(channel.getState().handling(), is(HttpChannelState.Action.DISPATCH));
assertEquals(Authentication.SEND_CONTINUE, _authenticator.validateRequest(req, res, true));
assertEquals(HttpHeader.NEGOTIATE.asString(), res.getHeader(HttpHeader.WWW_AUTHENTICATE.asString()));
assertEquals(HttpServletResponse.SC_UNAUTHORIZED, res.getStatus());

View File

@ -160,7 +160,7 @@ public class AsyncContextEvent extends AsyncEvent implements Runnable
Scheduler.Task task = _timeoutTask;
_timeoutTask = null;
if (task != null)
_state.getHttpChannel().execute(() -> _state.onTimeout());
_state.timeout();
}
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.HttpURI;
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.MultiMap;
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);
public static final String __ERROR_DISPATCH = "org.eclipse.jetty.server.Dispatcher.ERROR";
/**
* Dispatch include attribute names
*/
@ -77,15 +76,7 @@ public class Dispatcher implements RequestDispatcher
public void error(ServletRequest request, ServletResponse response) throws ServletException, IOException
{
try
{
request.setAttribute(__ERROR_DISPATCH, Boolean.TRUE);
forward(request, response, DispatcherType.ERROR);
}
finally
{
request.setAttribute(__ERROR_DISPATCH, null);
}
forward(request, response, DispatcherType.ERROR);
}
@Override

View File

@ -25,7 +25,6 @@ import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
@ -33,12 +32,12 @@ import java.util.function.Function;
import java.util.function.Supplier;
import javax.servlet.DispatcherType;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpGenerator;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpStatus;
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.handler.ContextHandler;
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.Callback;
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 final AtomicBoolean _committed = new AtomicBoolean();
private final AtomicBoolean _responseCompleted = new AtomicBoolean();
private final AtomicLong _requests = new AtomicLong();
private final Connector _connector;
private final Executor _executor;
@ -121,6 +119,11 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
_state);
}
public boolean isSendError()
{
return _state.isSendError();
}
protected HttpInput newHttpInput(HttpChannelState state)
{
return new HttpInput(state);
@ -284,8 +287,6 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
public void recycle()
{
_committed.set(false);
_responseCompleted.set(false);
_request.recycle();
_response.recycle();
_committedMetaData = null;
@ -320,7 +321,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
public boolean handle()
{
if (LOG.isDebugEnabled())
LOG.debug("{} handle {} ", this, _request.getHttpURI());
LOG.debug("handle {} {} ", _request.getHttpURI(), this);
HttpChannelState.Action action = _state.handling();
@ -334,19 +335,18 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
try
{
if (LOG.isDebugEnabled())
LOG.debug("{} action {}", this, action);
LOG.debug("action {} {}", action, this);
switch (action)
{
case TERMINATED:
onCompleted();
break loop;
case WAIT:
// break loop without calling unhandle
break loop;
case NOOP:
// do nothing other than call unhandle
break;
case DISPATCH:
{
if (!_request.hasMetaData())
@ -354,35 +354,17 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
_request.setHandled(false);
_response.getHttpOutput().reopen();
try
dispatch(DispatcherType.REQUEST, () ->
{
_request.setDispatcherType(DispatcherType.REQUEST);
notifyBeforeDispatch(_request);
List<HttpConfiguration.Customizer> customizers = _configuration.getCustomizers();
if (!customizers.isEmpty())
for (HttpConfiguration.Customizer customizer : _configuration.getCustomizers())
{
for (HttpConfiguration.Customizer customizer : customizers)
{
customizer.customize(getConnector(), _configuration, _request);
if (_request.isHandled())
break;
}
customizer.customize(getConnector(), _configuration, _request);
if (_request.isHandled())
return;
}
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;
}
@ -391,70 +373,70 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
_request.setHandled(false);
_response.getHttpOutput().reopen();
try
{
_request.setDispatcherType(DispatcherType.ASYNC);
notifyBeforeDispatch(_request);
getServer().handleAsync(this);
}
catch (Throwable x)
{
notifyDispatchFailure(_request, x);
throw x;
}
finally
{
notifyAfterDispatch(_request);
_request.setDispatcherType(null);
}
dispatch(DispatcherType.ASYNC,() -> getServer().handleAsync(this));
break;
}
case ERROR_DISPATCH:
case ASYNC_TIMEOUT:
_state.onTimeout();
break;
case SEND_ERROR:
{
try
{
_response.reset(true);
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);
// Get ready to send an error response
_request.setHandled(false);
_response.resetContent();
_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);
notifyBeforeDispatch(_request);
getServer().handle(this);
sendResponseAndComplete();
break;
}
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);
throw x;
// Allow ErrorHandler to generate response
errorHandler.handle(null, _request, _request, _response);
_request.setHandled(true);
}
finally
else
{
notifyAfterDispatch(_request);
_request.setDispatcherType(null);
// Do the error page dispatch
dispatch(DispatcherType.ERROR,() -> errorDispatcher.error(_request, _response));
}
}
catch (Throwable x)
{
if (LOG.isDebugEnabled())
LOG.debug("Could not perform ERROR dispatch, aborting", x);
Throwable failure = (Throwable)_request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
if (failure == null)
{
minimalErrorResponse(x);
}
if (_state.isResponseCommitted())
abort(x);
else
{
if (x != failure)
failure.addSuppressed(x);
minimalErrorResponse(failure);
_response.resetContent();
sendResponseAndComplete();
}
}
finally
{
// clean up the context that was set in Response.sendError
_request.removeAttribute(ErrorHandler.ERROR_CONTEXT);
}
break;
}
@ -463,6 +445,12 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
throw _state.getAsyncContextEvent().getThrowable();
}
case READ_REGISTER:
{
onAsyncWaitForContent();
break;
}
case READ_PRODUCE:
{
_request.getHttpInput().asyncReadProduce();
@ -491,41 +479,32 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
case COMPLETE:
{
try
if (!_response.isCommitted() && !_request.isHandled() && !_response.getHttpOutput().isClosed())
{
if (!_response.isCommitted() && !_request.isHandled())
{
_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();
_response.sendError(HttpStatus.NOT_FOUND_404);
break;
}
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:
{
throw new IllegalStateException("state=" + _state);
}
throw new IllegalStateException(this.toString());
}
}
catch (Throwable failure)
@ -540,7 +519,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
}
if (LOG.isDebugEnabled())
LOG.debug("{} handle exit, result {}", this, action);
LOG.debug("!handle {} {}", action, this);
boolean suspended = action == Action.WAIT;
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
{
_response.sendError(code, reason);
_request.setDispatcherType(type);
notifyBeforeDispatch(_request);
dispatchable.dispatch();
}
catch (Throwable x)
{
if (LOG.isDebugEnabled())
LOG.debug("Could not send error " + code + " " + reason, x);
notifyDispatchFailure(_request, x);
throw x;
}
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
if (LOG.isDebugEnabled())
LOG.debug(_request.getRequestURI(), failure);
LOG.warn("handleException " + _request.getRequestURI(), failure);
else
LOG.warn("{} {}", _request.getRequestURI(), noStack.toString());
LOG.warn("handleException {} {}", _request.getRequestURI(), noStack.toString());
}
else
{
LOG.warn(_request.getRequestURI(), failure);
}
try
{
if (isCommitted())
abort(failure);
else
_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;
}
private void minimalErrorResponse(Throwable failure)
public void sendResponseAndComplete()
{
try
{
int code = 500;
Integer status = (Integer)_request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
if (status != null)
_request.setHandled(true);
_state.completing();
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)
{
if (x != failure)
failure.addSuppressed(x);
abort(failure);
abort(x);
}
}
@ -690,11 +651,11 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
public String toString()
{
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(),
hashCode(),
_state,
_requests,
_committed.get(),
isRequestCompleted(),
isResponseCompleted(),
_state.getState(),
@ -731,7 +692,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
public boolean onContent(HttpInput.Content content)
{
if (LOG.isDebugEnabled())
LOG.debug("{} onContent {}", this, content);
LOG.debug("onContent {} {}", this, content);
notifyRequestContent(_request, content.getByteBuffer());
return _request.getHttpInput().addContent(content);
}
@ -739,7 +700,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
public boolean onContentComplete()
{
if (LOG.isDebugEnabled())
LOG.debug("{} onContentComplete", this);
LOG.debug("onContentComplete {}", this);
notifyRequestContentEnd(_request);
return false;
}
@ -747,7 +708,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
public void onTrailers(HttpFields trailers)
{
if (LOG.isDebugEnabled())
LOG.debug("{} onTrailers {}", this, trailers);
LOG.debug("onTrailers {} {}", this, trailers);
_trailers = trailers;
notifyRequestTrailers(_request);
}
@ -755,7 +716,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
public boolean onRequestComplete()
{
if (LOG.isDebugEnabled())
LOG.debug("{} onRequestComplete", this);
LOG.debug("onRequestComplete {}", this);
boolean result = _request.getHttpInput().eof();
notifyRequestEnd(_request);
return result;
@ -775,7 +736,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
public void onCompleted()
{
if (LOG.isDebugEnabled())
LOG.debug("COMPLETE for {} written={}", getRequest().getRequestURI(), getBytesWritten());
LOG.debug("onCompleted for {} written={}", getRequest().getRequestURI(), getBytesWritten());
if (_requestLog != null)
_requestLog.log(_request, _response);
@ -798,7 +759,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
{
int status = failure.getCode();
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);
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)
{
boolean committing = _committed.compareAndSet(false, true);
boolean committing = _state.commitResponse();
if (LOG.isDebugEnabled())
LOG.debug("sendResponse info={} content={} complete={} committing={} callback={}",
@ -867,9 +828,11 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
response = _response.newResponseMetaData();
commit(response);
// Wrap the callback to process 1xx responses.
Callback committed = HttpStatus.isInformational(response.getStatus())
? new Send100Callback(callback) : new SendCallback(callback, content, true, complete);
// wrap callback to process 100 responses
final int status = response.getStatus();
final Callback committed = (status < HttpStatus.OK_200 && status >= HttpStatus.CONTINUE_100)
? new Send100Callback(callback)
: new SendCallback(callback, content, true, complete);
notifyResponseBegin(_request);
@ -916,7 +879,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
public boolean isCommitted()
{
return _committed.get();
return _state.isResponseCommitted();
}
/**
@ -932,7 +895,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
*/
public boolean isResponseCompleted()
{
return _responseCompleted.get();
return _state.isResponseCompleted();
}
public boolean isPersistent()
@ -995,8 +958,11 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
*/
public void abort(Throwable failure)
{
notifyResponseFailure(_request, failure);
_transport.abort(failure);
if (_state.abortResponse())
{
notifyResponseFailure(_request, failure);
_transport.abort(failure);
}
}
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>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()
{
_written += _length;
if (_complete)
_response.getHttpOutput().closed();
super.succeeded();
if (_commit)
notifyResponseCommit(_request);
if (_length > 0)
notifyResponseContent(_request, _content);
if (_complete)
{
_responseCompleted.set(true);
if (_complete && _state.completeResponse())
notifyResponseEnd(_request);
}
}
@Override
@ -1340,13 +1310,14 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
@Override
public void succeeded()
{
super.failed(x);
_response.getHttpOutput().closed();
super.failed(x);
}
@Override
public void failed(Throwable th)
{
_response.getHttpOutput().closed();
abort(x);
super.failed(x);
}
@ -1370,7 +1341,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
@Override
public void succeeded()
{
if (_committed.compareAndSet(true, false))
if (_state.partialResponse())
super.succeeded();
else
super.failed(new IllegalStateException());

View File

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

View File

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

View File

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

View File

@ -212,6 +212,7 @@ public class Request implements HttpServletRequest
private String _contentType;
private String _characterEncoding;
private ContextHandler.Context _context;
private ContextHandler.Context _errorContext;
private Cookies _cookies;
private DispatcherType _dispatcherType;
private int _inputState = INPUT_NONE;
@ -759,6 +760,22 @@ public class Request implements HttpServletRequest
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()
*/
@ -1983,6 +2000,7 @@ public class Request implements HttpServletRequest
else
{
_context = context;
_errorContext = context;
}
}

View File

@ -18,19 +18,19 @@
package org.eclipse.jetty.server;
import java.io.Closeable;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.channels.IllegalSelectorException;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Iterator;
import java.util.ListIterator;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletResponse;
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.PreEncodedHttpField;
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.util.Callback;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.log.Log;
@ -379,66 +378,35 @@ public class Response implements HttpServletResponse
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
public void sendError(int code, String message) throws IOException
{
if (isIncluding())
return;
if (isCommitted())
{
if (LOG.isDebugEnabled())
LOG.debug("Aborting on sendError on committed response {} {}", code, message);
code = -1;
}
else
resetBuffer();
switch (code)
{
case -1:
_channel.abort(new IOException());
return;
case 102:
_channel.abort(new IOException(message));
break;
case HttpStatus.PROCESSING_102:
sendProcessing();
return;
break;
default:
_channel.getState().sendError(code, message);
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();
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;
_reason = null;
}
}
@ -714,6 +685,11 @@ public class Response implements HttpServletResponse
return _outputType == OutputType.STREAM;
}
public boolean isWritingOrStreaming()
{
return isWriting() || isStreaming();
}
@Override
public PrintWriter getWriter() throws IOException
{
@ -822,21 +798,15 @@ public class Response implements HttpServletResponse
public void closeOutput() throws IOException
{
switch (_outputType)
{
case WRITER:
_writer.close();
if (!_out.isClosed())
_out.close();
break;
case STREAM:
if (!_out.isClosed())
getOutputStream().close();
break;
default:
if (!_out.isClosed())
_out.close();
}
if (_outputType == OutputType.WRITER)
_writer.close();
if (!_out.isClosed())
_out.close();
}
public void closeOutput(Callback callback)
{
_out.close((_outputType == OutputType.WRITER) ? _writer : _out, callback);
}
public long getLongContentLength()
@ -1029,19 +999,20 @@ public class Response implements HttpServletResponse
@Override
public void reset()
{
reset(false);
}
public void reset(boolean preserveCookies)
{
resetForForward();
_status = 200;
_reason = null;
_out.resetBuffer();
_outputType = OutputType.NONE;
_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();
// recreate necessary connection related fields
for (String value : _channel.getRequest().getHttpFields().getCSV(HttpHeader.CONNECTION, false))
{
HttpHeaderValue cb = HttpHeaderValue.CACHE.get(value);
@ -1064,21 +1035,57 @@ public class Response implements HttpServletResponse
}
}
if (preserveCookies)
cookies.forEach(_fields::add);
else
// recreate session cookies
Request request = getHttpChannel().getRequest();
HttpSession session = request.getSession(false);
if (session != null && session.isNew())
{
Request request = getHttpChannel().getRequest();
HttpSession session = request.getSession(false);
if (session != null && session.isNew())
SessionHandler sh = request.getSessionHandler();
if (sh != null)
{
SessionHandler sh = request.getSessionHandler();
if (sh != null)
{
HttpCookie c = sh.getSessionCookie(session, request.getContextPath(), request.isSecure());
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()
{
_out.resetBuffer();
_out.reopen();
}
@Override
@ -1143,6 +1151,9 @@ public class Response implements HttpServletResponse
@Override
public boolean isCommitted()
{
// If we are in sendError state, we pretend to be committed
if (_channel.isSendError())
return true;
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()))
{
request.setHandled(true);
response.sendError(HttpStatus.BAD_REQUEST_400);
handleOptions(request, response);
if (!request.isHandled())
handle(target, request, request, response);
}
else
{
handleOptions(request, response);
if (!request.isHandled())
handle(target, request, request, response);
}
}
else
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.
*
* @throws Exception if unable to start the context
* @see org.eclipse.jetty.server.handler.ContextHandler.Context
* @see ContextHandler.Context
*/
protected void startContext() throws Exception
{
@ -1103,7 +1103,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
case UNAVAILABLE:
baseRequest.setHandled(true);
response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
return true;
return false;
default:
if ((DispatcherType.REQUEST.equals(dispatch) && baseRequest.isHandled()))
return false;
@ -1138,8 +1138,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
if (oldContext != _scontext)
{
// check the target.
if (DispatcherType.REQUEST.equals(dispatch) || DispatcherType.ASYNC.equals(dispatch) ||
DispatcherType.ERROR.equals(dispatch) && baseRequest.getHttpChannelState().isAsync())
if (DispatcherType.REQUEST.equals(dispatch) || DispatcherType.ASYNC.equals(dispatch))
{
if (_compactPath)
target = URIUtil.compactPath(target);
@ -1276,29 +1275,11 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
if (new_context)
requestInitialized(baseRequest, request);
switch (dispatch)
if (dispatch == DispatcherType.REQUEST && isProtectedTarget(target))
{
case REQUEST:
if (isProtectedTarget(target))
{
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;
baseRequest.setHandled(true);
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
nextHandle(target, baseRequest, request, response);

View File

@ -19,28 +19,29 @@
package org.eclipse.jetty.server.handler;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.List;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.QuotedQualityCSV;
import org.eclipse.jetty.io.ByteBufferOutputStream;
import org.eclipse.jetty.server.Dispatcher;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.QuotedStringTokenizer;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
@ -54,10 +55,13 @@ import org.eclipse.jetty.util.log.Logger;
*/
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);
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 _disableStacks = false;
boolean _showMessageInTitle = true;
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)
*/
@ -77,71 +94,13 @@ public class ErrorHandler extends AbstractHandler
@Override
public void doError(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
{
String method = request.getMethod();
if (!HttpMethod.GET.is(method) && !HttpMethod.POST.is(method) && !HttpMethod.HEAD.is(method))
{
baseRequest.setHandled(true);
return;
}
String cacheControl = getCacheControl();
if (cacheControl != null)
response.setHeader(HttpHeader.CACHE_CONTROL.asString(), cacheControl);
if (this instanceof ErrorPageMapper)
{
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);
String message = (String)request.getAttribute(Dispatcher.ERROR_MESSAGE);
if (message == null)
message = baseRequest.getResponse().getReason();
if (message == null)
message = HttpStatus.getMessage(response.getStatus());
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
* quality order and the method
* {@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 request The servlet request (may be wrapped)
@ -174,48 +133,10 @@ public class ErrorHandler extends AbstractHandler
for (String mimeType : acceptable)
{
generateAcceptableResponse(baseRequest, request, response, code, message, mimeType);
if (response.isCommitted() || baseRequest.getResponse().isWriting() || baseRequest.getResponse().isStreaming())
if (response.isCommitted() || baseRequest.getResponse().isWritingOrStreaming())
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
* @throws IOException if a Writer cannot be returned
*/
@Deprecated
protected Writer getAcceptableWriter(Request baseRequest, HttpServletRequest request, HttpServletResponse response)
throws IOException
{
@ -264,6 +186,139 @@ public class ErrorHandler extends AbstractHandler
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)
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("<title>Error ");
writer.write(Integer.toString(code));
if (_showMessageInTitle)
// TODO this code is duplicated in writeErrorPageMessage
String status = Integer.toString(code);
writer.write(status);
if (message != null && !message.equals(status))
{
writer.write(' ');
write(writer, message);
writer.write(StringUtil.sanitizeXmlString(message));
}
writer.write("</title>\n");
}
@ -304,7 +360,7 @@ public class ErrorHandler extends AbstractHandler
String uri = request.getRequestURI();
writeErrorPageMessage(request, writer, code, message, uri);
if (showStacks)
if (showStacks && !_disableStacks)
writeErrorPageStacks(request, writer);
Request.getBaseRequest(request).getHttpChannel().getHttpConfiguration()
@ -315,29 +371,97 @@ public class ErrorHandler extends AbstractHandler
throws IOException
{
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("</h2>\n<p>Problem accessing ");
write(writer, uri);
writer.write(". Reason:\n<pre> ");
write(writer, message);
writer.write("</pre></p>");
writer.write(' ');
writer.write(StringUtil.sanitizeXmlString(message));
writer.write("\n");
writer.printf("URI: %s%n", request.getRequestURI());
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)
throws IOException
{
Throwable th = (Throwable)request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
while (th != null)
if (_showStacks && th != null)
{
writer.write("<h3>Caused by:</h3><pre>");
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
th.printStackTrace(pw);
pw.flush();
write(writer, sw.getBuffer().toString());
PrintWriter pw = writer instanceof PrintWriter ? (PrintWriter)writer : new PrintWriter(writer);
pw.write("<pre>");
while (th != null)
{
th.printStackTrace(pw);
th = th.getCause();
}
writer.write("</pre>\n");
th = th.getCause();
}
}

View File

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

View File

@ -85,10 +85,10 @@ public abstract class AbstractHttpTest
HttpTester.parseResponse(input, response);
if (httpVersion.is("HTTP/1.1") &&
response.isComplete() &&
response.get("content-length") == null &&
response.get("transfer-encoding") == null &&
!__noBodyCodes.contains(response.getStatus()))
response.isComplete() &&
response.get("content-length") == null &&
response.get("transfer-encoding") == null &&
!__noBodyCodes.contains(response.getStatus()))
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"));
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.tools.HttpTester;
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.BeforeAll;
import org.junit.jupiter.api.Test;
@ -53,43 +52,6 @@ public class ErrorHandlerTest
server = new Server();
connector = new LocalConnector(server);
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()
{

View File

@ -190,6 +190,10 @@ public class HttpInputAsyncStateTest
__history.add("COMPLETE");
break;
case READ_REGISTER:
_state.getHttpChannel().onAsyncWaitForContent();
break;
default:
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
{
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" +
"<nimbus xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" + " xsi:noNamespaceSchemaLocation=\"nimbus.xsd\" version=\"1.0\">\n" +
"</nimbus>";
"<nimbus xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" +
" 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 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.
private static final String FRAGMENT1 = REQUEST1.substring(0, 16);
@ -104,7 +113,7 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
" <jobId>73</jobId>\n" +
" </getJobDetails>\n" +
" </request>\n" +
"</nimbus>\n";
"</nimbus>\n";
protected static final String RESPONSE2 =
"HTTP/1.1 200 OK\n" +
"Content-Type: text/xml;charset=iso-8859-1\n" +
@ -143,9 +152,9 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
OutputStream os = client.getOutputStream();
os.write(("OPTIONS * HTTP/1.1\r\n" +
"Host: " + _serverURI.getHost() + "\r\n" +
"Connection: close\r\n" +
"\r\n").getBytes(StandardCharsets.ISO_8859_1));
"Host: " + _serverURI.getHost() + "\r\n" +
"Connection: close\r\n" +
"\r\n").getBytes(StandardCharsets.ISO_8859_1));
os.flush();
// 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("Allow: GET"));
}
}
@Test
public void testGETStar() throws Exception
{
configureServer(new OptionsHandler());
try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
{
OutputStream os = client.getOutputStream();
os.write(("GET * HTTP/1.1\r\n" +
"Host: " + _serverURI.getHost() + "\r\n" +
"Connection: close\r\n" +
"\r\n").getBytes(StandardCharsets.ISO_8859_1));
"Host: " + _serverURI.getHost() + "\r\n" +
"Connection: close\r\n" +
"\r\n").getBytes(StandardCharsets.ISO_8859_1));
os.flush();
// Read the response.
@ -434,13 +448,13 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
{
OutputStream os = client.getOutputStream();
os.write(("GET /R2 HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"Transfer-Encoding: chunked\r\n" +
"Content-Type: text/plain\r\n" +
"Connection: close\r\n" +
"\r\n").getBytes());
os.flush();
Thread.sleep(1000);
os.write(("5").getBytes());
@ -450,7 +464,6 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
Thread.sleep(1000);
os.write(("ABCDE\r\n" +
"0;\r\n\r\n").getBytes());
os.flush();
// Read the response.
@ -467,6 +480,7 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
{
OutputStream os = client.getOutputStream();
//@checkstyle-disable-check : IllegalTokenText
os.write(("GET /R2 HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"Content-Length: 5\r\n" +
@ -475,6 +489,7 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
"\r\n" +
"ABCDE\r\n" +
"\r\n"
//@checkstyle-enable-check : IllegalTokenText
).getBytes());
os.flush();
@ -893,7 +908,7 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
if (line.length() == 0)
break;
int len = line.length();
assertEquals(Integer.parseInt(chunk, 16), len);
assertEquals(Integer.valueOf(chunk, 16).intValue(), len);
if (max < len)
max = len;
}
@ -1547,12 +1562,11 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
{
try
{
byte[] bytes = (
"GET / HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"Content-Length: " + cl + "\r\n" +
"\r\n" +
content).getBytes(StandardCharsets.ISO_8859_1);
byte[] bytes = ("GET / HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"Content-Length: " + cl + "\r\n" +
"\r\n" +
content).getBytes(StandardCharsets.ISO_8859_1);
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;
public class HttpServerTestFixture
{ // Useful constants
{
// Useful constants
protected static final long PAUSE = 10L;
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
{
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.session.SessionHandler;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@ -42,6 +44,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
public class LocalAsyncContextTest
{
public static final Logger LOG = Log.getLogger(LocalAsyncContextTest.class);
protected Server _server;
protected SuspendHandler _handler;
protected Connector _connector;
@ -232,6 +235,7 @@ public class LocalAsyncContextTest
private synchronized String process(String content) throws Exception
{
LOG.debug("TEST process: {}", content);
reset();
String request = "GET / HTTP/1.1\r\n" +
"Host: localhost\r\n" +
@ -305,6 +309,7 @@ public class LocalAsyncContextTest
@Override
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 (_read > 0)

View File

@ -35,6 +35,8 @@ import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.stream.Stream;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
@ -70,6 +72,8 @@ import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
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 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());
}
@Test
public void testStatusCodes() throws Exception
public static Stream<Object[]> sendErrorTestCodes()
{
Response response = getResponse();
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), 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()));
List<Object[]> data = new ArrayList<>();
data.add(new Object[]{404, null, "Not Found"});
data.add(new Object[]{500, "Database Error", "Database Error"});
data.add(new Object[]{406, "Super Nanny", "Super Nanny"});
return data.stream();
}
@Test
public void testStatusCodesNoErrorHandler() throws Exception
@ParameterizedTest
@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));
Response response = getResponse();
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());
testStatusCodes(code, message, expectedMessage);
}
@Test
@ -900,7 +875,7 @@ public class ResponseTest
assertFalse(response.isCommitted());
assertFalse(writer.checkError());
writer.print("");
assertFalse(writer.checkError());
// assertFalse(writer.checkError()); TODO check this
assertTrue(response.isCommitted());
}
@ -1034,7 +1009,7 @@ public class ResponseTest
}
@Test
public void testCookiesWithReset()
public void testResetContent() throws Exception
{
Response response = getResponse();
@ -1050,9 +1025,27 @@ public class ResponseTest
cookie2.setPath("/path");
response.addCookie(cookie2);
//keep the cookies
response.reset(true);
response.setContentType("some/type");
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");
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.Handler;
import org.eclipse.jetty.server.HttpChannel;
import org.eclipse.jetty.server.HttpChannelState;
import org.eclipse.jetty.server.LocalConnector;
import org.eclipse.jetty.server.Request;
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.util.BlockingArrayQueue;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.StacklessLogging;
import org.junit.jupiter.api.AfterEach;
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 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 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"});
});
@ -514,7 +516,9 @@ public class NcsaRequestLogTest
startServer();
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);
}
@ -574,6 +578,10 @@ public class NcsaRequestLogTest
{
try
{
while (baseRequest.getHttpChannel().getState().getState() != HttpChannelState.State.WAITING)
{
Thread.sleep(10);
}
baseRequest.setHandled(false);
testHandler.handle(target, baseRequest, request, response);
if (!baseRequest.isHandled())
@ -581,18 +589,21 @@ public class NcsaRequestLogTest
}
catch (BadMessageException bad)
{
response.sendError(bad.getCode());
response.sendError(bad.getCode(), bad.getReason());
}
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))
{
baseRequest.setHandled(true);
response.sendError(404);
return;
}

View File

@ -20,7 +20,6 @@ package org.eclipse.jetty.server.ssl;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
@ -43,6 +42,7 @@ 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.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
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.SslConnectionFactory;
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.Utf8StringBuilder;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.hamcrest.Matchers;
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.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.assertTrue;
public class SniSslConnectionFactoryTest
{
@ -81,23 +80,23 @@ public class SniSslConnectionFactoryTest
{
_server = new Server();
HttpConfiguration http_config = new HttpConfiguration();
http_config.setSecureScheme("https");
http_config.setSecurePort(8443);
http_config.setOutputBufferSize(32768);
_httpsConfiguration = new HttpConfiguration(http_config);
HttpConfiguration httpConfig = new HttpConfiguration();
httpConfig.setSecureScheme("https");
httpConfig.setSecurePort(8443);
httpConfig.setOutputBufferSize(32768);
_httpsConfiguration = new HttpConfiguration(httpConfig);
SecureRequestCustomizer src = new SecureRequestCustomizer();
src.setSniHostCheck(true);
_httpsConfiguration.addCustomizer(src);
_httpsConfiguration.addCustomizer((connector, httpConfig, request) ->
_httpsConfiguration.addCustomizer((connector, hc, request) ->
{
EndPoint endp = request.getHttpChannel().getEndPoint();
if (endp instanceof SslConnection.DecryptedEndPoint)
{
try
{
SslConnection.DecryptedEndPoint ssl_endp = (SslConnection.DecryptedEndPoint)endp;
SslConnection sslConnection = ssl_endp.getSslConnection();
SslConnection.DecryptedEndPoint sslEndp = (SslConnection.DecryptedEndPoint)endp;
SslConnection sslConnection = sslEndp.getSslConnection();
SSLEngine sslEngine = sslConnection.getSSLEngine();
SSLSession session = sslEngine.getSession();
for (Certificate c : session.getLocalCertificates())
@ -224,6 +223,7 @@ public class SniSslConnectionFactoryTest
public void testSameConnectionRequestsForManyDomains() throws Exception
{
start("src/test/resources/keystore_sni.p12");
_server.setErrorHandler(new ErrorHandler());
SslContextFactory clientContextFactory = new SslContextFactory.Client(true);
clientContextFactory.start();
@ -246,8 +246,8 @@ public class SniSslConnectionFactoryTest
output.flush();
InputStream input = sslSocket.getInputStream();
String response = response(input);
assertTrue(response.startsWith("HTTP/1.1 200 "));
HttpTester.Response response = HttpTester.parseResponse(input);
assertThat(response.getStatus(), is(200));
// Same socket, send a request for a different domain but same alias.
request =
@ -256,9 +256,8 @@ public class SniSslConnectionFactoryTest
"\r\n";
output.write(request.getBytes(StandardCharsets.UTF_8));
output.flush();
response = response(input);
assertTrue(response.startsWith("HTTP/1.1 200 "));
response = HttpTester.parseResponse(input);
assertThat(response.getStatus(), is(200));
// Same socket, send a request for a different domain but different alias.
request =
@ -268,10 +267,9 @@ public class SniSslConnectionFactoryTest
output.write(request.getBytes(StandardCharsets.UTF_8));
output.flush();
response = response(input);
String body = IO.toString(input);
assertThat(response, startsWith("HTTP/1.1 400 "));
assertThat(body, containsString("Host does not match SNI"));
response = HttpTester.parseResponse(input);
assertThat(response.getStatus(), is(400));
assertThat(response.getContent(), containsString("Host does not match SNI"));
}
finally
{
@ -304,8 +302,8 @@ public class SniSslConnectionFactoryTest
output.flush();
InputStream input = sslSocket.getInputStream();
String response = response(input);
assertTrue(response.startsWith("HTTP/1.1 200 "));
HttpTester.Response response = HttpTester.parseResponse(input);
assertThat(response.getStatus(), is(200));
// Now, on the same socket, send a request for a different valid domain.
request =
@ -315,8 +313,8 @@ public class SniSslConnectionFactoryTest
output.write(request.getBytes(StandardCharsets.UTF_8));
output.flush();
response = response(input);
assertTrue(response.startsWith("HTTP/1.1 200 "));
response = HttpTester.parseResponse(input);
assertThat(response.getStatus(), is(200));
// Now make a request for an invalid domain for this connection.
request =
@ -326,10 +324,9 @@ public class SniSslConnectionFactoryTest
output.write(request.getBytes(StandardCharsets.UTF_8));
output.flush();
response = response(input);
assertTrue(response.startsWith("HTTP/1.1 400 "));
String body = IO.toString(input);
assertThat(body, Matchers.containsString("Host does not match SNI"));
response = HttpTester.parseResponse(input);
assertThat(response.getStatus(), is(400));
assertThat(response.getContent(), containsString("Host does not match SNI"));
}
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
{
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.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
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("PathInfo= /500"));
assertThat("error servlet", responseBody, containsString("EXCEPTION: java.lang.RuntimeException: TEST"));
assertThat("error servlet", responseBody, not(containsString("EXCEPTION: ")));
}
private class DispatchingRunnable implements Runnable
@ -552,7 +553,7 @@ public class AsyncContextTest
@Override
public void onTimeout(AsyncEvent event) throws IOException
{
throw new RuntimeException("TEST");
throw new RuntimeException("BAD EXPIRE");
}
@Override

View File

@ -32,6 +32,7 @@ import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.tools.HttpTester;
import org.eclipse.jetty.io.QuietException;
import org.eclipse.jetty.server.LocalConnector;
import org.eclipse.jetty.server.QuietServletException;
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.Matchers.containsString;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class AsyncListenerTest
@ -140,7 +142,7 @@ public class AsyncListenerTest
test_StartAsync_Throw_OnError(event ->
{
HttpServletResponse response = (HttpServletResponse)event.getAsyncContext().getResponse();
response.sendError(HttpStatus.BAD_GATEWAY_502);
response.sendError(HttpStatus.BAD_GATEWAY_502, "Message!!!");
});
String httpResponse = connector.getResponse(
"GET /ctx/path HTTP/1.1\r\n" +
@ -148,7 +150,8 @@ public class AsyncListenerTest
"Connection: close\r\n" +
"\r\n");
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
@ -191,7 +194,7 @@ public class AsyncListenerTest
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
AsyncContext asyncContext = request.startAsync();
asyncContext.setTimeout(0);
asyncContext.setTimeout(10000);
asyncContext.addListener(new AsyncListenerAdapter()
{
@Override
@ -268,7 +271,8 @@ public class AsyncListenerTest
"Connection: close\r\n" +
"\r\n");
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
@ -292,6 +296,7 @@ public class AsyncListenerTest
{
HttpServletResponse response = (HttpServletResponse)event.getAsyncContext().getResponse();
response.sendError(HttpStatus.BAD_GATEWAY_502);
event.getAsyncContext().complete();
});
String httpResponse = connector.getResponse(
"GET / HTTP/1.1\r\n" +
@ -384,7 +389,7 @@ public class AsyncListenerTest
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
AsyncContext asyncContext = request.startAsync();
asyncContext.setTimeout(0);
asyncContext.setTimeout(10000);
asyncContext.addListener(new AsyncListenerAdapter()
{
@Override
@ -447,7 +452,7 @@ public class AsyncListenerTest
}
// 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.Server;
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.Logger;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
@ -305,6 +306,7 @@ public class AsyncServletIOTest
request.append(s).append("w=").append(w);
s = '&';
}
LOG.debug("process {} {}", request.toString(), BufferUtil.toDetailString(BufferUtil.toBuffer(content)));
request.append(" HTTP/1.1\r\n")
.append("Host: localhost\r\n")
@ -816,13 +818,15 @@ public class AsyncServletIOTest
// wait until server is ready
_servletStolenAsyncRead.ready.await();
final CountDownLatch wait = new CountDownLatch(1);
final CountDownLatch held = new CountDownLatch(1);
// Stop any dispatches until we want them
UnaryOperator<Runnable> old = _wQTP.wrapper.getAndSet(r ->
() ->
{
try
{
held.countDown();
wait.await();
r.run();
}
@ -836,7 +840,9 @@ public class AsyncServletIOTest
// We are an unrelated thread, let's mess with the input stream
ServletInputStream sin = _servletStolenAsyncRead.listener.in;
sin.setReadListener(_servletStolenAsyncRead.listener);
// thread should be dispatched to handle, but held by our wQTP wait.
assertTrue(held.await(10, TimeUnit.SECONDS));
// Let's steal our read
assertTrue(sin.isReady());

View File

@ -265,7 +265,6 @@ public class AsyncServletTest
"start",
"onTimeout",
"error",
"onError",
"ERROR /ctx/error/custom",
"!initial",
"onComplete"));
@ -273,44 +272,6 @@ public class AsyncServletTest
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
public void testStartOnTimeoutComplete() throws Exception
{
@ -526,8 +487,10 @@ public class AsyncServletTest
"onStartAsync",
"start",
"onTimeout",
"ERROR /ctx/path/error",
"!initial",
"onComplete")); // Error Page Loop!
assertContains("HTTP ERROR 500", response);
assertContains("AsyncContext timeout", response);
}
@Test

View File

@ -85,7 +85,8 @@ public class CustomRequestLogTest
_connector.getResponse("GET /context/servlet/info HTTP/1.0\n\n");
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

View File

@ -20,8 +20,22 @@ package org.eclipse.jetty.servlet;
import java.io.IOException;
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.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@ -29,8 +43,12 @@ import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.server.Dispatcher;
import org.eclipse.jetty.server.HttpChannel;
import org.eclipse.jetty.server.HttpChannelState;
import org.eclipse.jetty.server.LocalConnector;
import org.eclipse.jetty.server.Request;
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.hamcrest.Matchers;
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.Matchers.containsString;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class ErrorPageTest
{
private Server _server;
private LocalConnector _connector;
private StacklessLogging _stackless;
private static CountDownLatch __asyncSendErrorCompleted;
private ErrorPageErrorHandler _errorPageErrorHandler;
@BeforeEach
public void init() throws Exception
{
_server = new Server();
_connector = new LocalConnector(_server);
_server.addConnector(_connector);
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS);
_server.addConnector(_connector);
_server.setHandler(context);
context.setContextPath("/");
context.addFilter(SingleDispatchFilter.class, "/*", EnumSet.allOf(DispatcherType.class));
context.addServlet(DefaultServlet.class, "/");
context.addServlet(FailServlet.class, "/fail/*");
context.addServlet(FailClosedServlet.class, "/fail-closed/*");
context.addServlet(ErrorServlet.class, "/error/*");
context.addServlet(AppServlet.class, "/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();
context.setErrorHandler(error);
error.addErrorPage(599, "/error/599");
error.addErrorPage(400, "/error/400");
HandlerWrapper noopHandler = new HandlerWrapper()
{
@Override
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(IllegalStateException.class.getCanonicalName(), "/error/TestException");
error.addErrorPage(BadMessageException.class, "/error/BadMessageException");
error.addErrorPage(ErrorPageErrorHandler.GLOBAL_ERROR_PAGE, "/error/GlobalErrorPage");
_errorPageErrorHandler.addErrorPage(IllegalStateException.class.getCanonicalName(), "/error/TestException");
_errorPageErrorHandler.addErrorPage(BadMessageException.class, "/error/BadMessageException");
_errorPageErrorHandler.addErrorPage(ErrorPageErrorHandler.GLOBAL_ERROR_PAGE, "/error/GlobalErrorPage");
_server.start();
_stackless = new StacklessLogging(ServletHandler.class);
__asyncSendErrorCompleted = new CountDownLatch(1);
}
@AfterEach
@ -87,6 +133,101 @@ public class ErrorPageTest
_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
public void testSendErrorClosedResponse() throws Exception
{
@ -167,7 +308,7 @@ public class ErrorPageTest
try (StacklessLogging ignore = new StacklessLogging(Dispatcher.class))
{
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_MESSAGE: Bad query encoding"));
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
{
@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
{
@Override
@ -225,16 +560,51 @@ public class ErrorPageTest
}
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
{
@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();
writer.println("DISPATCH: " + request.getDispatcherType().name());
writer.println("ERROR_PAGE: " + request.getPathInfo());
@ -247,4 +617,55 @@ public class ErrorPageTest
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);
String ifnm = req.getHeader("If-None-Match");
if (ifnm != null && ifnm.equals(__contentETag))
response.sendError(304);
{
response.setStatus(304);
response.flushBuffer();
}
else
{
PrintWriter writer = response.getWriter();

View File

@ -501,7 +501,16 @@ public class DoSFilter implements Filter
{
if (LOG.isDebugEnabled())
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)
{

View File

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

View File

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

View File

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