blob: 2b7f25209a0060809846eb95571ca2f384f4c9f8 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.catalina.core;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.servlet.AsyncContext;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
import javax.servlet.DispatcherType;
import javax.servlet.GenericServlet;
import javax.servlet.RequestDispatcher;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.ServletRequestWrapper;
import javax.servlet.ServletResponse;
import javax.servlet.ServletResponseWrapper;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import org.junit.Assert;
import org.junit.Test;
import org.apache.catalina.Context;
import org.apache.catalina.Wrapper;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.startup.TomcatBaseTest;
import org.apache.catalina.valves.TesterAccessLogValve;
import org.apache.tomcat.unittest.TesterContext;
import org.apache.tomcat.util.buf.ByteChunk;
import org.apache.tomcat.util.buf.UDecoder;
import org.apache.tomcat.util.descriptor.web.ErrorPage;
import org.easymock.EasyMock;
public class TestAsyncContextImpl extends TomcatBaseTest {
// Time for a request to process (need to allow for threads to start etc.)
private static final long REQUEST_TIME = 1500;
// Timeout thread (where used) checks for timeout every second
private static final long TIMEOUT_MARGIN = 1000;
// Default timeout for these tests
private static final long TIMEOUT = 5000;
private static StringBuilder tracker;
public static synchronized void resetTracker() {
tracker = new StringBuilder();
}
public static synchronized void track(String trace) {
tracker.append(trace);
}
public static synchronized String getTrack() {
return tracker.toString();
}
@Test
public void testBug49528() throws Exception {
// Setup Tomcat instance
Tomcat tomcat = getTomcatInstance();
// No file system docBase required
Context ctx = tomcat.addContext("", null);
Bug49528Servlet servlet = new Bug49528Servlet();
Wrapper wrapper = Tomcat.addServlet(ctx, "servlet", servlet);
wrapper.setAsyncSupported(true);
ctx.addServletMapping("/", "servlet");
TesterAccessLogValve alv = new TesterAccessLogValve();
ctx.getPipeline().addValve(alv);
tomcat.start();
// Call the servlet once
ByteChunk bc = getUrl("http://localhost:" + getPort() + "/");
assertEquals("OK", bc.toString());
// Give the async thread a chance to finish (but not too long)
int counter = 0;
while (!servlet.isDone() && counter < 10) {
Thread.sleep(1000);
counter++;
}
assertEquals("1false2true3true4true5false", servlet.getResult());
// Check the access log
alv.validateAccessLog(1, 200, Bug49528Servlet.THREAD_SLEEP_TIME,
Bug49528Servlet.THREAD_SLEEP_TIME + REQUEST_TIME);
}
@Test
public void testBug49567() throws Exception {
// Setup Tomcat instance
Tomcat tomcat = getTomcatInstance();
// No file system docBase required
Context ctx = tomcat.addContext("", null);
Bug49567Servlet servlet = new Bug49567Servlet();
Wrapper wrapper = Tomcat.addServlet(ctx, "servlet", servlet);
wrapper.setAsyncSupported(true);
ctx.addServletMapping("/", "servlet");
TesterAccessLogValve alv = new TesterAccessLogValve();
ctx.getPipeline().addValve(alv);
tomcat.start();
// Call the servlet once
ByteChunk bc = getUrl("http://localhost:" + getPort() + "/");
assertEquals("OK", bc.toString());
// Give the async thread a chance to finish (but not too long)
int counter = 0;
while (!servlet.isDone() && counter < 10) {
Thread.sleep(1000);
counter++;
}
assertEquals("1false2true3true4true5false", servlet.getResult());
// Check the access log
alv.validateAccessLog(1, 200, Bug49567Servlet.THREAD_SLEEP_TIME,
Bug49567Servlet.THREAD_SLEEP_TIME + REQUEST_TIME);
}
@Test
public void testAsyncStartNoComplete() throws Exception {
resetTracker();
// Setup Tomcat instance
Tomcat tomcat = getTomcatInstance();
// Minimise pauses during test
tomcat.getConnector().setAttribute(
"connectionTimeout", Integer.valueOf(3000));
// No file system docBase required
Context ctx = tomcat.addContext("", null);
AsyncStartNoCompleteServlet servlet =
new AsyncStartNoCompleteServlet();
Wrapper wrapper = Tomcat.addServlet(ctx, "servlet", servlet);
wrapper.setAsyncSupported(true);
ctx.addServletMapping("/", "servlet");
TesterAccessLogValve alv = new TesterAccessLogValve();
ctx.getPipeline().addValve(alv);
tomcat.start();
// Call the servlet the first time
getUrl("http://localhost:" + getPort() + "/?echo=run1");
Assert.assertEquals("OK-run1", getTrack());
resetTracker();
// Call the servlet the second time with a request parameter
getUrl("http://localhost:" + getPort() + "/?echo=run2");
Assert.assertEquals("OK-run2", getTrack());
// Request may complete before listener has finished processing so wait
// up to 5 seconds for the right response
// Check the access log
alv.validateAccessLog(2, 500,
AsyncStartNoCompleteServlet.ASYNC_TIMEOUT,
AsyncStartNoCompleteServlet.ASYNC_TIMEOUT + TIMEOUT_MARGIN +
REQUEST_TIME);
}
@Test
public void testAsyncStartWithComplete() throws Exception {
// Setup Tomcat instance
Tomcat tomcat = getTomcatInstance();
// No file system docBase required
Context ctx = tomcat.addContext("", null);
AsyncStartWithCompleteServlet servlet =
new AsyncStartWithCompleteServlet();
Wrapper wrapper = Tomcat.addServlet(ctx, "servlet", servlet);
wrapper.setAsyncSupported(true);
ctx.addServletMapping("/", "servlet");
TesterAccessLogValve alv = new TesterAccessLogValve();
ctx.getPipeline().addValve(alv);
tomcat.start();
// Call the servlet once
ByteChunk bc = new ByteChunk();
Map<String,List<String>> headers = new HashMap<>();
getUrl("http://localhost:" + getPort() + "/", bc, headers);
assertEquals("OK", bc.toString());
List<String> contentLength = headers.get("Content-Length");
Assert.assertNotNull(contentLength);
Assert.assertEquals(1, contentLength.size());
Assert.assertEquals("2", contentLength.get(0));
// Check the access log
alv.validateAccessLog(1, 200, 0, REQUEST_TIME);
}
/*
* NOTE: This servlet is only intended to be used in single-threaded tests.
*/
private static class Bug49528Servlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private volatile boolean done = false;
private StringBuilder result;
public static final long THREAD_SLEEP_TIME = 1000;
public String getResult() {
return result.toString();
}
public boolean isDone() {
return done;
}
@Override
protected void doGet(final HttpServletRequest req,
final HttpServletResponse resp)
throws ServletException, IOException {
result = new StringBuilder();
result.append('1');
result.append(req.isAsyncStarted());
req.startAsync().setTimeout(10000);
result.append('2');
result.append(req.isAsyncStarted());
req.getAsyncContext().start(new Runnable() {
@Override
public void run() {
try {
result.append('3');
result.append(req.isAsyncStarted());
Thread.sleep(THREAD_SLEEP_TIME);
result.append('4');
result.append(req.isAsyncStarted());
resp.setContentType("text/plain");
resp.getWriter().print("OK");
req.getAsyncContext().complete();
result.append('5');
result.append(req.isAsyncStarted());
done = true;
} catch (InterruptedException e) {
result.append(e);
} catch (IOException e) {
result.append(e);
}
}
});
// Pointless method call so there is somewhere to put a break point
// when debugging
req.getMethod();
}
}
/*
* NOTE: This servlet is only intended to be used in single-threaded tests.
*/
private static class Bug49567Servlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private volatile boolean done = false;
private StringBuilder result;
public static final long THREAD_SLEEP_TIME = 1000;
public String getResult() {
return result.toString();
}
public boolean isDone() {
return done;
}
@Override
protected void doGet(final HttpServletRequest req,
final HttpServletResponse resp)
throws ServletException, IOException {
result = new StringBuilder();
result.append('1');
result.append(req.isAsyncStarted());
req.startAsync();
result.append('2');
result.append(req.isAsyncStarted());
req.getAsyncContext().start(new Runnable() {
@Override
public void run() {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
result.append('3');
result.append(req.isAsyncStarted());
Thread.sleep(THREAD_SLEEP_TIME);
result.append('4');
result.append(req.isAsyncStarted());
resp.setContentType("text/plain");
resp.getWriter().print("OK");
req.getAsyncContext().complete();
result.append('5');
result.append(req.isAsyncStarted());
done = true;
} catch (InterruptedException e) {
result.append(e);
} catch (IOException e) {
result.append(e);
}
}
});
t.start();
}
});
// Pointless method call so there is somewhere to put a break point
// when debugging
req.getMethod();
}
}
private static class AsyncStartNoCompleteServlet extends HttpServlet {
public static final long ASYNC_TIMEOUT = 1000;
private static final long serialVersionUID = 1L;
@Override
protected void doGet(final HttpServletRequest req,
final HttpServletResponse resp)
throws ServletException, IOException {
String echo = req.getParameter("echo");
AsyncContext actxt = req.startAsync();
TestAsyncContextImpl.track("OK");
if (echo != null) {
TestAsyncContextImpl.track("-" + echo);
}
// Speed up the test by reducing the timeout
actxt.setTimeout(ASYNC_TIMEOUT);
}
}
private static class AsyncStartWithCompleteServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(final HttpServletRequest req,
final HttpServletResponse resp)
throws ServletException, IOException {
AsyncContext actxt = req.startAsync();
actxt.setTimeout(3000);
resp.setContentType("text/plain");
resp.getWriter().print("OK");
actxt.complete();
}
}
@Test
public void testTimeoutListenerCompleteNoDispatch() throws Exception {
// Should work
doTestTimeout(Boolean.TRUE, null);
}
@Test
public void testTimeoutListenerNoCompleteNoDispatch() throws Exception {
// Should trigger an error - must do one or other
doTestTimeout(Boolean.FALSE, null);
}
@Test
public void testTimeoutListenerCompleteNonAsyncDispatch() throws Exception {
// Should trigger an error - can't do both
doTestTimeout(Boolean.TRUE, Boolean.FALSE);
}
@Test
public void testTimeoutListenerNoCompleteNonAsyncDispatch()
throws Exception {
// Should work
doTestTimeout(Boolean.FALSE, Boolean.FALSE);
}
@Test
public void testTimeoutListenerCompleteAsyncDispatch() throws Exception {
// Should trigger an error - can't do both
doTestTimeout(Boolean.TRUE, Boolean.TRUE);
}
@Test
public void testTimeoutListenerNoCompleteAsyncDispatch() throws Exception {
// Should work
doTestTimeout(Boolean.FALSE, Boolean.TRUE);
}
@Test
public void testTimeoutNoListener() throws Exception {
// Should work
doTestTimeout(null, null);
}
private void doTestTimeout(Boolean completeOnTimeout, Boolean asyncDispatch)
throws Exception {
resetTracker();
String dispatchUrl = null;
if (asyncDispatch != null) {
if (asyncDispatch.booleanValue()) {
dispatchUrl = "/async";
} else {
dispatchUrl = "/nonasync";
}
}
// Setup Tomcat instance
Tomcat tomcat = getTomcatInstance();
// No file system docBase required
Context ctx = tomcat.addContext("", null);
TimeoutServlet timeout = new TimeoutServlet(completeOnTimeout, dispatchUrl);
Wrapper wrapper = Tomcat.addServlet(ctx, "time", timeout);
wrapper.setAsyncSupported(true);
ctx.addServletMapping("/start", "time");
if (asyncDispatch != null) {
if (asyncDispatch.booleanValue()) {
AsyncStartRunnable asyncStartRunnable =
new AsyncStartRunnable();
Wrapper async =
Tomcat.addServlet(ctx, "async", asyncStartRunnable);
async.setAsyncSupported(true);
ctx.addServletMapping(dispatchUrl, "async");
} else {
NonAsyncServlet nonAsync = new NonAsyncServlet();
Tomcat.addServlet(ctx, "nonasync", nonAsync);
ctx.addServletMapping(dispatchUrl, "nonasync");
}
}
ctx.addApplicationListener(TrackingRequestListener.class.getName());
TesterAccessLogValve alv = new TesterAccessLogValve();
ctx.getPipeline().addValve(alv);
TesterAccessLogValve alvGlobal = new TesterAccessLogValve();
tomcat.getHost().getPipeline().addValve(alvGlobal);
tomcat.start();
try {
getUrl("http://localhost:" + getPort() + "/start");
} catch (IOException ioe) {
// Ignore - expected for some error conditions
}
StringBuilder expected = new StringBuilder("requestInitialized-");
expected.append("TimeoutServletGet-");
if (completeOnTimeout == null) {
expected.append("requestDestroyed");
} else if (completeOnTimeout.booleanValue()) {
expected.append("onTimeout-");
expected.append("onComplete-");
expected.append("requestDestroyed");
} else {
expected.append("onTimeout-");
if (asyncDispatch != null) {
if (asyncDispatch.booleanValue()) {
expected.append("onStartAsync-Runnable-");
} else {
expected.append("NonAsyncServletGet-");
}
}
expected.append("onComplete-");
expected.append("requestDestroyed");
}
// Request may complete before listener has finished processing so wait
// up to 5 seconds for the right response
String expectedTrack = expected.toString();
int count = 0;
while (!expectedTrack.equals(getTrack()) && count < 100) {
Thread.sleep(50);
count ++;
}
assertEquals(expectedTrack, getTrack());
// Check the access log
if (completeOnTimeout == null ||
(!completeOnTimeout.booleanValue() && asyncDispatch == null)) {
alvGlobal.validateAccessLog(1, 500, TimeoutServlet.ASYNC_TIMEOUT,
TimeoutServlet.ASYNC_TIMEOUT + TIMEOUT_MARGIN +
REQUEST_TIME);
alv.validateAccessLog(1, 500, TimeoutServlet.ASYNC_TIMEOUT,
TimeoutServlet.ASYNC_TIMEOUT + TIMEOUT_MARGIN +
REQUEST_TIME);
} else {
long timeoutDelay = TimeoutServlet.ASYNC_TIMEOUT;
if (asyncDispatch != null && asyncDispatch.booleanValue() &&
!completeOnTimeout.booleanValue()) {
// The async dispatch includes a sleep
timeoutDelay += AsyncStartRunnable.THREAD_SLEEP_TIME;
}
alvGlobal.validateAccessLog(1, 200, timeoutDelay,
timeoutDelay + TIMEOUT_MARGIN + REQUEST_TIME);
alv.validateAccessLog(1, 200, timeoutDelay,
timeoutDelay + TIMEOUT_MARGIN + REQUEST_TIME);
}
}
private static class TimeoutServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private final Boolean completeOnTimeout;
private final String dispatchUrl;
public static final long ASYNC_TIMEOUT = 100;
public TimeoutServlet(Boolean completeOnTimeout, String dispatchUrl) {
this.completeOnTimeout = completeOnTimeout;
this.dispatchUrl = dispatchUrl;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
if (req.isAsyncSupported()) {
TestAsyncContextImpl.track("TimeoutServletGet-");
final AsyncContext ac = req.startAsync();
ac.setTimeout(ASYNC_TIMEOUT);
if (completeOnTimeout != null) {
ac.addListener(new TrackingListener(false,
completeOnTimeout.booleanValue(), dispatchUrl));
}
} else {
resp.getWriter().print("FAIL: Async unsupported");
}
}
}
@Test
public void testDispatchSingle() throws Exception {
doTestDispatch(1, false);
}
@Test
public void testDispatchDouble() throws Exception {
doTestDispatch(2, false);
}
@Test
public void testDispatchMultiple() throws Exception {
doTestDispatch(5, false);
}
@Test
public void testDispatchWithThreadSingle() throws Exception {
doTestDispatch(1, true);
}
@Test
public void testDispatchWithThreadDouble() throws Exception {
doTestDispatch(2, true);
}
@Test
public void testDispatchWithThreadMultiple() throws Exception {
doTestDispatch(5, true);
}
private void doTestDispatch(int iter, boolean useThread) throws Exception {
resetTracker();
// Setup Tomcat instance
Tomcat tomcat = getTomcatInstance();
// No file system docBase required
Context ctx = tomcat.addContext("", null);
DispatchingServlet dispatch = new DispatchingServlet(false, false);
Wrapper wrapper = Tomcat.addServlet(ctx, "dispatch", dispatch);
wrapper.setAsyncSupported(true);
ctx.addServletMapping("/stage1", "dispatch");
NonAsyncServlet nonasync = new NonAsyncServlet();
Wrapper wrapper2 = Tomcat.addServlet(ctx, "nonasync", nonasync);
wrapper2.setAsyncSupported(true);
ctx.addServletMapping("/stage2", "nonasync");
ctx.addApplicationListener(TrackingRequestListener.class.getName());
TesterAccessLogValve alv = new TesterAccessLogValve();
ctx.getPipeline().addValve(alv);
tomcat.start();
StringBuilder url = new StringBuilder(48);
url.append("http://localhost:");
url.append(getPort());
url.append("/stage1?iter=");
url.append(iter);
if (useThread) {
url.append("&useThread=y");
}
getUrl(url.toString());
StringBuilder expected = new StringBuilder("requestInitialized-");
int loop = iter;
while (loop > 0) {
expected.append("DispatchingServletGet-");
loop--;
}
expected.append("NonAsyncServletGet-");
expected.append("requestDestroyed");
// Request may complete before listener has finished processing so wait
// up to 5 seconds for the right response
String expectedTrack = expected.toString();
int count = 0;
while (!expectedTrack.equals(getTrack()) && count < 100) {
Thread.sleep(50);
count ++;
}
assertEquals(expectedTrack, getTrack());
// Check the access log
alv.validateAccessLog(1, 200, 0, REQUEST_TIME);
}
private static class DispatchingServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private static final String ITER_PARAM = "iter";
private static final String DISPATCH_CHECK = "check";
private boolean addTrackingListener = false;
private boolean completeOnError = false;
public DispatchingServlet(boolean addTrackingListener,
boolean completeOnError) {
this.addTrackingListener = addTrackingListener;
this.completeOnError = completeOnError;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
if ("y".equals(req.getParameter(DISPATCH_CHECK))) {
if (req.getDispatcherType() != DispatcherType.ASYNC) {
track("WrongDispatcherType-");
}
}
track("DispatchingServletGet-");
final int iter = Integer.parseInt(req.getParameter(ITER_PARAM)) - 1;
final AsyncContext ctxt = req.startAsync();
if (addTrackingListener) {
TrackingListener listener =
new TrackingListener(completeOnError, true, null);
ctxt.addListener(listener);
}
Runnable run = new Runnable() {
@Override
public void run() {
if (iter > 0) {
ctxt.dispatch("/stage1?" + ITER_PARAM + "=" + iter +
"&" + DISPATCH_CHECK + "=y");
} else {
ctxt.dispatch("/stage2");
}
}
};
if ("y".equals(req.getParameter("useThread"))) {
new Thread(run).start();
} else {
run.run();
}
}
}
private static class NonAsyncServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
TestAsyncContextImpl.track("NonAsyncServletGet-");
}
}
@Test
public void testListeners() throws Exception {
resetTracker();
// Setup Tomcat instance
Tomcat tomcat = getTomcatInstance();
// No file system docBase required
Context ctx = tomcat.addContext("", null);
TrackingServlet tracking = new TrackingServlet();
Wrapper wrapper = Tomcat.addServlet(ctx, "tracking", tracking);
wrapper.setAsyncSupported(true);
ctx.addServletMapping("/stage1", "tracking");
TimeoutServlet timeout = new TimeoutServlet(Boolean.TRUE, null);
Wrapper wrapper2 = Tomcat.addServlet(ctx, "timeout", timeout);
wrapper2.setAsyncSupported(true);
ctx.addServletMapping("/stage2", "timeout");
TesterAccessLogValve alv = new TesterAccessLogValve();
ctx.getPipeline().addValve(alv);
tomcat.start();
StringBuilder url = new StringBuilder(48);
url.append("http://localhost:");
url.append(getPort());
url.append("/stage1");
getUrl(url.toString());
// Request may complete before listener has finished processing so wait
// up to 5 seconds for the right response
String expectedTrack = "DispatchingServletGet-DispatchingServletGet-" +
"onStartAsync-TimeoutServletGet-onStartAsync-onTimeout-" +
"onComplete-";
int count = 0;
while (!expectedTrack.equals(getTrack()) && count < 100) {
Thread.sleep(50);
count ++;
}
Assert.assertEquals(expectedTrack, getTrack());
// Check the access log
alv.validateAccessLog(1, 200, TimeoutServlet.ASYNC_TIMEOUT,
TimeoutServlet.ASYNC_TIMEOUT + TIMEOUT_MARGIN + REQUEST_TIME);
}
private static class TrackingServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private static volatile boolean first = true;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
TestAsyncContextImpl.track("DispatchingServletGet-");
resp.flushBuffer();
final boolean first = TrackingServlet.first;
TrackingServlet.first = false;
final AsyncContext ctxt = req.startAsync();
TrackingListener listener = new TrackingListener(false, true, null);
ctxt.addListener(listener);
ctxt.setTimeout(3000);
Runnable run = new Runnable() {
@Override
public void run() {
if (first) {
ctxt.dispatch("/stage1");
} else {
ctxt.dispatch("/stage2");
}
}
};
if ("y".equals(req.getParameter("useThread"))) {
new Thread(run).start();
} else {
run.run();
}
}
}
private static class TrackingListener implements AsyncListener {
private final boolean completeOnError;
private final boolean completeOnTimeout;
private final String dispatchUrl;
public TrackingListener(boolean completeOnError,
boolean completeOnTimeout, String dispatchUrl) {
this.completeOnError = completeOnError;
this.completeOnTimeout = completeOnTimeout;
this.dispatchUrl = dispatchUrl;
}
@Override
public void onComplete(AsyncEvent event) throws IOException {
TestAsyncContextImpl.track("onComplete-");
}
@Override
public void onTimeout(AsyncEvent event) throws IOException {
TestAsyncContextImpl.track("onTimeout-");
if (completeOnTimeout){
event.getAsyncContext().complete();
}
if (dispatchUrl != null) {
event.getAsyncContext().dispatch(dispatchUrl);
}
}
@Override
public void onError(AsyncEvent event) throws IOException {
TestAsyncContextImpl.track("onError-");
if (completeOnError) {
event.getAsyncContext().complete();
}
}
@Override
public void onStartAsync(AsyncEvent event) throws IOException {
TestAsyncContextImpl.track("onStartAsync-");
}
}
private static class StickyTrackingListener extends TrackingListener {
public StickyTrackingListener(boolean completeOnError,
boolean completeOnTimeout, String dispatchUrl) {
super(completeOnError, completeOnTimeout, dispatchUrl);
}
@Override
public void onStartAsync(AsyncEvent event) throws IOException {
super.onStartAsync(event);
// Re-add this listener to the new AsyncContext
event.getAsyncContext().addListener(this);
}
}
public static class TrackingRequestListener
implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent sre) {
TestAsyncContextImpl.track("requestDestroyed");
}
@Override
public void requestInitialized(ServletRequestEvent sre) {
TestAsyncContextImpl.track("requestInitialized-");
}
}
@Test
public void testDispatchErrorSingle() throws Exception {
doTestDispatchError(1, false, false);
}
@Test
public void testDispatchErrorDouble() throws Exception {
doTestDispatchError(2, false, false);
}
@Test
public void testDispatchErrorMultiple() throws Exception {
doTestDispatchError(5, false, false);
}
@Test
public void testDispatchErrorWithThreadSingle() throws Exception {
doTestDispatchError(1, true, false);
}
@Test
public void testDispatchErrorWithThreadDouble() throws Exception {
doTestDispatchError(2, true, false);
}
@Test
public void testDispatchErrorWithThreadMultiple() throws Exception {
doTestDispatchError(5, true, false);
}
@Test
public void testDispatchErrorSingleThenComplete() throws Exception {
doTestDispatchError(1, false, true);
}
@Test
public void testDispatchErrorDoubleThenComplete() throws Exception {
doTestDispatchError(2, false, true);
}
@Test
public void testDispatchErrorMultipleThenComplete() throws Exception {
doTestDispatchError(5, false, true);
}
@Test
public void testDispatchErrorWithThreadSingleThenComplete()
throws Exception {
doTestDispatchError(1, true, true);
}
@Test
public void testDispatchErrorWithThreadDoubleThenComplete()
throws Exception {
doTestDispatchError(2, true, true);
}
@Test
public void testDispatchErrorWithThreadMultipleThenComplete()
throws Exception {
doTestDispatchError(5, true, true);
}
private void doTestDispatchError(int iter, boolean useThread,
boolean completeOnError)
throws Exception {
resetTracker();
// Setup Tomcat instance
Tomcat tomcat = getTomcatInstance();
// No file system docBase required
Context ctx = tomcat.addContext("", null);
DispatchingServlet dispatch =
new DispatchingServlet(true, completeOnError);
Wrapper wrapper = Tomcat.addServlet(ctx, "dispatch", dispatch);
wrapper.setAsyncSupported(true);
ctx.addServletMapping("/stage1", "dispatch");
ErrorServlet error = new ErrorServlet();
Tomcat.addServlet(ctx, "error", error);
ctx.addServletMapping("/stage2", "error");
ctx.addApplicationListener(TrackingRequestListener.class.getName());
TesterAccessLogValve alv = new TesterAccessLogValve();
ctx.getPipeline().addValve(alv);
tomcat.start();
StringBuilder url = new StringBuilder(48);
url.append("http://localhost:");
url.append(getPort());
url.append("/stage1?iter=");
url.append(iter);
if (useThread) {
url.append("&useThread=y");
}
getUrl(url.toString());
StringBuilder expected = new StringBuilder("requestInitialized-");
int loop = iter;
while (loop > 0) {
expected.append("DispatchingServletGet-");
if (loop != iter) {
expected.append("onStartAsync-");
}
loop--;
}
expected.append("ErrorServletGet-onError-onComplete-requestDestroyed");
// Request may complete before listener has finished processing so wait
// up to 5 seconds for the right response
String expectedTrack = expected.toString();
int count = 0;
while (!expectedTrack.equals(getTrack()) && count < 100) {
Thread.sleep(50);
count ++;
}
assertEquals(expectedTrack, getTrack());
// Check the access log
alv.validateAccessLog(1, 500, 0, REQUEST_TIME);
}
private static class ErrorServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
TestAsyncContextImpl.track("ErrorServletGet-");
try {
// Give the original thread a chance to exit the
// ErrorReportValve before we throw this exception
Thread.sleep(500);
} catch (InterruptedException e) {
throw new ServletException(e);
}
throw new ServletException("Oops.");
}
}
@Test
public void testBug50352() throws Exception {
resetTracker();
// Setup Tomcat instance
Tomcat tomcat = getTomcatInstance();
// No file system docBase required
Context ctx = tomcat.addContext("", null);
AsyncStartRunnable servlet = new AsyncStartRunnable();
Wrapper wrapper = Tomcat.addServlet(ctx, "servlet", servlet);
wrapper.setAsyncSupported(true);
ctx.addServletMapping("/", "servlet");
TesterAccessLogValve alv = new TesterAccessLogValve();
ctx.getPipeline().addValve(alv);
tomcat.start();
getUrl("http://localhost:" + getPort() + "/");
// Request may complete before listener has finished processing so wait
// up to 5 seconds for the right response
String expectedTrack = "Runnable-onComplete-";
int count = 0;
while (!expectedTrack.equals(getTrack()) && count < 100) {
Thread.sleep(50);
count ++;
}
assertEquals(expectedTrack, getTrack());
// Check the access log
alv.validateAccessLog(1, 200, AsyncStartRunnable.THREAD_SLEEP_TIME,
AsyncStartRunnable.THREAD_SLEEP_TIME + REQUEST_TIME);
}
private static final class AsyncStartRunnable extends HttpServlet {
private static final long serialVersionUID = 1L;
public static final long THREAD_SLEEP_TIME = 3000;
@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
final AsyncContext asyncContext =
request.startAsync(request, response);
asyncContext.addListener(new TrackingListener(false, false, null));
asyncContext.start(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(THREAD_SLEEP_TIME);
TestAsyncContextImpl.track("Runnable-");
asyncContext.complete();
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
}
@Test
public void testBug50753() throws Exception {
// Setup Tomcat instance
Tomcat tomcat = getTomcatInstance();
// No file system docBase required
Context ctx = tomcat.addContext("", null);
Bug50753Servlet servlet = new Bug50753Servlet();
Wrapper wrapper = Tomcat.addServlet(ctx, "servlet", servlet);
wrapper.setAsyncSupported(true);
ctx.addServletMapping("/", "servlet");
TesterAccessLogValve alv = new TesterAccessLogValve();
ctx.getPipeline().addValve(alv);
tomcat.start();
// Call the servlet once
Map<String,List<String>> headers = new LinkedHashMap<>();
ByteChunk bc = new ByteChunk();
int rc = getUrl("http://localhost:" + getPort() + "/", bc, headers);
assertEquals(200, rc);
assertEquals("OK", bc.toString());
List<String> testHeader = headers.get("A");
assertNotNull(testHeader);
assertEquals(1, testHeader.size());
assertEquals("xyz",testHeader.get(0));
// Check the access log
alv.validateAccessLog(1, 200, Bug50753Servlet.THREAD_SLEEP_TIME,
Bug50753Servlet.THREAD_SLEEP_TIME + REQUEST_TIME);
}
private static class Bug50753Servlet extends HttpServlet {
private static final long serialVersionUID = 1L;
public static final long THREAD_SLEEP_TIME = 5000;
@Override
protected void doGet(HttpServletRequest req,
final HttpServletResponse resp)
throws ServletException, IOException {
final AsyncContext ctx = req.startAsync();
ctx.start(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(THREAD_SLEEP_TIME);
resp.setHeader("A", "xyz");
resp.setContentType("text/plain");
resp.setContentLength("OK".getBytes().length);
resp.getWriter().print("OK");
ctx.complete();
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
}
@Test
public void testErrorHandling() throws Exception {
resetTracker();
// Setup Tomcat instance
Tomcat tomcat = getTomcatInstance();
// No file system docBase required
Context ctx = tomcat.addContext("", null);
ErrorServlet error = new ErrorServlet();
Tomcat.addServlet(ctx, "error", error);
ctx.addServletMapping("/error", "error");
TesterAccessLogValve alv = new TesterAccessLogValve();
ctx.getPipeline().addValve(alv);
tomcat.start();
StringBuilder url = new StringBuilder(48);
url.append("http://localhost:");
url.append(getPort());
url.append("/error");
int rc = getUrl(url.toString(), new ByteChunk(), null);
assertEquals(500, rc);
// Without this test may complete before access log has a chance to log
// the request
Thread.sleep(REQUEST_TIME);
// Check the access log
alv.validateAccessLog(1, 500, 0, REQUEST_TIME);
}
@Test
public void testCommitOnComplete() throws Exception {
// Setup Tomcat instance
Tomcat tomcat = getTomcatInstance();
// No file system docBase required
Context ctx = tomcat.addContext("", null);
AsyncStatusServlet asyncStatusServlet =
new AsyncStatusServlet(HttpServletResponse.SC_BAD_REQUEST);
Wrapper wrapper =
Tomcat.addServlet(ctx, "asyncStatusServlet", asyncStatusServlet);
wrapper.setAsyncSupported(true);
ctx.addServletMapping("/asyncStatusServlet", "asyncStatusServlet");
TesterAccessLogValve alv = new TesterAccessLogValve();
ctx.getPipeline().addValve(alv);
tomcat.start();
StringBuilder url = new StringBuilder(48);
url.append("http://localhost:");
url.append(getPort());
url.append("/asyncStatusServlet");
int rc = getUrl(url.toString(), new ByteChunk(), null);
assertEquals(HttpServletResponse.SC_BAD_REQUEST, rc);
// Without this test may complete before access log has a chance to log
// the request
Thread.sleep(REQUEST_TIME);
// Check the access log
alv.validateAccessLog(1, HttpServletResponse.SC_BAD_REQUEST, 0,
REQUEST_TIME);
}
private static class AsyncStatusServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private int status;
public AsyncStatusServlet(int status) {
this.status = status;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
AsyncContext actxt = req.startAsync();
resp.setStatus(status);
actxt.complete();
}
}
@Test
public void testBug51197a() throws Exception {
doTestBug51197(false, false);
}
@Test
public void testBug51197b() throws Exception {
doTestBug51197(true, false);
}
@Test
public void testBug51197c() throws Exception {
doTestBug51197(false, true);
}
@Test
public void testBug51197d() throws Exception {
doTestBug51197(true, true);
}
private void doTestBug51197(boolean threaded, boolean customError) throws Exception {
// Setup Tomcat instance
Tomcat tomcat = getTomcatInstance();
// No file system docBase required
Context ctx = tomcat.addContext("", null);
AsyncErrorServlet asyncErrorServlet =
new AsyncErrorServlet(HttpServletResponse.SC_BAD_REQUEST, threaded);
Wrapper wrapper =
Tomcat.addServlet(ctx, "asyncErrorServlet", asyncErrorServlet);
wrapper.setAsyncSupported(true);
ctx.addServletMapping("/asyncErrorServlet", "asyncErrorServlet");
if (customError) {
CustomErrorServlet customErrorServlet = new CustomErrorServlet();
Tomcat.addServlet(ctx, "customErrorServlet", customErrorServlet);
ctx.addServletMapping("/customErrorServlet", "customErrorServlet");
ErrorPage ep = new ErrorPage();
ep.setLocation("/customErrorServlet");
ctx.addErrorPage(ep);
}
TesterAccessLogValve alv = new TesterAccessLogValve();
ctx.getPipeline().addValve(alv);
tomcat.start();
StringBuilder url = new StringBuilder(48);
url.append("http://localhost:");
url.append(getPort());
url.append("/asyncErrorServlet");
ByteChunk res = new ByteChunk();
int rc = getUrl(url.toString(), res, null);
assertEquals(HttpServletResponse.SC_BAD_REQUEST, rc);
// SRV 10.9.2 - Handling an error is entirely the application's
// responsibility when an error occurs on an application thread.
// Calling sendError() followed by complete() and expecting the standard
// error page mechanism to kick in could be viewed as handling the error
String responseBody = res.toString();
Assert.assertNotNull(responseBody);
assertTrue(responseBody.length() > 0);
if (customError) {
assertTrue(responseBody, responseBody.contains(CustomErrorServlet.ERROR_MESSAGE));
} else {
assertTrue(responseBody, responseBody.contains(AsyncErrorServlet.ERROR_MESSAGE));
}
// Without this test may complete before access log has a chance to log
// the request
Thread.sleep(REQUEST_TIME);
// Check the access log
alv.validateAccessLog(1, HttpServletResponse.SC_BAD_REQUEST, 0,
REQUEST_TIME);
}
private static class CustomErrorServlet extends GenericServlet {
private static final long serialVersionUID = 1L;
public static final String ERROR_MESSAGE = "Custom error page";
@Override
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException {
res.getWriter().print(ERROR_MESSAGE);
}
}
private static class AsyncErrorServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
public static final String ERROR_MESSAGE = "It broke.";
private int status;
private boolean threaded;
public AsyncErrorServlet(int status, boolean threaded) {
this.status = status;
this.threaded = threaded;
}
@Override
protected void doGet(HttpServletRequest req, final HttpServletResponse resp)
throws ServletException, IOException {
final AsyncContext actxt = req.startAsync();
actxt.setTimeout(TIMEOUT);
if (threaded) {
actxt.start(new Runnable() {
@Override
public void run() {
try {
resp.sendError(status, ERROR_MESSAGE);
actxt.complete();
} catch (IOException e) {
// Ignore
}
}
});
} else {
resp.sendError(status, ERROR_MESSAGE);
actxt.complete();
}
}
}
@Test
public void testBug53337() throws Exception {
// Setup Tomcat instance
Tomcat tomcat = getTomcatInstance();
// No file system docBase required
Context ctx = tomcat.addContext("", null);
Wrapper a = Tomcat.addServlet(ctx, "ServletA", new Bug53337ServletA());
a.setAsyncSupported(true);
Wrapper b = Tomcat.addServlet(ctx, "ServletB", new Bug53337ServletB());
b.setAsyncSupported(true);
Tomcat.addServlet(ctx, "ServletC", new Bug53337ServletC());
ctx.addServletMapping("/ServletA", "ServletA");
ctx.addServletMapping("/ServletB", "ServletB");
ctx.addServletMapping("/ServletC", "ServletC");
tomcat.start();
StringBuilder url = new StringBuilder(48);
url.append("http://localhost:");
url.append(getPort());
url.append("/ServletA");
ByteChunk body = new ByteChunk();
int rc = getUrl(url.toString(), body, null);
assertEquals(HttpServletResponse.SC_OK, rc);
assertEquals("OK", body.toString());
}
private static class Bug53337ServletA extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
RequestDispatcher rd = req.getRequestDispatcher("/ServletB");
rd.forward(req, resp);
}
}
private static class Bug53337ServletB extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(final HttpServletRequest req,
final HttpServletResponse resp)
throws ServletException, IOException {
final AsyncContext async = req.startAsync();
// Just for debugging
async.setTimeout(100000);
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(new Runnable() {
@Override
public void run() {
async.dispatch("/ServletC");
}
});
executor.shutdown();
}
}
private static class Bug53337ServletC extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setContentType("text/plain");
resp.getWriter().print("OK");
}
}
@Test
public void testBug53843() throws Exception {
// Setup Tomcat instance
Tomcat tomcat = getTomcatInstance();
// No file system docBase required
Context ctx = tomcat.addContext("", null);
Bug53843ServletA servletA = new Bug53843ServletA();
Wrapper a = Tomcat.addServlet(ctx, "ServletA", servletA);
a.setAsyncSupported(true);
Tomcat.addServlet(ctx, "ServletB", new Bug53843ServletB());
ctx.addServletMapping("/ServletA", "ServletA");
ctx.addServletMapping("/ServletB", "ServletB");
tomcat.start();
StringBuilder url = new StringBuilder(48);
url.append("http://localhost:");
url.append(getPort());
url.append("/ServletA");
ByteChunk body = new ByteChunk();
int rc = getUrl(url.toString(), body, null);
assertEquals(HttpServletResponse.SC_OK, rc);
assertEquals("OK", body.toString());
assertTrue(servletA.isAsyncWhenExpected());
}
private static class Bug53843ServletA extends HttpServlet {
private static final long serialVersionUID = 1L;
private boolean isAsyncWhenExpected = true;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// Should not be async at this point
isAsyncWhenExpected = isAsyncWhenExpected && !req.isAsyncStarted();
final AsyncContext async = req.startAsync();
// Should be async at this point
isAsyncWhenExpected = isAsyncWhenExpected && req.isAsyncStarted();
async.start(new Runnable() {
@Override
public void run() {
// This should be delayed until the original container
// thread exists
async.dispatch("/ServletB");
}
});
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new ServletException(e);
}
// Should be async at this point
isAsyncWhenExpected = isAsyncWhenExpected && req.isAsyncStarted();
}
public boolean isAsyncWhenExpected() {
return isAsyncWhenExpected;
}
}
private static class Bug53843ServletB extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setContentType("text/plain");
resp.getWriter().print("OK");
}
}
@Test
public void testTimeoutErrorDispatchNone() throws Exception {
doTestTimeoutErrorDispatch(null, null);
}
@Test
public void testTimeoutErrorDispatchNonAsync() throws Exception {
doTestTimeoutErrorDispatch(Boolean.FALSE, null);
}
@Test
public void testTimeoutErrorDispatchAsyncStart() throws Exception {
doTestTimeoutErrorDispatch(
Boolean.TRUE, ErrorPageAsyncMode.NO_COMPLETE);
}
@Test
public void testTimeoutErrorDispatchAsyncComplete() throws Exception {
doTestTimeoutErrorDispatch(Boolean.TRUE, ErrorPageAsyncMode.COMPLETE);
}
@Test
public void testTimeoutErrorDispatchAsyncDispatch() throws Exception {
doTestTimeoutErrorDispatch(Boolean.TRUE, ErrorPageAsyncMode.DISPATCH);
}
private void doTestTimeoutErrorDispatch(Boolean asyncError,
ErrorPageAsyncMode mode) throws Exception {
resetTracker();
// Setup Tomcat instance
Tomcat tomcat = getTomcatInstance();
// No file system docBase required
Context ctx = tomcat.addContext("", null);
TimeoutServlet timeout = new TimeoutServlet(null, null);
Wrapper w1 = Tomcat.addServlet(ctx, "time", timeout);
w1.setAsyncSupported(true);
ctx.addServletMapping("/async", "time");
NonAsyncServlet nonAsync = new NonAsyncServlet();
Wrapper w2 = Tomcat.addServlet(ctx, "nonAsync", nonAsync);
w2.setAsyncSupported(true);
ctx.addServletMapping("/error/nonasync", "nonAsync");
AsyncErrorPage asyncErrorPage = new AsyncErrorPage(mode);
Wrapper w3 = Tomcat.addServlet(ctx, "asyncErrorPage", asyncErrorPage);
w3.setAsyncSupported(true);
ctx.addServletMapping("/error/async", "asyncErrorPage");
if (asyncError != null) {
ErrorPage ep = new ErrorPage();
ep.setErrorCode(500);
if (asyncError.booleanValue()) {
ep.setLocation("/error/async");
} else {
ep.setLocation("/error/nonasync");
}
ctx.addErrorPage(ep);
}
ctx.addApplicationListener(TrackingRequestListener.class.getName());
TesterAccessLogValve alv = new TesterAccessLogValve();
ctx.getPipeline().addValve(alv);
TesterAccessLogValve alvGlobal = new TesterAccessLogValve();
tomcat.getHost().getPipeline().addValve(alvGlobal);
tomcat.start();
ByteChunk res = new ByteChunk();
try {
getUrl("http://localhost:" + getPort() + "/async", res, null);
} catch (IOException ioe) {
// Ignore - expected for some error conditions
}
StringBuilder expected = new StringBuilder();
expected.append("requestInitialized-TimeoutServletGet-");
if (asyncError != null) {
if (asyncError.booleanValue()) {
expected.append("AsyncErrorPageGet-");
if (mode == ErrorPageAsyncMode.NO_COMPLETE){
expected.append("NoOp-");
} else if (mode == ErrorPageAsyncMode.COMPLETE) {
expected.append("Complete-");
} else if (mode == ErrorPageAsyncMode.DISPATCH) {
expected.append("Dispatch-NonAsyncServletGet-");
}
} else {
expected.append("NonAsyncServletGet-");
}
}
expected.append("requestDestroyed");
// Request may complete before listener has finished processing so wait
// up to 5 seconds for the right response
String expectedTrack = expected.toString();
int count = 0;
while (!expectedTrack.equals(getTrack()) && count < 100) {
Thread.sleep(50);
count ++;
}
Assert.assertEquals(expectedTrack, getTrack());
// Check the access log
alvGlobal.validateAccessLog(1, 500, TimeoutServlet.ASYNC_TIMEOUT,
TimeoutServlet.ASYNC_TIMEOUT + TIMEOUT_MARGIN +
REQUEST_TIME);
alv.validateAccessLog(1, 500, TimeoutServlet.ASYNC_TIMEOUT,
TimeoutServlet.ASYNC_TIMEOUT + TIMEOUT_MARGIN +
REQUEST_TIME);
}
private static enum ErrorPageAsyncMode {
NO_COMPLETE,
COMPLETE,
DISPATCH
}
private static class AsyncErrorPage extends HttpServlet {
private static final long serialVersionUID = 1L;
private final ErrorPageAsyncMode mode;
public AsyncErrorPage(ErrorPageAsyncMode mode) {
this.mode = mode;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
TestAsyncContextImpl.track("AsyncErrorPageGet-");
final AsyncContext ctxt = req.getAsyncContext();
switch(mode) {
case COMPLETE:
TestAsyncContextImpl.track("Complete-");
ctxt.complete();
break;
case DISPATCH:
TestAsyncContextImpl.track("Dispatch-");
ctxt.dispatch("/error/nonasync");
break;
case NO_COMPLETE:
TestAsyncContextImpl.track("NoOp-");
break;
default:
// Impossible
break;
}
}
}
@Test
public void testBug54178() throws Exception {
// Setup Tomcat instance
Tomcat tomcat = getTomcatInstance();
// No file system docBase required
Context ctx = tomcat.addContext("", null);
Bug54178ServletA bug54178ServletA = new Bug54178ServletA();
Wrapper wrapper =
Tomcat.addServlet(ctx, "bug54178ServletA", bug54178ServletA);
wrapper.setAsyncSupported(true);
ctx.addServletMapping("/bug54178ServletA", "bug54178ServletA");
Bug54178ServletB bug54178ServletB = new Bug54178ServletB();
Tomcat.addServlet(ctx, "bug54178ServletB", bug54178ServletB);
ctx.addServletMapping("/bug54178ServletB", "bug54178ServletB");
tomcat.start();
ByteChunk body = new ByteChunk();
int rc = -1;
try {
rc = getUrl("http://localhost:" + getPort() + "/bug54178ServletA?" +
Bug54178ServletA.PARAM_NAME + "=bar",
body, null);
} catch (IOException ioe) {
// This may happen if test fails. Output the exception in case it is
// useful and let asserts handle the failure
ioe.printStackTrace();
}
assertEquals(HttpServletResponse.SC_OK, rc);
body.recycle();
rc = getUrl("http://localhost:" + getPort() + "/bug54178ServletB",
body, null);
assertEquals(HttpServletResponse.SC_OK, rc);
assertEquals("OK", body.toString());
}
private static class Bug54178ServletA extends HttpServlet {
public static final String PARAM_NAME = "foo";
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
req.getParameter(PARAM_NAME);
AsyncContext actxt = req.startAsync();
actxt.addListener(new Bug54178AsyncListener());
actxt.complete();
}
}
private static class Bug54178ServletB extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setContentType("text/plain");
PrintWriter pw = resp.getWriter();
String result = req.getParameter(Bug54178ServletA.PARAM_NAME);
if (result == null) {
pw.write("OK");
} else {
pw.write("FAIL");
}
}
}
private static class Bug54178AsyncListener implements AsyncListener {
@Override
public void onComplete(AsyncEvent event) throws IOException {
throw new RuntimeException("Testing Bug54178");
}
@Override
public void onTimeout(AsyncEvent event) throws IOException {
// NO-OP
}
@Override
public void onError(AsyncEvent event) throws IOException {
// NO-OP
}
@Override
public void onStartAsync(AsyncEvent event) throws IOException {
// NO-OP
}
}
@Test
public void testBug59219a() throws Exception{
doTestBug59219("", "doGet-onError-onComplete-");
}
@Test
public void testBug59219b() throws Exception{
doTestBug59219("?loops=3", "doGet-doGet-onStartAsync-doGet-onStartAsync-onError-onComplete-");
}
private void doTestBug59219(String queryString, String expectedTrack) throws Exception {
resetTracker();
Tomcat tomcat = getTomcatInstance();
Context ctx = tomcat.addContext("", null);
Wrapper w = tomcat.addServlet("", "async", new Bug59219Servlet());
w.setAsyncSupported(true);
ctx.addServletMapping("/async", "async");
tomcat.start();
getUrl("http://localhost:" + getPort() + "/async" + queryString);
// Wait up to 5s for the response
int count = 0;
while(!expectedTrack.equals(getTrack()) && count < 100) {
Thread.sleep(50);
count++;
}
Assert.assertEquals(expectedTrack, getTrack());
}
private static class Bug59219Servlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
track("doGet-");
AsyncContext ctx = req.startAsync();
ctx.setTimeout(3000);
ctx.addListener(new TrackingListener(true, false, "/async"));
String loopsParam = req.getParameter("loops");
Integer loopsAttr = (Integer) req.getAttribute("loops");
int loops = 0;
if (loopsAttr != null) {
loops = loopsAttr.intValue();
} else if (loopsParam != null) {
loops = Integer.parseInt(loopsParam);
}
if (loops > 1) {
loops--;
req.setAttribute("loops", Integer.valueOf(loops));
ctx.dispatch();
} else
throw new ServletException();
}
}
@Test
public void testForbiddenDispatching() throws Exception {
resetTracker();
// Setup Tomcat instance
Tomcat tomcat = getTomcatInstance();
// No file system docBase required
Context ctx = tomcat.addContext("", null);
NonAsyncServlet nonAsyncServlet = new NonAsyncServlet();
Wrapper wrapper = Tomcat.addServlet(ctx, "nonAsyncServlet",
nonAsyncServlet);
wrapper.setAsyncSupported(true);
ctx.addServletMapping("/target", "nonAsyncServlet");
DispatchingGenericServlet forbiddenDispatchingServlet = new DispatchingGenericServlet();
Wrapper wrapper1 = Tomcat.addServlet(ctx,
"forbiddenDispatchingServlet", forbiddenDispatchingServlet);
wrapper1.setAsyncSupported(true);
ctx.addServletMapping("/forbiddenDispatchingServlet",
"forbiddenDispatchingServlet");
tomcat.start();
try {
getUrl("http://localhost:" + getPort()
+ "/forbiddenDispatchingServlet");
} catch (IOException ioe) {
// This may happen if test fails. Output the exception in case it is
// useful and let asserts handle the failure
ioe.printStackTrace();
}
// Request may complete before listener has finished processing so wait
// up to 5 seconds for the right response
String expectedTrack = "OKNonAsyncServletGet-";
int count = 0;
while (!expectedTrack.equals(getTrack()) && count < 100) {
Thread.sleep(50);
count ++;
}
Assert.assertEquals(expectedTrack, getTrack());
}
private static class DispatchingGenericServlet extends GenericServlet {
private static final long serialVersionUID = 1L;
private static final String CUSTOM_REQ_RESP = "crr";
private static final String EMPTY_DISPATCH = "empty";
@Override
public void service(ServletRequest req, ServletResponse resp)
throws ServletException, IOException {
if (DispatcherType.ASYNC != req.getDispatcherType()) {
AsyncContext asyncContext;
if ("y".equals(req.getParameter(CUSTOM_REQ_RESP))) {
asyncContext = req.startAsync(
new ServletRequestWrapper(req),
new ServletResponseWrapper(resp));
} else {
asyncContext = req.startAsync();
}
if ("y".equals(req.getParameter(EMPTY_DISPATCH))) {
asyncContext.dispatch();
} else {
asyncContext.dispatch("/target");
}
try {
asyncContext.dispatch("/nonExistingServlet");
TestAsyncContextImpl.track("FAIL");
} catch (IllegalStateException e) {
TestAsyncContextImpl.track("OK");
}
} else {
TestAsyncContextImpl.track("DispatchingGenericServletGet-");
}
}
}
@Test
public void testGetRequestISE() throws Exception {
doTestAsyncISE(true);
}
@Test
public void testGetResponseISE() throws Exception {
doTestAsyncISE(false);
}
private void doTestAsyncISE(boolean useGetRequest) throws Exception {
// Setup Tomcat instance
Tomcat tomcat = getTomcatInstance();
// No file system docBase required
Context ctx = tomcat.addContext("", null);
AsyncISEServlet servlet = new AsyncISEServlet();
Wrapper w = Tomcat.addServlet(ctx, "AsyncISEServlet", servlet);
w.setAsyncSupported(true);
ctx.addServletMapping("/test", "AsyncISEServlet");
tomcat.start();
ByteChunk response = new ByteChunk();
int rc = getUrl("http://localhost:" + getPort() +"/test", response,
null);
Assert.assertEquals(HttpServletResponse.SC_OK, rc);
boolean hasIse = false;
try {
if (useGetRequest) {
servlet.getAsyncContext().getRequest();
} else {
servlet.getAsyncContext().getResponse();
}
} catch (IllegalStateException ise) {
hasIse = true;
}
Assert.assertTrue(hasIse);
}
/**
* Accessing the AsyncContext in this way is an ugly hack that should never
* be used in a real application since it is not thread safe. That said, it
* is this sort of hack that the ISE is meant to be preventing.
*
*/
private static class AsyncISEServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private AsyncContext asyncContext;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setContentType("text/plain;UTF-8");
asyncContext = req.startAsync();
// This will commit the response
asyncContext.complete();
}
public AsyncContext getAsyncContext() {
return asyncContext;
}
}
@Test
public void testDispatchWithCustomRequestResponse() throws Exception {
prepareApplicationWithGenericServlet("");
StringBuilder expected = new StringBuilder();
expected.append("OK");
expected.append("CustomGenericServletGet-");
requestApplicationWithGenericServlet("/dispatch?crr=y", expected);
expected = new StringBuilder();
expected.append("OK");
expected.append("DispatchingGenericServletGet-");
requestApplicationWithGenericServlet("/dispatch?crr=y&empty=y",
expected);
}
private static class CustomGenericServlet extends GenericServlet {
private static final long serialVersionUID = 1L;
@Override
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException {
if (req instanceof ServletRequestWrapper
&& res instanceof ServletResponseWrapper) {
TestAsyncContextImpl.track("CustomGenericServletGet-");
}
}
}
@Test
public void testEmptyDispatch() throws Exception {
prepareApplicationWithGenericServlet("/fo o");
StringBuilder expected = new StringBuilder();
expected.append("OK");
expected.append("DispatchingGenericServletGet-");
requestApplicationWithGenericServlet("/fo%20o/dispatch?empty=y",
expected);
requestApplicationWithGenericServlet("//fo%20o/dispatch?empty=y",
expected);
requestApplicationWithGenericServlet("/./fo%20o/dispatch?empty=y",
expected);
requestApplicationWithGenericServlet("/fo%20o//dispatch?empty=y",
expected);
requestApplicationWithGenericServlet("/fo%20o/./dispatch?empty=y",
expected);
requestApplicationWithGenericServlet("/fo%20o/c/../dispatch?empty=y",
expected);
}
@Test
public void testEmptyDispatchWithCustomRequestResponse() throws Exception {
prepareApplicationWithGenericServlet("/fo o");
StringBuilder expected = new StringBuilder();
expected.append("OK");
expected.append("DispatchingGenericServletGet-");
requestApplicationWithGenericServlet("/fo%20o/dispatch?crr=y&empty=y",
expected);
requestApplicationWithGenericServlet("//fo%20o/dispatch?crr=y&empty=y",
expected);
requestApplicationWithGenericServlet(
"/./fo%20o/dispatch?crr=y&empty=y", expected);
requestApplicationWithGenericServlet("/fo%20o//dispatch?crr=y&empty=y",
expected);
requestApplicationWithGenericServlet(
"/fo%20o/./dispatch?crr=y&empty=y", expected);
requestApplicationWithGenericServlet(
"/fo%20o/c/../dispatch?crr=y&empty=y", expected);
}
private void prepareApplicationWithGenericServlet(String contextPath)
throws Exception {
// Setup Tomcat instance
Tomcat tomcat = getTomcatInstance();
// No file system docBase required
Context ctx = tomcat.addContext(contextPath, null);
DispatchingGenericServlet dispatch = new DispatchingGenericServlet();
Wrapper wrapper = Tomcat.addServlet(ctx, "dispatch", dispatch);
wrapper.setAsyncSupported(true);
ctx.addServletMapping("/dispatch", "dispatch");
CustomGenericServlet customGeneric = new CustomGenericServlet();
Wrapper wrapper2 = Tomcat.addServlet(ctx, "customGeneric",
customGeneric);
wrapper2.setAsyncSupported(true);
ctx.addServletMapping("/target", "customGeneric");
tomcat.start();
}
private void requestApplicationWithGenericServlet(String path,
StringBuilder expectedContent) throws Exception {
resetTracker();
getUrl("http://localhost:" + getPort() + path);
// Request may complete before listener has finished processing so wait
// up to 5 seconds for the right response
String expectedTrack = expectedContent.toString();
int count = 0;
while (!expectedTrack.equals(getTrack()) && count < 100) {
Thread.sleep(50);
count ++;
}
Assert.assertEquals(expectedTrack, getTrack());
}
// https://bz.apache.org/bugzilla/show_bug.cgi?id=57326
@Test
public void testAsyncContextListenerClearing() throws Exception {
resetTracker();
// Setup Tomcat instance
Tomcat tomcat = getTomcatInstance();
// No file system docBase required
Context ctx = tomcat.addContext("", null);
Servlet stage1 = new DispatchingServletTracking("/stage2", true);
Wrapper wrapper1 = Tomcat.addServlet(ctx, "stage1", stage1);
wrapper1.setAsyncSupported(true);
ctx.addServletMapping("/stage1", "stage1");
Servlet stage2 = new DispatchingServletTracking("/stage3", false);
Wrapper wrapper2 = Tomcat.addServlet(ctx, "stage2", stage2);
wrapper2.setAsyncSupported(true);
ctx.addServletMapping("/stage2", "stage2");
Servlet stage3 = new NonAsyncServlet();
Tomcat.addServlet(ctx, "stage3", stage3);
ctx.addServletMapping("/stage3", "stage3");
TesterAccessLogValve alv = new TesterAccessLogValve();
ctx.getPipeline().addValve(alv);
tomcat.start();
getUrl("http://localhost:" + getPort()+ "/stage1");
assertEquals("doGet-startAsync-doGet-startAsync-onStartAsync-NonAsyncServletGet-onComplete-", getTrack());
// Check the access log
alv.validateAccessLog(1, 200, 0, REQUEST_TIME);
}
private static class DispatchingServletTracking extends HttpServlet {
private static final long serialVersionUID = 1L;
private final String target;
private final boolean addTrackingListener;
public DispatchingServletTracking(String target, boolean addTrackingListener) {
this.target = target;
this.addTrackingListener = addTrackingListener;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
track("doGet-startAsync-");
AsyncContext ac = req.startAsync();
if (addTrackingListener) {
ac.addListener(new StickyTrackingListener(false, false, null));
}
ac.dispatch(target);
}
}
// https://bz.apache.org/bugzilla/show_bug.cgi?id=57559
@Test
public void testAsyncRequestURI() throws Exception {
// Setup Tomcat instance
Tomcat tomcat = getTomcatInstance();
// No file system docBase required
Context ctx = tomcat.addContext("", null);
Servlet servlet = new AsyncRequestUriServlet();
Wrapper wrapper1 = Tomcat.addServlet(ctx, "bug57559", servlet);
wrapper1.setAsyncSupported(true);
ctx.addServletMapping("/", "bug57559");
tomcat.start();
String uri = "/foo/%24/bar";
ByteChunk body = getUrl("http://localhost:" + getPort()+ uri);
Assert.assertEquals(uri, body.toString());
}
private static class AsyncRequestUriServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
if (DispatcherType.ASYNC.equals(req.getDispatcherType())) {
resp.setContentType("text/plain");
resp.setCharacterEncoding("UTF-8");
resp.getWriter().write(req.getRequestURI());
} else {
req.startAsync().dispatch();
}
}
}
@Test
public void testDispatchFromOtherContainerThread() throws Exception {
resetTracker();
// Setup Tomcat instance
Tomcat tomcat = getTomcatInstance();
// No file system docBase required
Context ctx = tomcat.addContext("", null);
NonAsyncServlet nonAsyncServlet = new NonAsyncServlet();
Tomcat.addServlet(ctx, "nonAsyncServlet", nonAsyncServlet);
ctx.addServletMapping("/target", "nonAsyncServlet");
AsyncStashServlet asyncStashServlet = new AsyncStashServlet();
Wrapper w1 = Tomcat.addServlet(ctx, "asyncStashServlet", asyncStashServlet);
w1.setAsyncSupported(true);
ctx.addServletMapping("/asyncStashServlet", "asyncStashServlet");
AsyncRetrieveServlet asyncRetrieveServlet = new AsyncRetrieveServlet();
Wrapper w2 = Tomcat.addServlet(ctx, "asyncRetrieveServlet", asyncRetrieveServlet);
w2.setAsyncSupported(true);
ctx.addServletMapping("/asyncRetrieveServlet", "asyncRetrieveServlet");
tomcat.start();
// First request in separate thread because the response won't be
// written until after the second request has been made.
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
getUrl("http://localhost:" + getPort() + "/asyncStashServlet");
} catch (IOException e) {
e.printStackTrace();
}
}
});
t.start();
// Wait for first request to get as far as it can
int count = 0;
while (count < 100 && getTrack() != null &&
!getTrack().startsWith("AsyncStashServletGet-")) {
count++;
Thread.sleep(100);
}
getUrl("http://localhost:" + getPort() + "/asyncRetrieveServlet");
// Wait for second request to release first and allow it to complete
String expectedTrack = "AsyncStashServletGet-AsyncRetrieveServletGet-NonAsyncServletGet-";
count = 0;
while (count < 100 && !getTrack().equals(expectedTrack)) {
count++;
Thread.sleep(100);
}
Assert.assertEquals(expectedTrack, getTrack());
}
private static class AsyncStashServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private static final String DEFAULT_KEY = "DEFAULT";
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String key = req.getParameter("key");
if (key == null) {
key = DEFAULT_KEY;
}
req.getServletContext().setAttribute(key, req.startAsync());
TestAsyncContextImpl.track("AsyncStashServletGet-");
}
}
private static class AsyncRetrieveServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private static final String DEFAULT_KEY = "DEFAULT";
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String key = req.getParameter("key");
if (key == null) {
key = DEFAULT_KEY;
}
AsyncContext ac = (AsyncContext) req.getServletContext().getAttribute(key);
if (ac == null) {
TestAsyncContextImpl.track("FAIL:nullAsyncContext-");
} else {
TestAsyncContextImpl.track("AsyncRetrieveServletGet-");
ac.dispatch("/target");
}
}
}
/*
* See https://bz.apache.org/bugzilla/show_bug.cgi?id=58751 comment 1
*/
@Test
public void testTimeoutDispatchCustomErrorPage() throws Exception {
Tomcat tomcat = getTomcatInstance();
Context context = tomcat.addContext("", null);
tomcat.addServlet("", "timeout", Bug58751AsyncServlet.class.getName())
.setAsyncSupported(true);
CustomErrorServlet customErrorServlet = new CustomErrorServlet();
Tomcat.addServlet(context, "customErrorServlet", customErrorServlet);
context.addServletMapping("/timeout", "timeout");
context.addServletMapping("/error", "customErrorServlet");
ErrorPage errorPage = new ErrorPage();
errorPage.setLocation("/error");
context.addErrorPage(errorPage);
tomcat.start();
ByteChunk responseBody = new ByteChunk();
int rc = getUrl("http://localhost:" + getPort() + "/timeout", responseBody, null);
Assert.assertEquals(503, rc);
Assert.assertEquals(CustomErrorServlet.ERROR_MESSAGE, responseBody.toString());
}
public static class Bug58751AsyncServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
if (req.getAttribute("timeout") != null) {
resp.sendError(503);
}
else {
final AsyncContext context = req.startAsync();
context.setTimeout(5000);
context.addListener(new AsyncListener() {
@Override
public void onTimeout(AsyncEvent event) throws IOException {
HttpServletResponse response = (HttpServletResponse) event
.getSuppliedResponse();
if (!response.isCommitted()) {
((HttpServletRequest) event.getSuppliedRequest())
.setAttribute("timeout", Boolean.TRUE);
context.dispatch();
}
}
@Override
public void onStartAsync(AsyncEvent event) throws IOException {
}
@Override
public void onError(AsyncEvent event) throws IOException {
}
@Override
public void onComplete(AsyncEvent event) throws IOException {
}
});
}
}
}
@Test
public void testAsyncListenerSupplyRequestResponse() {
final ServletRequest servletRequest = EasyMock.createMock(ServletRequest.class);
final ServletResponse servletResponse = EasyMock.createMock(ServletResponse.class);
final AsyncListener listener = new AsyncListener() {
@Override
public void onTimeout(AsyncEvent event) throws IOException {
checkRequestResponse(event);
}
@Override
public void onStartAsync(AsyncEvent event) throws IOException {
checkRequestResponse(event);
}
@Override
public void onError(AsyncEvent event) throws IOException {
checkRequestResponse(event);
}
@Override
public void onComplete(AsyncEvent event) throws IOException {
checkRequestResponse(event);
}
private void checkRequestResponse(AsyncEvent event) {
assertEquals(servletRequest, event.getSuppliedRequest());
assertEquals(servletResponse, event.getSuppliedResponse());
}
};
final Context context = new TesterContext();
final Response response = new Response();
final Request request = new Request();
request.setCoyoteRequest(new org.apache.coyote.Request());
request.getMappingData().context = context;
final AsyncContextImpl ac = new AsyncContextImpl(request);
ac.addListener(listener, servletRequest, servletResponse);
ac.setStarted(context, request, response, true);
ac.addListener(listener, servletRequest, servletResponse);
ac.setErrorState(new Exception(), true);
ac.fireOnComplete();
}
/*
* https://bz.apache.org/bugzilla/show_bug.cgi?id=59317
*/
@Test
public void testAsyncDispatchUrlWithSpaces() throws Exception {
doTestDispatchWithSpaces(true);
}
@Test
public void testForwardDispatchUrlWithSpaces() throws Exception {
doTestDispatchWithSpaces(false);
}
private void doTestDispatchWithSpaces(boolean async) throws Exception {
Tomcat tomcat = getTomcatInstance();
Context context = tomcat.addContext("", null);
if (async) {
Servlet s = new AsyncDispatchUrlWithSpacesServlet();
Wrapper w = Tomcat.addServlet(context, "space", s);
w.setAsyncSupported(true);
} else {
Tomcat.addServlet(context, "space", new ForwardDispatchUrlWithSpacesServlet());
}
context.addServletMapping("/space/*", "space");
tomcat.start();
ByteChunk responseBody = new ByteChunk();
int rc = getUrl("http://localhost:" + getPort() + "/sp%61ce/foo%20bar", responseBody, null);
Assert.assertEquals(200, rc);
}
private static class AsyncDispatchUrlWithSpacesServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
Integer countObj = (Integer) req.getAttribute("count");
int count = 0;
if (countObj != null) {
count = countObj.intValue();
}
count++;
req.setAttribute("count", Integer.valueOf(count));
String encodedUri = req.getRequestURI();
String decodedUri = UDecoder.URLDecode(encodedUri);
try {
// Just here to trigger the error
@SuppressWarnings("unused")
URI u = new URI(encodedUri);
} catch (URISyntaxException e) {
throw new ServletException(e);
}
if (count > 3) {
resp.setContentType("text/plain");
resp.getWriter().print("OK");
} else {
AsyncContext ac = req.startAsync();
ac.dispatch(decodedUri);
}
}
}
private static class ForwardDispatchUrlWithSpacesServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
Integer countObj = (Integer) req.getAttribute("count");
int count = 0;
if (countObj != null) {
count = countObj.intValue();
}
count++;
req.setAttribute("count", Integer.valueOf(count));
String encodedUri = req.getRequestURI();
String decodedUri = UDecoder.URLDecode(encodedUri);
try {
// Just here to trigger the error
@SuppressWarnings("unused")
URI u = new URI(req.getRequestURI());
} catch (URISyntaxException e) {
throw new ServletException(e);
}
if (count > 3) {
resp.setContentType("text/plain");
resp.getWriter().print("OK");
} else {
RequestDispatcher rd = req.getRequestDispatcher(decodedUri);
rd.forward(req, resp);
}
}
}
@Test
public void testAsyncDispatchEncoding() throws Exception {
}
}