diff --git a/VERSION.txt b/VERSION.txt index e60de54f0a9..42749263e05 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -6,6 +6,7 @@ jetty-7.5.0.RC1 - 19 August 2011 + 335001 Eliminate expected exceptions from log when running in JBoss + 355103 Make allowCredentials default to true in CrossOriginFilter + 355162 Allow creating an empty resource collection + + JETTY-1410 HTTP client handles CONTINUE 100 response correctly + JETTY-1414 HashLoginService doesn't refresh realm if specified config filename is not an absolute platform specific value jetty-7.5.0.RC0 - 15 August 2011 diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConnection.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConnection.java index fc01818e681..d6d6d60a615 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConnection.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConnection.java @@ -70,7 +70,7 @@ public class HttpConnection extends AbstractConnection implements Dumpable HttpConnection(Buffers requestBuffers, Buffers responseBuffers, EndPoint endp) { super(endp); - + _generator = new HttpGenerator(requestBuffers,endp); _parser = new HttpParser(responseBuffers,endp,new Handler()); } @@ -571,14 +571,24 @@ public class HttpConnection extends AbstractConnection implements Dumpable @Override public void startResponse(Buffer version, int status, Buffer reason) throws IOException { - HttpExchange exchange = _exchange; if (exchange!=null) { - // handle special case for CONNECT 200 responses - if (status==HttpStatus.OK_200 && HttpMethods.CONNECT.equalsIgnoreCase(exchange.getMethod())) - _parser.setHeadResponse(true); - + switch(status) + { + case HttpStatus.CONTINUE_100: + case HttpStatus.PROCESSING_102: + // TODO check if appropriate expect was sent in the request. + exchange.setEventListener(new NonFinalResponseListener(exchange)); + break; + + case HttpStatus.OK_200: + // handle special case for CONNECT 200 responses + if (HttpMethods.CONNECT.equalsIgnoreCase(exchange.getMethod())) + _parser.setHeadResponse(true); + break; + } + _http11 = HttpVersions.HTTP_1_1_BUFFER.equals(version); _status=status; exchange.getEventListener().onResponseStatus(version,status,reason); @@ -734,8 +744,10 @@ public class HttpConnection extends AbstractConnection implements Dumpable } } + /* ------------------------------------------------------------ */ private class ConnectionIdleTask extends Timeout.Task { + /* ------------------------------------------------------------ */ @Override public void expired() { @@ -746,4 +758,87 @@ public class HttpConnection extends AbstractConnection implements Dumpable } } } + + + /* ------------------------------------------------------------ */ + private class NonFinalResponseListener implements HttpEventListener + { + final HttpExchange _exchange; + final HttpEventListener _next; + + /* ------------------------------------------------------------ */ + public NonFinalResponseListener(HttpExchange exchange) + { + _exchange=exchange; + _next=exchange.getEventListener(); + } + + /* ------------------------------------------------------------ */ + public void onRequestCommitted() throws IOException + { + } + + /* ------------------------------------------------------------ */ + public void onRequestComplete() throws IOException + { + } + + /* ------------------------------------------------------------ */ + public void onResponseStatus(Buffer version, int status, Buffer reason) throws IOException + { + } + + /* ------------------------------------------------------------ */ + public void onResponseHeader(Buffer name, Buffer value) throws IOException + { + _next.onResponseHeader(name,value); + } + + /* ------------------------------------------------------------ */ + public void onResponseHeaderComplete() throws IOException + { + _next.onResponseHeaderComplete(); + } + + /* ------------------------------------------------------------ */ + public void onResponseContent(Buffer content) throws IOException + { + } + + /* ------------------------------------------------------------ */ + public void onResponseComplete() throws IOException + { + _exchange.setEventListener(_next); + _exchange.setStatus(HttpExchange.STATUS_WAITING_FOR_RESPONSE); + _parser.reset(); + } + + /* ------------------------------------------------------------ */ + public void onConnectionFailed(Throwable ex) + { + _exchange.setEventListener(_next); + _next.onConnectionFailed(ex); + } + + /* ------------------------------------------------------------ */ + public void onException(Throwable ex) + { + _exchange.setEventListener(_next); + _next.onException(ex); + } + + /* ------------------------------------------------------------ */ + public void onExpire() + { + _exchange.setEventListener(_next); + _next.onExpire(); + } + + /* ------------------------------------------------------------ */ + public void onRetry() + { + _exchange.setEventListener(_next); + _next.onRetry(); + } + } } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpExchange.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpExchange.java index f9454cceda1..d4de784909a 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpExchange.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpExchange.java @@ -302,6 +302,7 @@ public class HttpExchange { case STATUS_START: case STATUS_EXCEPTED: + case STATUS_WAITING_FOR_RESPONSE: set=_status.compareAndSet(oldStatus,newStatus); break; case STATUS_CANCELLING: @@ -344,7 +345,7 @@ public class HttpExchange } if (!set) - throw new IllegalStateException(oldStatus + " => " + newStatus); + throw new IllegalStateException(toState(oldStatus) + " => " + toState(newStatus)); } catch (IOException x) { @@ -726,11 +727,10 @@ public class HttpExchange return result; } - @Override - public String toString() + public static String toState(int s) { String state; - switch(getStatus()) + switch(s) { case STATUS_START: state="START"; break; case STATUS_WAITING_FOR_CONNECTION: state="CONNECTING"; break; @@ -746,6 +746,13 @@ public class HttpExchange case STATUS_CANCELLED: state="CANCELLED"; break; default: state="UNKNOWN"; } + return state; + } + + @Override + public String toString() + { + String state=toState(getStatus()); long now=System.currentTimeMillis(); long forMs = now -_lastStateChange; String s= String.format("%s@%x=%s//%s%s#%s(%dms)",getClass().getSimpleName(),hashCode(),_method,_address,_uri,state,forMs); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/Http100ContinueTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/Http100ContinueTest.java new file mode 100644 index 00000000000..b764de3c788 --- /dev/null +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/Http100ContinueTest.java @@ -0,0 +1,240 @@ +// ======================================================================== +// Copyright (c) Webtide LLC +// ------------------------------------------------------------------------ +// 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.client; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.http.HttpMethods; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.eclipse.jetty.server.nio.SelectChannelConnector; +import org.eclipse.jetty.util.log.Log; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + + +/* ------------------------------------------------------------ */ +public class Http100ContinueTest +{ + private static final int TIMEOUT = 500; + + private static final String CONTENT = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. In quis felis nunc. " + + "Quisque suscipit mauris et ante auctor ornare rhoncus lacus aliquet. Pellentesque " + + "habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. " + + "Vestibulum sit amet felis augue, vel convallis dolor. Cras accumsan vehicula diam " + + "at faucibus. Etiam in urna turpis, sed congue mi. Morbi et lorem eros. Donec vulputate " + + "velit in risus suscipit lobortis. Aliquam id urna orci, nec sollicitudin ipsum. " + + "Cras a orci turpis. Donec suscipit vulputate cursus. Mauris nunc tellus, fermentum " + + "eu auctor ut, mollis at diam. Quisque porttitor ultrices metus, vitae tincidunt massa " + + "sollicitudin a. Vivamus porttitor libero eget purus hendrerit cursus. Integer aliquam " + + "consequat mauris quis luctus. Cras enim nibh, dignissim eu faucibus ac, mollis nec neque. " + + "Aliquam purus mauris, consectetur nec convallis lacinia, porta sed ante. Suspendisse " + + "et cursus magna. Donec orci enim, molestie a lobortis eu, imperdiet vitae neque."; + + private static TestFeature _feature; + + private static Server _server; + private static TestHandler _handler; + private static HttpClient _client; + private static String _requestUrl; + + @BeforeClass + public static void init() throws Exception + { + File docRoot = new File("target/test-output/docroot/"); + if (!docRoot.exists()) + assertTrue(docRoot.mkdirs()); + docRoot.deleteOnExit(); + + _server = new Server(); + Connector connector = new SelectChannelConnector(); + _server.addConnector(connector); + + _handler = new TestHandler(); + _server.setHandler(_handler); + + _server.start(); + + _client = new HttpClient(); + _client.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL); + _client.setTimeout(TIMEOUT); + _client.setMaxRetries(0); + _client.start(); + + _requestUrl = "http://localhost:" + connector.getLocalPort() + "/"; + + } + + @AfterClass + public static void destroy() throws Exception + { + _client.stop(); + + _server.stop(); + _server.join(); + } + + @Test + public void testSuccess() throws Exception + { + // Handler to send CONTINUE 100 + _feature = TestFeature.CONTINUE; + + ContentExchange exchange = sendExchange(); + + int state = exchange.waitForDone(); + assertEquals(HttpExchange.STATUS_COMPLETED, state); + + int responseStatus = exchange.getResponseStatus(); + assertEquals(HttpStatus.OK_200,responseStatus); + + String content = exchange.getResponseContent(); + assertEquals(Http100ContinueTest.CONTENT,content); + } + + @Test + public void testMissingContinue() throws Exception + { + // Handler does not send CONTINUE 100 + _feature = TestFeature.NORMAL; + + ContentExchange exchange = sendExchange(); + + int state = exchange.waitForDone(); + assertEquals(HttpExchange.STATUS_COMPLETED, state); + + int responseStatus = exchange.getResponseStatus(); + assertEquals(HttpStatus.OK_200,responseStatus); + + String content = exchange.getResponseContent(); + assertEquals(Http100ContinueTest.CONTENT,content); + } + + @Test + public void testError() throws Exception + { + // Handler sends NOT FOUND 404 response + _feature = TestFeature.NOTFOUND; + + ContentExchange exchange = sendExchange(); + + int state = exchange.waitForDone(); + assertEquals(HttpExchange.STATUS_COMPLETED, state); + + int responseStatus = exchange.getResponseStatus(); + assertEquals(HttpStatus.NOT_FOUND_404,responseStatus); + } + + @Test + public void testTimeout() throws Exception + { + // Handler delays response till client times out + _feature = TestFeature.TIMEOUT; + + final CountDownLatch expires = new CountDownLatch(1); + ContentExchange exchange = new ContentExchange() + { + @Override + protected void onExpire() + { + expires.countDown(); + } + }; + + configureExchange(exchange); + _client.send(exchange); + + assertTrue(expires.await(TIMEOUT*10,TimeUnit.MILLISECONDS)); + } + + public ContentExchange sendExchange() throws Exception + { + ContentExchange exchange = new ContentExchange(); + + configureExchange(exchange); + _client.send(exchange); + + return exchange; + } + + public void configureExchange(ContentExchange exchange) + { + exchange.setURL(_requestUrl); + exchange.setMethod(HttpMethods.GET); + exchange.addRequestHeader("User-Agent","Jetty-Client/7.0"); + exchange.addRequestHeader("Expect","100-continue"); //server to send CONTINUE 100 + } + + private static class TestHandler extends AbstractHandler + { + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + if (baseRequest.isHandled()) + return; + + baseRequest.setHandled(true); + + switch (_feature) + { + case CONTINUE: + // force 100 Continue response to be sent + request.getInputStream(); + // next send data + + case NORMAL: + response.setContentType("text/plain"); + response.setStatus(HttpServletResponse.SC_OK); + response.getWriter().print(CONTENT); + break; + + case NOTFOUND: + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + break; + + case TIMEOUT: + try + { + Thread.sleep(TIMEOUT*4); + } + catch (InterruptedException ex) + { + Log.ignore(ex); + } + break; + } + } + } + + enum TestFeature { + CONTINUE, + NORMAL, + NOTFOUND, + TIMEOUT + } +}