blob: 5caddc470e1fe526984f7b65b1b3391d776c1d4c [file] [log] [blame]
//
// ========================================================================
// Copyright (c) 1995-2017 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.servlet;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
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.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.toolchain.test.http.SimpleHttpParser;
import org.eclipse.jetty.toolchain.test.http.SimpleHttpResponse;
import org.eclipse.jetty.util.IO;
import org.junit.After;
import org.junit.Assert;
import org.junit.Test;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
public class AsyncIOServletTest
{
private Server server;
private ServerConnector connector;
private ServletContextHandler context;
private String path = "/path";
public void startServer(HttpServlet servlet) throws Exception
{
server = new Server();
connector = new ServerConnector(server);
server.addConnector(connector);
context = new ServletContextHandler(server, "/", false, false);
ServletHolder holder = new ServletHolder(servlet);
holder.setAsyncSupported(true);
context.addServlet(holder, path);
server.start();
}
@After
public void stopServer() throws Exception
{
server.stop();
}
@Test
public void testAsyncReadThrowsException() throws Exception
{
testAsyncReadThrows(new NullPointerException("explicitly_thrown_by_test"));
}
@Test
public void testAsyncReadThrowsError() throws Exception
{
testAsyncReadThrows(new Error("explicitly_thrown_by_test"));
}
private void testAsyncReadThrows(final Throwable throwable) throws Exception
{
final CountDownLatch latch = new CountDownLatch(1);
startServer(new HttpServlet()
{
@Override
protected void service(HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException
{
final AsyncContext asyncContext = request.startAsync(request, response);
request.getInputStream().setReadListener(new ReadListener()
{
@Override
public void onDataAvailable() throws IOException
{
if (throwable instanceof RuntimeException)
throw (RuntimeException)throwable;
if (throwable instanceof Error)
throw (Error)throwable;
throw new IOException(throwable);
}
@Override
public void onAllDataRead() throws IOException
{
}
@Override
public void onError(Throwable t)
{
Assert.assertThat("onError type",t,instanceOf(throwable.getClass()));
Assert.assertThat("onError message",t.getMessage(),is(throwable.getMessage()));
latch.countDown();
response.setStatus(500);
asyncContext.complete();
}
});
}
});
String data = "0123456789";
String request = "GET " + path + " HTTP/1.1\r\n" +
"Host: localhost:" + connector.getLocalPort() + "\r\n" +
"Content-Length: " + data.length() + "\r\n" +
"\r\n" +
data;
try (Socket client = new Socket("localhost", connector.getLocalPort()))
{
OutputStream output = client.getOutputStream();
output.write(request.getBytes("UTF-8"));
output.flush();
SimpleHttpParser parser = new SimpleHttpParser();
SimpleHttpResponse response = parser.readResponse(new BufferedReader(new InputStreamReader(client.getInputStream(), "UTF-8")));
Assert.assertTrue(latch.await(5, TimeUnit.SECONDS));
Assert.assertEquals("500", response.getCode());
}
}
@Test
public void testAsyncReadIdleTimeout() throws Exception
{
final int status = 567;
startServer(new HttpServlet()
{
@Override
protected void service(HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException
{
final AsyncContext asyncContext = request.startAsync(request, response);
asyncContext.setTimeout(0);
final ServletInputStream inputStream = request.getInputStream();
inputStream.setReadListener(new ReadListener()
{
@Override
public void onDataAvailable() throws IOException
{
while (inputStream.isReady() && !inputStream.isFinished())
inputStream.read();
}
@Override
public void onAllDataRead() throws IOException
{
}
@Override
public void onError(Throwable t)
{
response.setStatus(status);
// Do not put Connection: close header here, the test
// verifies that the server closes no matter what.
asyncContext.complete();
}
});
}
});
server.stop();
long idleTimeout = 1000;
connector.setIdleTimeout(idleTimeout);
server.start();
String data1 = "0123456789";
String data2 = "ABCDEF";
// Only send the first chunk of data and then let it idle timeout.
String request = "GET " + path + " HTTP/1.1\r\n" +
"Host: localhost:" + connector.getLocalPort() + "\r\n" +
"Content-Length: " + (data1.length() + data2.length()) + "\r\n" +
"\r\n" +
data1;
try (Socket client = new Socket("localhost", connector.getLocalPort()))
{
OutputStream output = client.getOutputStream();
output.write(request.getBytes("UTF-8"));
output.flush();
SimpleHttpParser parser = new SimpleHttpParser();
SimpleHttpResponse response = parser.readResponse(new BufferedReader(new InputStreamReader(client.getInputStream(), "UTF-8")));
assertEquals(String.valueOf(status), response.getCode());
// Make sure the connection was closed by the server.
assertEquals(-1, client.getInputStream().read());
}
}
@Test
public void testOnErrorThrows() throws Exception
{
final AtomicInteger errors = new AtomicInteger();
startServer(new HttpServlet()
{
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
final AsyncContext asyncContext = request.startAsync(request, response);
request.getInputStream().setReadListener(new ReadListener()
{
@Override
public void onDataAvailable() throws IOException
{
throw new NullPointerException("explicitly_thrown_by_test_1");
}
@Override
public void onAllDataRead() throws IOException
{
}
@Override
public void onError(Throwable t)
{
errors.incrementAndGet();
throw new NullPointerException("explicitly_thrown_by_test_2");
}
});
}
});
String data = "0123456789";
String request = "GET " + path + " HTTP/1.1\r\n" +
"Host: localhost:" + connector.getLocalPort() + "\r\n" +
"Content-Length: " + data.length() + "\r\n" +
"\r\n" +
data;
try (Socket client = new Socket("localhost", connector.getLocalPort()))
{
OutputStream output = client.getOutputStream();
output.write(request.getBytes("UTF-8"));
output.flush();
SimpleHttpParser parser = new SimpleHttpParser();
SimpleHttpResponse response = parser.readResponse(new BufferedReader(new InputStreamReader(client.getInputStream(), "UTF-8")));
Assert.assertEquals("500", response.getCode());
Assert.assertEquals(1, errors.get());
}
}
@Test
public void testAsyncWriteThrowsException() throws Exception
{
testAsyncWriteThrows(new NullPointerException("explicitly_thrown_by_test"));
}
@Test
public void testAsyncWriteThrowsError() throws Exception
{
testAsyncWriteThrows(new Error("explicitly_thrown_by_test"));
}
private void testAsyncWriteThrows(final Throwable throwable) throws Exception
{
final CountDownLatch latch = new CountDownLatch(1);
startServer(new HttpServlet()
{
@Override
protected void service(HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException
{
final AsyncContext asyncContext = request.startAsync(request, response);
response.getOutputStream().setWriteListener(new WriteListener()
{
@Override
public void onWritePossible() throws IOException
{
if (throwable instanceof RuntimeException)
throw (RuntimeException)throwable;
if (throwable instanceof Error)
throw (Error)throwable;
throw new IOException(throwable);
}
@Override
public void onError(Throwable t)
{
latch.countDown();
response.setStatus(500);
asyncContext.complete();
Assert.assertSame(throwable, t);
}
});
}
});
String request = "GET " + path + " HTTP/1.1\r\n" +
"Host: localhost:" + connector.getLocalPort() + "\r\n" +
"\r\n";
try (Socket client = new Socket("localhost", connector.getLocalPort()))
{
OutputStream output = client.getOutputStream();
output.write(request.getBytes("UTF-8"));
output.flush();
SimpleHttpParser parser = new SimpleHttpParser();
SimpleHttpResponse response = parser.readResponse(new BufferedReader(new InputStreamReader(client.getInputStream(), "UTF-8")));
Assert.assertTrue(latch.await(5, TimeUnit.SECONDS));
Assert.assertEquals("500", response.getCode());
}
}
@Test
public void testAsyncWriteClosed() throws Exception
{
final CountDownLatch latch = new CountDownLatch(1);
String text = "Now is the winter of our discontent. How Now Brown Cow. The quick brown fox jumped over the lazy dog.\n";
for (int i=0;i<10;i++)
text=text+text;
final byte[] data = text.getBytes(StandardCharsets.ISO_8859_1);
startServer(new HttpServlet()
{
@Override
protected void service(HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException
{
response.flushBuffer();
final AsyncContext async = request.startAsync();
final ServletOutputStream out = response.getOutputStream();
out.setWriteListener(new WriteListener()
{
@Override
public void onWritePossible() throws IOException
{
while (out.isReady())
{
try
{
Thread.sleep(100);
out.write(data);
}
catch(IOException e)
{
throw e;
}
catch(Exception e)
{
e.printStackTrace();
}
}
}
@Override
public void onError(Throwable t)
{
async.complete();
latch.countDown();
}
});
}
});
String request = "GET " + path + " HTTP/1.1\r\n" +
"Host: localhost:" + connector.getLocalPort() + "\r\n" +
"\r\n";
try (Socket client = new Socket("localhost", connector.getLocalPort()))
{
OutputStream output = client.getOutputStream();
output.write(request.getBytes("UTF-8"));
output.flush();
BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
String line=in.readLine();
assertThat(line, containsString("200 OK"));
while (line.length()>0)
line=in.readLine();
line=in.readLine();
assertThat(line, not(containsString(" ")));
line=in.readLine();
assertThat(line, containsString("discontent. How Now Brown Cow. The "));
}
if (!latch.await(5, TimeUnit.SECONDS))
Assert.fail();
}
@Test
public void testIsNotReadyAtEOF() throws Exception
{
String text = "Test\n";
final byte[] data = text.getBytes(StandardCharsets.ISO_8859_1);
startServer(new HttpServlet()
{
@Override
protected void service(HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException
{
response.flushBuffer();
final AsyncContext async = request.startAsync();
final ServletInputStream in = request.getInputStream();
final ServletOutputStream out = response.getOutputStream();
in.setReadListener(new ReadListener()
{
transient int _i=0;
transient boolean _minusOne=false;;
transient boolean _finished=false;;
@Override
public void onError(Throwable t)
{
t.printStackTrace();
async.complete();
}
@Override
public void onDataAvailable() throws IOException
{
while(in.isReady() && !in.isFinished())
{
int b = in.read();
if (b==-1)
_minusOne=true;
else if (data[_i++]!=b)
throw new IllegalStateException();
}
if (in.isFinished())
_finished=true;
}
@Override
public void onAllDataRead() throws IOException
{
out.write(String.format("i=%d eof=%b finished=%b",_i,_minusOne,_finished).getBytes(StandardCharsets.ISO_8859_1));
async.complete();
}
});
}
});
String request = "GET " + path + " HTTP/1.1\r\n" +
"Host: localhost:" + connector.getLocalPort() + "\r\n" +
"Content-Type: text/plain\r\n"+
"Content-Length: "+data.length+"\r\n" +
"Connection: close\r\n" +
"\r\n";
try (Socket client = new Socket("localhost", connector.getLocalPort()))
{
OutputStream output = client.getOutputStream();
output.write(request.getBytes("UTF-8"));
output.write(data);
output.flush();
BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
String line=in.readLine();
assertThat(line, containsString("200 OK"));
while (line.length()>0)
line=in.readLine();
line=in.readLine();
assertThat(line, containsString("i="+data.length+" eof=false finished=true"));
}
}
@Test
public void testEmptyAsyncRead() throws Exception
{
final AtomicBoolean oda = new AtomicBoolean();
final CountDownLatch latch = new CountDownLatch(1);
startServer(new HttpServlet()
{
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
final AsyncContext asyncContext = request.startAsync(request, response);
response.setStatus(200);
response.getOutputStream().close();
request.getInputStream().setReadListener(new ReadListener()
{
@Override
public void onDataAvailable() throws IOException
{
oda.set(true);
}
@Override
public void onAllDataRead() throws IOException
{
asyncContext.complete();
latch.countDown();
}
@Override
public void onError(Throwable t)
{
t.printStackTrace();
asyncContext.complete();
}
});
}
});
String request = "GET " + path + " HTTP/1.1\r\n" +
"Host: localhost:" + connector.getLocalPort() + "\r\n" +
"Connection: close\r\n" +
"\r\n";
try (Socket client = new Socket("localhost", connector.getLocalPort()))
{
OutputStream output = client.getOutputStream();
output.write(request.getBytes("UTF-8"));
output.flush();
String response = IO.toString(client.getInputStream());
assertThat(response,containsString(" 200 OK"));
// wait for onAllDataRead BEFORE closing client
latch.await();
}
// ODA not called at all!
Assert.assertFalse(oda.get());
}
}