oglueck 2002/12/09 01:16:17 Modified: httpclient/src/java/org/apache/commons/httpclient AutoCloseInputStream.java ChunkedInputStream.java ContentLengthInputStream.java HttpConnection.java HttpMethodBase.java MultiThreadedHttpConnectionManager.java SimpleHttpConnectionManager.java httpclient/src/java/org/apache/commons/httpclient/methods GetMethod.java HeadMethod.java PostMethod.java httpclient/src/test/org/apache/commons/httpclient TestGetMethodLocal.java TestHttpClientLocalHost.java TestMethodsLocalHost.java TestMethodsNoHost.java Added: httpclient/src/java/org/apache/commons/httpclient ResponseConsumedWatcher.java Log: Handle closing of streams properly to avoid response parse failures. Contributed by: Eric Johnson. Revision Changes Path 1.3 +91 -23 jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/AutoCloseInputStream.java Index: AutoCloseInputStream.java =================================================================== RCS file: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/AutoCloseInputStream.java,v retrieving revision 1.2 retrieving revision 1.3 diff -u -r1.2 -r1.3 --- AutoCloseInputStream.java 9 Sep 2002 05:43:39 -0000 1.2 +++ AutoCloseInputStream.java 9 Dec 2002 09:16:16 -0000 1.3 @@ -67,44 +67,58 @@ import java.io.InputStream; /** - * Closes a HttpConnection as soon as the end of the stream is reached. + * Closes an underlying stream as soon as the end of the stream is reached, and + * notifies a client when it has done so. + * * @author Ortwin Glück + * @author Eric Johnson * * @since 2.0 */ class AutoCloseInputStream extends FilterInputStream { - /** the connection the input stream comes from */ - private HttpConnection conn; + + // assume that the underlying stream is open until we get an EOF indication. + private boolean streamOpen = true; + + private boolean selfClosed = false; + + /** The watcher is notified when the contents of the stream have been exhausted */ + private ResponseConsumedWatcher watcher = null; /** * Create a new auto closing stream for the provided connection - * + * * @param in the input stream to read from - * @param conn the connection to close when done reading + * @param watcher To be notified when the contents of the stream have been + * consumed. */ - public AutoCloseInputStream(InputStream in, HttpConnection conn) { + public AutoCloseInputStream(InputStream in, ResponseConsumedWatcher watcher) { super(in); - this.conn = conn; + this.watcher = watcher; } /** * Reads the next byte of data from the input stream. - * + * * @throws IOException when there is an error reading * @return the character read, or -1 for EOF */ public int read() throws IOException { - int l = super.read(); - if (l == -1) { - conn.close(); + int l = -1; + + if (isReadAllowed()) { + // underlying stream not closed, go ahead and read. + l = super.read(); + checkClose(l); } + return l; } /** * Reads up to <code>len</code> bytes of data from the stream. - * + * * @param b a <code>byte</code> array to read data into * @param off an offset within the array to store data * @param len the maximum number of bytes to read @@ -112,26 +126,80 @@ * @throws IOException if there are errors reading */ public int read(byte[] b, int off, int len) throws IOException { - int l = super.read(b, off, len); - if (l == -1) { - conn.close(); + int l = -1; + + if ( isReadAllowed() ) { + l = super.read(b, off, len); + checkClose(l); } + return l; } /** * Reads some number of bytes from the input stream and stores them into the * buffer array b. - * + * * @param b a <code>byte</code> array to read data into * @return the number of bytes read or -1 for EOF * @throws IOException if there are errors reading */ public int read(byte[] b) throws IOException { - int l = super.read(b); - if (l == -1) { - conn.close(); + int l = -1; + + if ( isReadAllowed() ) { + l = super.read(b); + checkClose(l); } return l; } -} \ No newline at end of file + + /** + * Close the stream, and also close the underlying stream if it is not + * already closed. + */ + public void close() throws IOException { + if (!selfClosed) { + selfClosed = true; + notifyWatcher(); + } + } + + /** + * Close the underlying stream should the end of the stream arrive. + * + * @param readResult The result of the read operation to check. + */ + private void checkClose(int readResult) throws IOException { + if (readResult == -1) { + notifyWatcher(); + } + } + + /** + * See whether a read of the underlying stream should be allowed, and if + * not, check to see whether our stream has already been closed! + * + * @return <code>true</code> if it is still OK to read from the stream. + */ + private boolean isReadAllowed() throws IOException { + if (!streamOpen && selfClosed) { + throw new IOException("Attempted read on closed stream."); + } + return streamOpen; + } + + /** + * Notify the watcher that the contents have been consumed. + */ + private void notifyWatcher() throws IOException { + if (streamOpen) { + super.close(); + streamOpen = false; + + if (watcher != null) + watcher.responseConsumed(); + } + } +} + 1.7 +52 -6 jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/ChunkedInputStream.java Index: ChunkedInputStream.java =================================================================== RCS file: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/ChunkedInputStream.java,v retrieving revision 1.6 retrieving revision 1.7 diff -u -r1.6 -r1.7 --- ChunkedInputStream.java 16 Oct 2002 13:14:11 -0000 1.6 +++ ChunkedInputStream.java 9 Dec 2002 09:16:16 -0000 1.7 @@ -63,17 +63,22 @@ package org.apache.commons.httpclient; import java.io.*; -import java.util.*; /** * <p>Transparently coalesces chunks of a HTTP stream that uses Transfer-Encoding * chunked.</p> * + * <p>Note that this class NEVER closes the underlying stream, even when close gets + * called. Instead, it will read until the "end" of its chunking on close, which + * allows for the seamless invocation of subsequent HTTP 1.1 calls, while not + * requiring the client to remember to read the entire contents of the response.</p> + * * @see ResponseInputStream * * @author Ortwin Glück * @author Sean C. Sullivan * @author Martin Elwin + * @author Eric Johnson * * @since 2.0 * @@ -83,6 +88,7 @@ private InputStream in; private int chunkSize, pos; private boolean eof = false; + private boolean closed = false; private static final String HTTP_ENC = "US-ASCII"; private HttpMethod method; @@ -96,7 +102,7 @@ * @throws java.lang.NullPointerException * */ - public ChunkedInputStream(final InputStream in, final HttpMethod method) throws IOException { + public ChunkedInputStream(InputStream in, HttpMethod method) throws IOException { if (null == in) { throw new NullPointerException("InputStream parameter"); } @@ -124,6 +130,9 @@ * @throws IOException */ public int read() throws IOException { + + if (closed) + throw new IOException("Attempted read from closed stream."); if (eof) return -1; if (pos >= chunkSize) { nextChunk(); @@ -134,6 +143,10 @@ } public int read(byte[] b, int off, int len) throws java.io.IOException { + + if (closed) + throw new IOException("Attempted read from closed stream."); + if (eof) return -1; if (pos >= chunkSize) { nextChunk(); @@ -274,7 +287,40 @@ return (buf.toString()); } + /** + * Upon close, this reads the remainder of the chunked message, + * leaving the underlying socket at a position to start reading the + * next response without scanning. + */ public void close() throws IOException { - in.close(); + if (!closed) { + try { + if (!eof) { + exhaustInputStream(this); + } + } + finally { + eof = true; + closed = true; + } + } + } + + /** + * Exhaust an input stream, reading until EOF has been encountered. + * + * <p>Note that this function is intended as a non-public utility. + * This is a little weird, but it seemed silly to make a utility + * class for this one function, so instead it is just static and + * shared that way.</p> + * + * @param inStream The {@link InputStream} to exhaust. + */ + static void exhaustInputStream(InputStream inStream) throws IOException { + // read and discard the remainder of the message + byte buffer[] = new byte[1024]; + while ( inStream.read(buffer) >= 0) { + ; + } } } 1.3 +47 -6 jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/ContentLengthInputStream.java Index: ContentLengthInputStream.java =================================================================== RCS file: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/ContentLengthInputStream.java,v retrieving revision 1.2 retrieving revision 1.3 diff -u -r1.2 -r1.3 --- ContentLengthInputStream.java 19 Sep 2002 10:15:08 -0000 1.2 +++ ContentLengthInputStream.java 9 Dec 2002 09:16:16 -0000 1.3 @@ -68,6 +68,7 @@ * Cuts the wrapped InputStream off after a specified number of bytes. * * @author Ortwin Glück + * @author Eric Johnson * @since 2.0 */ @@ -75,6 +76,8 @@ private int contentLength; private int pos = 0; + private boolean closed = false; + /** * Creates a new length limited stream * @@ -87,15 +90,53 @@ this.contentLength = contentLength; } + /** + * Reads until the end of the known length of content. + * + * <p>Does not close the underlying socket input, but instead leaves it + * primed to parse the next response.</p> + */ + public void close() throws IOException { + if (!closed) { + try { + ChunkedInputStream.exhaustInputStream(this); + } finally { + // close after above so that we don't throw an exception trying + // to read after closed! + closed = true; + } + } + } + public int read() throws java.io.IOException { - if (pos >= contentLength) return -1; + if (closed) + throw new IOException("Attempted read from closed stream."); + + if (pos >= contentLength) + return -1; pos++; return super.read(); } - + /** + * Does standard {@link InputStream#read(byte[], int, int)} behavior, but + * also notifies the watcher when the contents have been consumed. + * + * @param b The byte array to fill. + * @param off Start filling at this position. + * @param len The number of bytes to attempt to read. + * @return The number of bytes read, or -1 if the end of content has been + * reached. + * + * @throws java.io.IOException Should an error occur on the wrapped stream. + */ public int read(byte[] b, int off, int len) throws java.io.IOException { - if (pos >= contentLength) return -1; + if (closed) + throw new IOException("Attempted read from closed stream."); + + if (pos >= contentLength) + return -1; + if (pos + len > contentLength) { len = contentLength - pos; } 1.27 +40 -4 jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/HttpConnection.java Index: HttpConnection.java =================================================================== RCS file: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/HttpConnection.java,v retrieving revision 1.26 retrieving revision 1.27 diff -u -r1.26 -r1.27 --- HttpConnection.java 3 Dec 2002 05:46:15 -0000 1.26 +++ HttpConnection.java 9 Dec 2002 09:16:16 -0000 1.27 @@ -306,6 +306,37 @@ return (!(null == _proxyHost || 0 >= _proxyPort)); } + /** + * Set the state to keep track of the last response for the last request. + * + * <p>The connection managers use this to ensure that previous requests are + * properly closed before a new request is attempted. That way, a GET + * request need not be read in its entirety before a new request is issued. + * Instead, this stream can be closed as appropriate.</p> + * + * @param inStream The stream associated with an HttpMethod. + */ + public void setLastResponseInputStream(InputStream inStream) { + _lastResponseInput = inStream; + } + + /** + * Returns the stream used to read the last response's body. + * + * <p>Clients will generally not need to call this function unless + * using HttpConnection directly, instead of calling {@link HttpClient#executeMethod}. + * For those clients, call this function, and if it returns a non-null stream, + * close the stream before attempting to execute a method. Note that + * calling "close" on the stream returned by this function <i>may</i> close + * the connection if the previous response contained a "Connection: close" header. </p> + * + * @return An {@link InputStream} corresponding to the body of the last + * response. + */ + public InputStream getLastResponseInputStream() { + return _lastResponseInput; + } + // --------------------------------------------------- Other Public Methods /** @@ -776,6 +807,9 @@ protected void closeSocketAndStreams() { log.trace("enter HttpConnection.closeSockedAndStreams()"); + // no longer care about previous responses... + _lastResponseInput = null; + if (null != _input) { try { _input.close(); @@ -891,6 +925,8 @@ private InputStream _input = null; /** My OutputStream. */ private OutputStream _output = null; + /** An {@link InputStream} for the response to an individual request. */ + private InputStream _lastResponseInput = null; /** Whether or not I am connected. */ private boolean _open = false; /** Whether or not I am/should connect via SSL. */ 1.85 +210 -194 jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/HttpMethodBase.java Index: HttpMethodBase.java =================================================================== RCS file: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/HttpMethodBase.java,v retrieving revision 1.84 retrieving revision 1.85 diff -u -r1.84 -r1.85 --- HttpMethodBase.java 8 Dec 2002 18:51:06 -0000 1.84 +++ HttpMethodBase.java 9 Dec 2002 09:16:16 -0000 1.85 @@ -158,6 +158,7 @@ * @author <a href="mailto:[EMAIL PROTECTED]">Jeff Dever</a> * @author <a href="mailto:[EMAIL PROTECTED]">Davanum Srinivas</a> * @author Ortwin Glück + * @author Eric Johnson */ public abstract class HttpMethodBase implements HttpMethod { //~ Static variables/initializers ·········································· @@ -235,12 +236,19 @@ /** Whether or not I have been executed. */ private boolean used = false; + /** How many times did this transparently handle a recoverable exception? */ + private int recoverableExceptionCount = 0; + /** * The maximum number of attempts to attempt recovery from an * HttpRecoverableException. */ private int maxRetries = 3; + private boolean inExecute = false; + + private boolean doneWithConnection = false; + /** Default content encoding chatset */ protected static final String DEFAULT_CHARSET = "ISO-8859-1"; @@ -568,6 +576,8 @@ while ((len = is.read(buffer)) > 0) { os.write(buffer, 0, len); } + is.close(); + os.close(); responseBody = os.toByteArray(); setResponseStream(null); log.debug("buffering response body"); @@ -592,9 +602,9 @@ return responseStream; } if (responseBody != null) { - responseStream = new ByteArrayInputStream(responseBody); + InputStream byteResponseStream = new ByteArrayInputStream(responseBody); log.debug("re-creating response stream from byte array"); - return responseStream; + return byteResponseStream; } return null; } @@ -719,25 +729,7 @@ setRequestHeader(header); } - - /** - * Close the provided HTTP connection, if: - * http 1.0 and not using the 'connect' method, or - * http 1.1 and the Connection: close header is sent - * - * @param connection the HTTP connection to process - * Add the specified request header. If a header of the same name already - * exists, the new value will be appended onto the the existing value - * list. A <i>header</i> value of <code>null</code> will be ignored. Note - * that header-name matching is case insensitive. - */ - private void closeConnection(HttpConnection connection) { - if (shouldCloseConnection()) { - connection.close(); - } - } - - private boolean shouldCloseConnection() { + protected boolean shouldCloseConnection() { if (!http11) { if (getName().equals(ConnectMethod.NAME) && (statusLine.getStatusCode() == HttpStatus.SC_OK)) { @@ -759,13 +751,59 @@ return false; } - private void wrapResponseStream( HttpConnection connection ) { + private boolean isRetryNeeded(int statusCode, HttpState state, HttpConnection conn) { + switch (statusCode) { + case HttpStatus.SC_UNAUTHORIZED: + case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED: + log.debug("Authorization required"); + if (doAuthentication) { //process authentication response + //if the authentication is successful, return the statusCode + //otherwise, drop through the switch and try again. + if (processAuthenticationResponse(state)) { + return false; + } + } else { //let the client handle the authenticaiton + return false; + } + break; + + case HttpStatus.SC_MOVED_TEMPORARILY: + case HttpStatus.SC_MOVED_PERMANENTLY: + case HttpStatus.SC_TEMPORARY_REDIRECT: + log.debug("Redirect required"); + + if (! processRedirectResponse(conn)) { + return false; + } + break; + + default: + // neither an unauthorized nor a redirect response + return false; + } //end of switch + + return true; + } + + private void checkExecuteConditions(HttpState state, HttpConnection conn) + throws HttpException { - if ( responseStream != null ) { - this.responseConnection = connection; - this.responseStream = new ResponseAutoReleaseInputStream(responseStream); + if (null == state) { + throw new NullPointerException("HttpState parameter"); + } + if (null == conn) { + throw new NullPointerException("HttpConnection parameter"); + } + if (hasBeenUsed()) { + throw new HttpException("Already used, but not recycled."); + } + if (!validate()) { + throw new HttpException("Not valid"); } + if (inExecute) { + throw new IllegalStateException("Execute invoked recursively, or exited abnormally."); + } } /** @@ -795,104 +833,83 @@ throws HttpException, IOException, NullPointerException { log.trace("enter HttpMethodBase.execute(HttpState, HttpConnection)"); - //TODO: This method is too large - //check some error conditions - if (null == state) { - throw new NullPointerException("HttpState parameter"); - } - if (null == conn) { - throw new NullPointerException("HttpConnection parameter"); - } - if (hasBeenUsed()) { - throw new HttpException("Already used, but not recycled."); - } - if (!validate()) { - throw new HttpException("Not valid"); - } - - //pre-emptively add the authorization header, if required. - Authenticator.authenticate(this, state); - if (conn.isProxied()) { - Authenticator.authenticateProxy(this, state); - } + checkExecuteConditions(state, conn); + inExecute = true; - //Set visited = new HashSet(); - realms = new HashSet(); - proxyRealms = new HashSet(); - int forwardCount = 0; //protect from an infinite loop + try { + //TODO: This method is too large - while (forwardCount++ < maxForwards) { - if (log.isDebugEnabled()) { - log.debug("Execute loop try " + forwardCount); - } + //pre-emptively add the authorization header, if required. + Authenticator.authenticate(this, state); + if (conn.isProxied()) { + Authenticator.authenticateProxy(this, state); + } + + //Set visited = new HashSet(); + realms = new HashSet(); + proxyRealms = new HashSet(); + int forwardCount = 0; //protect from an infinite loop + + while (forwardCount++ < maxForwards) { + // on every retry, reset this state information. + responseConnection = conn; + conn.setLastResponseInputStream(null); - //write the request and read the response, will retry - processRequest(state, conn); + if (log.isDebugEnabled()) { + log.debug("Execute loop try " + forwardCount); + } - //if SC_CONTINUE write the request body - writeRemainingRequestBody(state, conn); + //write the request and read the response, will retry + processRequest(state, conn); - int statusCode = statusLine.getStatusCode(); - switch (statusCode) { - case HttpStatus.SC_UNAUTHORIZED: - case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED: - log.debug("Authorization required"); - if (doAuthentication) { //process authentication response - //if the authentication is successful, return the statusCode - //otherwise, drop through the switch and try again. - if (processAuthenticationResponse(state, conn)) { - wrapResponseStream(conn); - return statusCode; - } - } else { //let the client handle the authenticaiton - wrapResponseStream(conn); - return statusCode; - } - break; + //if SC_CONTINUE write the request body + writeRemainingRequestBody(state, conn); - case HttpStatus.SC_MOVED_TEMPORARILY: - case HttpStatus.SC_MOVED_PERMANENTLY: - case HttpStatus.SC_TEMPORARY_REDIRECT: - log.debug("Redirect required"); - - if (! processRedirectResponse(state, conn)) { - wrapResponseStream(conn); - return statusCode; - } + if (!isRetryNeeded(statusLine.getStatusCode(), state, conn)) { + // nope, no retry needed, exit loop. break; + } + /* + Revisiting may be desired. We do not know about the server's internal state. - default: - // neither an unauthorized nor a redirect response - wrapResponseStream(conn); + //check to see if we have visited this url before + if (visited.contains(generateVisitedKey(conn))) { + log.error("Link " + generateVisitedKey(conn) + "' revisited"); return statusCode; - } //end of switch + } + visited.add(generateVisitedKey(conn)); + */ -/* - Revisiting may be desired. We do not know about the server's internal state. + // retry - close previous stream. Caution - this causes + // responseBodyConsumed to be called, which may also close the + // connection. + if (responseStream != null) { + responseStream.close(); + } - //check to see if we have visited this url before - if (visited.contains(generateVisitedKey(conn))) { - log.error("Link " + generateVisitedKey(conn) + "' revisited"); - return statusCode; - } - visited.add(generateVisitedKey(conn)); -*/ + } //end of retry loop - //close connection if required - closeConnection(conn); - if (conn.isOpen()) { - //throw away body to position the stream after the response - getResponseBodyAsString(); + if (forwardCount >= maxForwards) { + log.error("Narrowly avoided an infinite loop in execute"); + throw new HttpRecoverableException("Maximum redirects ("+ maxForwards +") exceeded"); + } + } + finally { + inExecute = false; + // If the response has been fully processed, return the connection + // to the pool. Use this flag, rather than other tests (like + // responseStream == null), as subclasses, might reset the stream, + // for example, reading the entire response into a file and then + // setting the file as the stream. + if (doneWithConnection) { + ensureConnectionRelease(); } - } //end of loop - - wrapResponseStream(conn); + } - log.error("Narrowly avoided an infinite loop in execute"); - throw new HttpRecoverableException("Maximum redirects ("+ maxForwards +") exceeded"); + return statusLine.getStatusCode(); } - private boolean processRedirectResponse(HttpState state, HttpConnection conn) { + private boolean processRedirectResponse(HttpConnection conn) { if (!getFollowRedirects()) { log.info("Redirect requested but followRedirects is " @@ -967,8 +984,8 @@ * Check for a valid redirect given the current conn and new url. * Redirect to a different protocol, host or port are checked for validity. * - * @param conn The existing HttpConnection - * @param url The new URL to redirect to + * @param currentUrl The current URL (redirecting from) + * @param redirectUrl The new URL to redirect to * @throws HttpException if the redirect is invalid * @since 2.0 */ @@ -1056,6 +1073,9 @@ http11 = true; bodySent = false; responseBody = null; + recoverableExceptionCount = 0; + inExecute = false; + doneWithConnection = false; } /** @@ -1065,10 +1085,13 @@ */ public void releaseConnection() { - if ( responseConnection != null ) { - responseConnection.releaseConnection(); - this.responseConnection = null; - this.responseStream = null; + if (responseStream != null) { + try { + // FYI - this may indirectly invoke responseBodyConsumed. + responseStream.close(); + } catch (IOException e) { + // attempting cleanup, don't care about exception. + } } } @@ -1603,9 +1626,10 @@ * Read the response body from the given {@link HttpConnection}. * * <p> - * The current implementation simply consumes the expected response body - * (according to the values of the <tt>Content-Length</tt> and - * <tt>Transfer-Encoding</tt> headers, if any). + * The current implementation wraps the socket level stream with + * an appropriate stream for the type of response (chunked, content-length, + * or auto-close). If there is no response body, the connection associated + * with the request will be returned to the connection manager. * </p> * * <p> @@ -1626,7 +1650,17 @@ log.trace( "enter HttpMethodBase.readResponseBody(HttpState, HttpConnection)"); - setResponseStream(_readResponseBody(state, conn)); + // assume we are not done with the connection if we get a stream + doneWithConnection = false; + InputStream stream = _readResponseBody(conn); + if (stream == null) { + // done using the connection! + responseBodyConsumed(); + } + else { + conn.setLastResponseInputStream(stream); + setResponseStream(stream); + } } /** @@ -1641,11 +1675,10 @@ * @see #readResponse * @see #processResponseBody * - * @param state the client state * @param conn the {@link HttpConnection} to read the response from * @return InputStream to read the response body from */ - private InputStream _readResponseBody(HttpState state, HttpConnection conn) + private InputStream _readResponseBody(HttpConnection conn) throws IOException { log.trace("enter HttpMethodBase.readResponseBody(HttpState, HttpConnection)"); @@ -1697,12 +1730,12 @@ && !getName().equals(ConnectMethod.NAME)){ result = is; } - if (result == null) { - return null; - } - if (shouldCloseConnection()) { - result = new AutoCloseInputStream(result, conn); + // if there is a result - ALWAYS wrap it in an observer which will + // close the underlying stream as soon as it is consumed, and notify + // the watcher that the stream has been consumed. + if (result != null) { + result = new AutoCloseInputStream(result, m_responseWatcher); } return result; @@ -2100,29 +2133,14 @@ } /** - * Generates a key used for idenifying visited URLs. - * - * @param conn DOCUMENT ME! - * - * @return DOCUMENT ME! - */ - private String generateVisitedKey(HttpConnection conn) { - return conn.getHost() + ":" + conn.getPort() + "|" - + generateRequestLine(conn, getName(), getPath(), - getQueryString(), getHttpVersion()); - } - - /** * process a response that requires authentication * * @param state the current state - * @param connection the connection for communication * * @return true if the request has completed process, false if more * attempts are needed */ - private boolean processAuthenticationResponse(HttpState state, - HttpConnection connection) { + private boolean processAuthenticationResponse(HttpState state) { log.trace("enter HttpMethodBase.processAuthenticationResponse(" + "HttpState, HttpConnection)"); @@ -2210,8 +2228,8 @@ * @throws IOException when an I/O error occurs communicating with the * server * - * @see writeRequest(HttpState,HttpConnection) - * @see readResponse(HttpState,HttpConnection) + * @see #writeRequest(HttpState,HttpConnection) + * @see #readResponse(HttpState,HttpConnection) */ private void processRequest(HttpState state, HttpConnection connection) throws HttpException, IOException { @@ -2238,6 +2256,9 @@ log.debug("Closing the connection."); } + // update the recoverable exception count. + recoverableExceptionCount++; + connection.close(); log.info("Recoverable exception caught when writing request"); if (retryCount == maxRetries) { @@ -2335,64 +2356,59 @@ } /** - * Releases this connection from its connectionManager when the response has - * been read. + * Returns the number of "recoverable" exceptions thrown and handled, to + * allow for monitoring the quality of the connection. + * + * @return The number of recoverable exceptions handled by the method. */ - private class ResponseAutoReleaseInputStream extends InputStream { + public int getRecoverableExceptionCount() { + return recoverableExceptionCount; + } - private InputStream is; + /** + * A response has been consumed. + * + * <p>The default behavior for this class is to check to see if the connection + * should be closed, and close if need be, and to ensure that the connection + * is returned to the connection manager - if and only if we are not still + * inside the execute call.</p> + * + */ + protected void responseBodyConsumed() { - public ResponseAutoReleaseInputStream(InputStream is) { - this.is = is; - } + // make sure this is the initial invocation of the notification, + // ignore subsequent ones. + responseStream = null; + responseConnection.setLastResponseInputStream(null); - /** - * @see java.io.InputStream#close() - */ - public void close() throws IOException { - is.close(); - releaseConnection(); + if (shouldCloseConnection()) { + responseConnection.close(); } - /** - * @see java.io.InputStream#read() - */ - public int read() throws IOException { - int b = is.read(); - - if ( b == -1 ) { - releaseConnection(); - } - - return b; + doneWithConnection = true; + if (!inExecute) { + ensureConnectionRelease(); } + } - /** - * @see java.io.InputStream#read(byte, int, int) - */ - public int read(byte[] array, int off, int len) throws IOException { - int b = is.read(array, off, len); - - if ( b == -1 ) { - releaseConnection(); - } - - return b; + /** + * Insure that the connection is released back to the pool. + */ + private void ensureConnectionRelease() { + if ( responseConnection != null ) { + responseConnection.releaseConnection(); + responseConnection = null; } + } - /** - * @see java.io.InputStream#read(byte) - */ - public int read(byte[] array) throws IOException { - int b = is.read(array); - - if ( b == -1 ) { - releaseConnection(); - } - - return b; + /** + * This exists so that the public interface to this class need not include + * either the responseConsumed or the responseBodyConsumed methods. + */ + private ResponseConsumedWatcher m_responseWatcher = new ResponseConsumedWatcher() { + public void responseConsumed() { + responseBodyConsumed(); } - - } + }; } 1.2 +59 -56 jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/MultiThreadedHttpConnectionManager.java Index: MultiThreadedHttpConnectionManager.java =================================================================== RCS file: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/MultiThreadedHttpConnectionManager.java,v retrieving revision 1.1 retrieving revision 1.2 diff -u -r1.1 -r1.2 --- MultiThreadedHttpConnectionManager.java 3 Dec 2002 05:46:15 -0000 1.1 +++ MultiThreadedHttpConnectionManager.java 9 Dec 2002 09:16:16 -0000 1.2 @@ -77,11 +77,12 @@ * Manages a set of HttpConnections for various host:ports. * * @author <a href="mailto:[EMAIL PROTECTED]">Michael Becke</a> - * + * @author Eric Johnson + * * @since 2.0 */ public class MultiThreadedHttpConnectionManager implements HttpConnectionManager { - + // -------------------------------------------------------- Class Variables /** Log object for this class. */ private static final Log log = LogFactory.getLog(MultiThreadedHttpConnectionManager.class); @@ -100,12 +101,12 @@ * No-args constructor */ public MultiThreadedHttpConnectionManager() { - + this.referenceToHostPort = Collections.synchronizedMap( new HashMap() ); this.referenceQueue = new ReferenceQueue(); - + new ReferenceQueueThread().start(); - + } /** @@ -132,23 +133,23 @@ */ public HttpConnection getConnection(HostConfiguration hostConfiguration) throws MalformedURLException { - + while( true ) { try { return getConnection(hostConfiguration, 0); } catch ( HttpException e ) { - log.debug( - "Unexpected exception while waiting for connection", - e + log.debug( + "Unexpected exception while waiting for connection", + e ); }; } - + } /** * Return the port provided if not -1 (default), return 443 if the - * protocol is HTTPS, otherwise 80. + * protocol is HTTPS, otherwise 80. * * This functionality is a URLUtil and may be better off in URIUtils * @@ -171,11 +172,11 @@ } return portForProtocol; } - + /** * @see HttpConnectionManager#getConnection(HostConfiguration, long) */ - public HttpConnection getConnection(HostConfiguration hostConfiguration, long timeout) + public HttpConnection getConnection(HostConfiguration hostConfiguration, long timeout) throws HttpException, MalformedURLException { log.trace("enter HttpConnectionManager.getConnection(HostConfiguration, long)"); @@ -187,7 +188,7 @@ String protocol = hostConfiguration.getProtocol(); String host = hostConfiguration.getHost(); int port = MultiThreadedHttpConnectionManager.getPort( - protocol, + protocol, hostConfiguration.getPort() ); String hostAndPort = host + ":" + port; @@ -199,8 +200,8 @@ // Look for a list of connections for the given host:port HostConnectionPool connectionPool = getConnectionPool(hostAndPort); - - HttpConnection conn = getConnection( + + HttpConnection conn = getConnection( connectionPool, host, port, @@ -217,7 +218,7 @@ * Gets a connection or waits if one is not available. A connection is * available if one exists that is not being used or if fewer than * maxConnections have been created in the connectionPool. - * + * * @param connectionPool * @param host * @param port @@ -226,9 +227,9 @@ * @param proxyPort * @param timeout the number of milliseconds to wait for a connection, 0 to * wait indefinitely - * + * * @return HttpConnection an available connection - * + * * @throws HttpException if a connection does not available in 'timeout' * milliseconds */ @@ -241,15 +242,15 @@ int proxyPort, long timeout ) throws HttpException { - + HttpConnection connection = null; - + synchronized( connectionPool ) { - - // keep trying until a connection is available, should happen at + + // keep trying until a connection is available, should happen at // most twice while ( connection == null ) { - + if (connectionPool.freeConnections.size() > 0) { connection = (HttpConnection)connectionPool.freeConnections.removeFirst(); } else { @@ -257,9 +258,9 @@ if (connectionPool.numConnections < maxConnections) { // Create a new connection connection = new HttpConnection( - proxyHost, - proxyPort, - host, + proxyHost, + proxyPort, + host, port, useHttps ); @@ -271,14 +272,14 @@ new WeakReference( connection, referenceQueue ), host + ":" + port ); - + } else { - + TimeoutThread threadTimeout = new TimeoutThread(); threadTimeout.setTimeout(timeout); threadTimeout.setWakeupThread(Thread.currentThread()); threadTimeout.start(); - + try { log.debug( "HttpConnectionManager.getConnection: waiting for " @@ -291,14 +292,14 @@ } catch (InterruptedException e) { throw new HttpException("Timeout waiting for connection."); } - + } } } - + } - - return connection; + + return connection; } /** @@ -322,7 +323,7 @@ } return listConnections; } - + /** * Get the number of connections in use for the key * @@ -334,21 +335,23 @@ HostConnectionPool connectionPool = getConnectionPool(hostAndPort); synchronized( connectionPool ) { - return connectionPool.numConnections; + return connectionPool.numConnections; } } - + /** * Make the given HttpConnection available for use by other requests. * If another thread is blocked in getConnection() waiting for a connection * for this host:port, they will be woken up. - * + * * @param conn - The HttpConnection to make available. */ public void releaseConnection(HttpConnection conn) { log.trace("enter HttpConnectionManager.releaseConnection(HttpConnection)"); + // make sure that the response has been read. + SimpleHttpConnectionManager.finishLastResponse(conn); String host = conn.getHost(); int port = conn.getPort(); String key = host + ":" + port; @@ -369,38 +372,38 @@ listConnections.notify(); } } - + /** * A simple struct-link class to combine the connection list and the count * of created connections. */ private class HostConnectionPool { - + public LinkedList freeConnections = new LinkedList(); public int numConnections = 0; - + } /** * A thread for listening for HttpConnections reclaimed by the garbage * collector. - */ + */ private class ReferenceQueueThread extends Thread { - + public ReferenceQueueThread() { - setDaemon(true); + setDaemon(true); } - + /** * @see java.lang.Runnable#run() */ public void run() { - + while(true) { - + try { Reference ref = referenceQueue.remove(); - + if ( ref != null ) { String hostPort = (String)referenceToHostPort.get(ref); referenceToHostPort.remove(ref); @@ -413,9 +416,9 @@ } catch (InterruptedException e) { log.debug("ReferenceQueueThread interrupted", e); } - + } - + } } @@ -454,7 +457,7 @@ } public void run() { - log.trace("TimeoutThread.run()"); + log.trace("TimeoutThread.run()"); if(timeout == 0){ return; } @@ -466,10 +469,10 @@ sleep(timeout); thrdWakeup.interrupt(); }catch(InterruptedException e){ - log.debug("InterruptedException caught as expected"); + log.debug("InterruptedException caught as expected"); // This is expected } } } - + } 1.2 +43 -16 jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/SimpleHttpConnectionManager.java Index: SimpleHttpConnectionManager.java =================================================================== RCS file: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/SimpleHttpConnectionManager.java,v retrieving revision 1.1 retrieving revision 1.2 diff -u -r1.1 -r1.2 --- SimpleHttpConnectionManager.java 3 Dec 2002 05:46:15 -0000 1.1 +++ SimpleHttpConnectionManager.java 9 Dec 2002 09:16:16 -0000 1.2 @@ -62,14 +62,17 @@ package org.apache.commons.httpclient; import java.net.MalformedURLException; +import java.io.InputStream; +import java.io.IOException; /** * A connection manager that provides access to a single HttpConnection. This * manager makes no attempt to provide exclusive access to the contained * HttpConnection. - * + * * @author <a href="mailto:[EMAIL PROTECTED]">Michael Becke</a> + * @author Eric Johnson * * @since 2.0 */ @@ -103,27 +106,27 @@ String host = hostConfiguration.getHost(); int port = hostConfiguration.getPort(); boolean isSecure = protocol.equalsIgnoreCase("HTTPS"); - + if ( httpConnection == null ) { - + if ( hostConfiguration.isProxySet() ) { httpConnection = new HttpConnection( hostConfiguration.getProxyHost(), hostConfiguration.getProxyPort(), - host, - port, + host, + port, isSecure ); - } else { + } else { httpConnection = new HttpConnection(host, port, isSecure); } - + } else { - + // make sure the host and proxy are correct for this connection // close it and set the values if they are not - if ( - !hostConfiguration.hostEquals(httpConnection) + if ( + !hostConfiguration.hostEquals(httpConnection) || !hostConfiguration.proxyEquals(httpConnection) ) { if ( httpConnection.isOpen() ) { @@ -137,18 +140,42 @@ httpConnection.setProxyHost(hostConfiguration.getProxyHost()); httpConnection.setProxyPort(hostConfiguration.getProxyPort()); - } - + } + else { + finishLastResponse(httpConnection); + } } return httpConnection; - + } /** * @see org.apache.commons.httpclient.HttpConnectionManager#releaseConnection(org.apache.commons.httpclient.HttpConnection) */ public void releaseConnection(HttpConnection conn) { + if (conn != httpConnection) + throw new IllegalStateException("Unexpected close on a different connection."); + + finishLastResponse(httpConnection); } + /** + * Since the same connection is about to be reused, make sure the + * previous request was completely processed, and if not + * consume it now. + */ + static void finishLastResponse(HttpConnection conn) { + InputStream lastResponse = conn.getLastResponseInputStream(); + if ( lastResponse != null) { + conn.setLastResponseInputStream(null); + try { + lastResponse.close(); + } + catch (IOException ioe) { + // badness - close to force reconnect. + conn.close(); + } + } + } } 1.1 jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/ResponseConsumedWatcher.java Index: ResponseConsumedWatcher.java =================================================================== /* * $Header: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/ResponseConsumedWatcher.java,v 1.1 2002/12/09 09:16:16 oglueck Exp $ * $Revision: 1.1 $ * $Date: 2002/12/09 09:16:16 $ * ==================================================================== * * The Apache Software License, Version 1.1 * * Copyright (c) 1999-2002 The Apache Software Foundation. All rights * reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * 3. The end-user documentation included with the redistribution, if * any, must include the following acknowlegement: * "This product includes software developed by the * Apache Software Foundation (http://www.apache.org/)." * Alternately, this acknowlegement may appear in the software itself, * if and wherever such third-party acknowlegements normally appear. * * 4. The names "The Jakarta Project", "HttpClient", and "Apache Software * Foundation" must not be used to endorse or promote products derived * from this software without prior written permission. For written * permission, please contact [EMAIL PROTECTED] * * 5. Products derived from this software may not be called "Apache" * nor may "Apache" appear in their names without prior written * permission of the Apache Group. * * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * ==================================================================== * * This software consists of voluntary contributions made by many * individuals on behalf of the Apache Software Foundation. For more * information on the Apache Software Foundation, please see * <http://www.apache.org/>. * * [Additional notices, if required by prior licensing conditions] * */ package org.apache.commons.httpclient; /** * When a response stream has been consumed, various parts of the HttpClient * implementation need to respond appropriately. * * <p>When one of the three types of {@link java.io.InputStream}, one of * AutoCloseInputStream (package), {@link ContentLengthInputStream}, or * {@link ChunkedInputStream} finishes with its content, either because * all content has been consumed, or because it was explicitly closed, * it notifies its corresponding method via this interface.</p> * * @see ContentLengthInputStream * @see ChunkedInputStream * @author Eric Johnson */ interface ResponseConsumedWatcher { /** * A response has been consumed. */ void responseConsumed(); } 1.19 +4 -3 jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/methods/GetMethod.java Index: GetMethod.java =================================================================== RCS file: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/methods/GetMethod.java,v retrieving revision 1.18 retrieving revision 1.19 diff -u -r1.18 -r1.19 --- GetMethod.java 3 Sep 2002 01:36:26 -0000 1.18 +++ GetMethod.java 9 Dec 2002 09:16:17 -0000 1.19 @@ -372,6 +372,7 @@ while ((len = in.read(buffer)) > 0) { out.write(buffer, 0, len); } + in.close(); out.close(); setResponseStream(new FileInputStream(createTempFile())); } 1.12 +6 -5 jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/methods/HeadMethod.java Index: HeadMethod.java =================================================================== RCS file: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/methods/HeadMethod.java,v retrieving revision 1.11 retrieving revision 1.12 diff -u -r1.11 -r1.12 --- HeadMethod.java 1 Sep 2002 01:27:37 -0000 1.11 +++ HeadMethod.java 9 Dec 2002 09:16:17 -0000 1.12 @@ -146,8 +146,9 @@ log.trace( "enter HeadMethod.readResponseBody(HttpState, HttpConnection)"); - // despite the possible presence of a content-length header, + // despite the possible presence of a content-length header, // HEAD returns no response body + responseBodyConsumed(); return; } -} \ No newline at end of file +} 1.28 +5 -3 jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/methods/PostMethod.java Index: PostMethod.java =================================================================== RCS file: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/methods/PostMethod.java,v retrieving revision 1.27 retrieving revision 1.28 diff -u -r1.27 -r1.28 --- PostMethod.java 12 Nov 2002 09:58:23 -0000 1.27 +++ PostMethod.java 9 Dec 2002 09:16:17 -0000 1.28 @@ -736,6 +736,8 @@ outstream = new ChunkedOutputStream(outstream); } if (this.requestContentLength >= 0) { + // don't need a watcher here - we're reading from something local, + // not server-side. instream = new ContentLengthInputStream(instream, this.requestContentLength); } 1.4 +39 -4 jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/TestGetMethodLocal.java Index: TestGetMethodLocal.java =================================================================== RCS file: /home/cvs/jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/TestGetMethodLocal.java,v retrieving revision 1.3 retrieving revision 1.4 diff -u -r1.3 -r1.4 --- TestGetMethodLocal.java 4 Feb 2002 15:26:43 -0000 1.3 +++ TestGetMethodLocal.java 9 Dec 2002 09:16:17 -0000 1.4 @@ -89,6 +89,7 @@ // -------------------------------------------------------------- Constants private static final String host = System.getProperty("httpclient.test.localHost","127.0.0.1"); + private static final String webAppContext = System.getProperty("httpclient.test.webappContext"); private static final int port; static { String portString = System.getProperty("httpclient.test.localPort","8080"); @@ -223,6 +224,40 @@ } assertEquals(404,method.getStatusCode()); + } + + /** + * The intent of this test is to allow for the incomplete parsing of a GET + * response, and to make it particularly tricky, the GET response issues + * a Connection: close". + * + * <p>This wants to insure that a recoverable exception is not unexpectedly + * triggered.</p> + */ + public void testGetResponseNotReadAutoRecover() { + + HttpClient client = new HttpClient(); + client.startSession(host, port); + + try { + // issue a GET with a connection: close, and don't parse the body. + String path = "/" + webAppContext + "/body"; + GetMethod method1 = new GetMethod(path); + method1.addRequestHeader("Connection", "close"); + client.executeMethod(method1); + assertEquals(0, method1.getRecoverableExceptionCount() ); + + // issue another GET. + GetMethod method2 = new GetMethod(path); + client.executeMethod(method2); + assertEquals(0, method2.getRecoverableExceptionCount() ); + + client.endSession(); + } + catch (IOException ioe) { + + fail("Problem executing method : " + ioe.toString() ); + } } } 1.5 +13 -7 jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/TestHttpClientLocalHost.java Index: TestHttpClientLocalHost.java =================================================================== RCS file: /home/cvs/jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/TestHttpClientLocalHost.java,v retrieving revision 1.4 retrieving revision 1.5 diff -u -r1.4 -r1.5 --- TestHttpClientLocalHost.java 3 Dec 2002 05:46:16 -0000 1.4 +++ TestHttpClientLocalHost.java 9 Dec 2002 09:16:17 -0000 1.5 @@ -84,6 +84,10 @@ // -------------------------------------------------------------- Constants + private static final String host = "127.0.0.1"; + private static final int port = 8080; + private static final String webAppContext = System.getProperty("httpclient.test.webappContext"); + // ------------------------------------------------------------ Constructor @@ -100,12 +104,12 @@ } private HttpClient client = null; + private String getPath = null; private GetMethod getSlash = null; - private GetMethod getSlash2 = null; public void setUp() { + getPath = "/" + webAppContext + "/body"; client = new HttpClient(); - getSlash = new GetMethod("/"); } public void tearDown() { @@ -115,6 +119,7 @@ public void testExecuteMethod() throws Exception { client.startSession(host, port); + GetMethod getSlash = new GetMethod(getPath); assertEquals(200, client.executeMethod(getSlash)); String data = getSlash.getResponseBodyAsString(); assertTrue(null != data); @@ -125,13 +130,14 @@ public void testExecuteMultipleMethods() throws Exception { client.startSession(host, port); + getSlash = new GetMethod(getPath); for(int i=0;i<10;i++) { assertEquals(200, client.executeMethod(getSlash)); String data = getSlash.getResponseBodyAsString(); assertTrue(null != data); assertTrue(data.length() > 0); getSlash.recycle(); - getSlash.setPath("/"); + getSlash.setPath(getPath); } client.endSession(); } 1.5 +8 -6 jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/TestMethodsLocalHost.java Index: TestMethodsLocalHost.java =================================================================== RCS file: /home/cvs/jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/TestMethodsLocalHost.java,v retrieving revision 1.4 retrieving revision 1.5 diff -u -r1.4 -r1.5 --- TestMethodsLocalHost.java 1 Nov 2002 09:51:05 -0000 1.4 +++ TestMethodsLocalHost.java 9 Dec 2002 09:16:17 -0000 1.5 @@ -88,6 +88,7 @@ // -------------------------------------------------------------- Constants + private static final String webAppContext = System.getProperty("httpclient.test.webappContext"); private static final String host = "127.0.0.1"; private static final int port = 8080; @@ -208,7 +209,8 @@ fail("Unable to execute method : " + t.toString()); } - HeadMethod method = new HeadMethod("/"); + String path = "/" + webAppContext + "/body"; + HeadMethod method = new HeadMethod(path); try { client.executeMethod(method); @@ -220,7 +222,7 @@ assertEquals(200, method.getStatusCode()); method.recycle(); - method.setPath("/index.html"); + method.setPath(path); try { client.executeMethod(method); 1.12 +23 -6 jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/TestMethodsNoHost.java Index: TestMethodsNoHost.java =================================================================== RCS file: /home/cvs/jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/TestMethodsNoHost.java,v retrieving revision 1.11 retrieving revision 1.12 diff -u -r1.11 -r1.12 --- TestMethodsNoHost.java 31 Oct 2002 07:45:35 -0000 1.11 +++ TestMethodsNoHost.java 9 Dec 2002 09:16:17 -0000 1.12 @@ -71,6 +71,7 @@ import junit.framework.TestSuite; import org.apache.commons.httpclient.methods.GetMethod; import org.apache.commons.httpclient.methods.PostMethod; +import org.apache.commons.httpclient.methods.HeadMethod; /** * @author Rodney Waldhoff @@ -276,7 +277,8 @@ HttpMethodBase method = new GetMethod("/"); method.execute(new HttpState(), conn); String responseBody = method.getResponseBodyAsString(); - conn.close(); + // verify that the connection was closed. + conn.assertNotOpen(); assertEquals("1234567890123", responseBody); } @@ -300,7 +302,22 @@ while ((c = response.read()) != -1) { assertEquals((int) 'A', c); } - assertTrue(!conn.isOpen()); + conn.assertNotOpen(); + + // note - this test is here because the HEAD method handler overrides the + // standard behavior for reading a response body. + HeadMethod headMethod = new HeadMethod("/"); + + conn.addResponse(headers, ""); + + try { + headMethod.execute(new HttpState(), conn); + conn.assertNotOpen(); + + } catch (Throwable t) { + t.printStackTrace(); + fail("Unable to execute method : " + t.toString()); + } } public void testSetGetQueryString1() {
-- To unsubscribe, e-mail: <mailto:[EMAIL PROTECTED]> For additional commands, e-mail: <mailto:[EMAIL PROTECTED]>