/* * 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.coyote; import java.io.IOException; import javax.servlet.AsyncContext; import javax.servlet.ReadListener; import javax.servlet.ServletException; import javax.servlet.ServletInputStream; import javax.servlet.ServletOutputStream; import javax.servlet.WriteListener; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.junit.Assert; import org.junit.Test; import org.apache.catalina.Context; import org.apache.catalina.Wrapper; import org.apache.catalina.startup.SimpleHttpClient; import org.apache.catalina.startup.Tomcat; import org.apache.catalina.startup.TomcatBaseTest; public class TestIoTimeouts extends TomcatBaseTest { @Test public void testNonBlockingReadWithNoTimeout() { // Sends complete request in 3 packets ChunkedClient client = new ChunkedClient(true); client.doRequest(); Assert.assertTrue(client.isResponse200()); Assert.assertTrue(client.isResponseBodyOK()); Assert.assertNull(EchoListener.t); } @Test public void testNonBlockingReadTimeout() { // Sends incomplete request (no end chunk) so read times out ChunkedClient client = new ChunkedClient(false); client.doRequest(); Assert.assertFalse(client.isResponse200()); Assert.assertFalse(client.isResponseBodyOK()); // Socket will be closed before the error handler runs. Closing the // socket triggers the client code's return from the doRequest() method // above so we need to wait at this point for the error handler to be // triggered. int count = 0; // Shouldn't need to wait long but allow plenty of time as the CI // systems are sometimes slow. while (count < 100 && EchoListener.t == null) { try { Thread.sleep(100); } catch (InterruptedException e) { // Ignore } count++; } Assert.assertNotNull(EchoListener.t); } private class ChunkedClient extends SimpleHttpClient { private final boolean sendEndChunk; public ChunkedClient(boolean sendEndChunk) { this.sendEndChunk = sendEndChunk; } private Exception doRequest() { Tomcat tomcat = getTomcatInstance(); Context root = tomcat.addContext("", TEMP_DIR); Wrapper w = Tomcat.addServlet(root, "Test", new NonBlockingEchoServlet()); w.setAsyncSupported(true); root.addServletMappingDecoded("/test", "Test"); try { tomcat.start(); setPort(tomcat.getConnector().getLocalPort()); // Open connection connect(); int packetCount = 2; if (sendEndChunk) { packetCount++; } String[] request = new String[packetCount]; request[0] = "POST /test HTTP/1.1" + CRLF + "Host: localhost:8080" + CRLF + "Transfer-Encoding: chunked" + CRLF + "Connection: close" + CRLF + CRLF; request[1] = "b8" + CRLF + "{" + CRLF + " \"tenantId\": \"dotCom\", " + CRLF + " \"locale\": \"en-US\", " + CRLF + " \"defaultZoneId\": \"25\", " + CRLF + " \"itemIds\": [\"StaplesUSCAS/en-US/2/<EOF>/<EOF>\"] , " + CRLF + " \"assetStoreId\": \"5051\", " + CRLF + " \"zipCode\": \"98109\"" + CRLF + "}" + CRLF; if (sendEndChunk) { request[2] = "0" + CRLF + CRLF; } setRequest(request); processRequest(); // blocks until response has been read // Close the connection disconnect(); } catch (Exception e) { return e; } return null; } @Override public boolean isResponseBodyOK() { if (getResponseBody() == null) { return false; } if (!getResponseBody().contains("98109")) { return false; } return true; } } private static class NonBlockingEchoServlet extends HttpServlet { private static final long serialVersionUID = 1L; @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // Need to be in async mode to use non-blocking I/O AsyncContext ac = req.startAsync(); ac.setTimeout(10000); ServletInputStream sis = null; ServletOutputStream sos = null; try { sis = req.getInputStream(); sos = resp.getOutputStream(); } catch (IOException ioe) { throw new ServletException(ioe); } EchoListener listener = new EchoListener(ac, sis, sos); sis.setReadListener(listener); sos.setWriteListener(listener); } } private static class EchoListener implements ReadListener, WriteListener { private static volatile Throwable t; private final AsyncContext ac; private final ServletInputStream sis; private final ServletOutputStream sos; private final byte[] buffer = new byte[8192]; public EchoListener(AsyncContext ac, ServletInputStream sis, ServletOutputStream sos) { t = null; this.ac = ac; this.sis = sis; this.sos = sos; } @Override public void onWritePossible() throws IOException { if (sis.isFinished()) { sos.flush(); ac.complete(); return; } while (sis.isReady()) { int read = sis.read(buffer); if (read > 0) { sos.write(buffer, 0, read); if (!sos.isReady()) { break; } } } } @Override public void onDataAvailable() throws IOException { if (sos.isReady()) { onWritePossible(); } } @Override public void onAllDataRead() throws IOException { if (sos.isReady()) { onWritePossible(); } } @Override public void onError(Throwable throwable) { t = throwable; ac.complete(); } } }