diff --git a/google-http-client/src/main/java/com/google/api/client/http/javanet/NetHttpResponse.java b/google-http-client/src/main/java/com/google/api/client/http/javanet/NetHttpResponse.java index 16d3c717f..4a5edc7ec 100644 --- a/google-http-client/src/main/java/com/google/api/client/http/javanet/NetHttpResponse.java +++ b/google-http-client/src/main/java/com/google/api/client/http/javanet/NetHttpResponse.java @@ -16,6 +16,7 @@ import com.google.api.client.http.LowLevelHttpResponse; +import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; @@ -70,14 +71,27 @@ public int getStatusCode() { * with version 1.17 it returns {@link HttpURLConnection#getInputStream} when it doesn't throw * {@link IOException}, otherwise it returns {@link HttpURLConnection#getErrorStream} *
+ * + *+ * Upgrade warning: in versions prior to 1.20 {@link #getContent()} returned + * {@link HttpURLConnection#getInputStream()} or {@link HttpURLConnection#getErrorStream()}, both + * of which silently returned -1 for read() calls when the connection got closed in the middle + * of receiving a response. This is highly likely a bug from JDK's {@link HttpURLConnection}. + * Since version 1.20, the bytes read off the wire will be checked and an {@link IOException} will + * be thrown if the response is not fully delivered when the connection is closed by server for + * whatever reason, e.g., server restarts. Note though that this is a best-effort check: when the + * response is chunk encoded, we have to rely on the underlying HTTP library to behave correctly. + *
*/ @Override public InputStream getContent() throws IOException { + InputStream in = null; try { - return connection.getInputStream(); + in = connection.getInputStream(); } catch (IOException ioe) { - return connection.getErrorStream(); + in = connection.getErrorStream(); } + return in == null ? null : new SizeValidatingInputStream(in); } @Override @@ -131,4 +145,63 @@ public String getHeaderValue(int index) { public void disconnect() { connection.disconnect(); } + + /** + * A wrapper arround the base {@link InputStream} that validates EOF returned by the read calls. + * + * @since 1.20 + */ + private final class SizeValidatingInputStream extends FilterInputStream { + + private long bytesRead = 0; + + public SizeValidatingInputStream(InputStream in) { + super(in); + } + + /** + * java.io.InputStream#read(byte[], int, int) swallows IOException thrown from read() so we have + * to override it. + * @see "http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/8-b132/java/io/InputStream.java#185" + */ + @Override + public int read(byte[] b, int off, int len) throws IOException { + int n = in.read(b, off, len); + if (n == -1) { + throwIfFalseEOF(); + } else { + bytesRead += n; + } + return n; + } + + @Override + public int read() throws IOException { + int n = in.read(); + if (n == -1) { + throwIfFalseEOF(); + } else { + bytesRead++; + } + return n; + } + + // Throws an IOException if gets an EOF in the middle of a response. + private void throwIfFalseEOF() throws IOException { + long contentLength = getContentLength(); + if (contentLength == -1) { + // If a Content-Length header is missing, there's nothing we can do. + return; + } + // According to RFC2616, message-body is prohibited in responses to certain requests, e.g., + // HEAD. Nevertheless an entity-header (possibly with non-zero Content-Length) may be present. + // Thus we exclude the case where bytesRead == 0. + // + // See http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.4 for details. + if (bytesRead != 0 && bytesRead < contentLength) { + throw new IOException("Connection closed prematurely: bytesRead = " + bytesRead + + ", Content-Length = " + contentLength); + } + } + } } diff --git a/google-http-client/src/main/java/com/google/api/client/testing/http/javanet/MockHttpURLConnection.java b/google-http-client/src/main/java/com/google/api/client/testing/http/javanet/MockHttpURLConnection.java index 2425e4edf..b59648e51 100644 --- a/google-http-client/src/main/java/com/google/api/client/testing/http/javanet/MockHttpURLConnection.java +++ b/google-http-client/src/main/java/com/google/api/client/testing/http/javanet/MockHttpURLConnection.java @@ -17,7 +17,6 @@ import com.google.api.client.util.Beta; import com.google.api.client.util.Preconditions; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -25,6 +24,10 @@ import java.net.HttpURLConnection; import java.net.URL; import java.net.UnknownServiceException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; /** * {@link Beta}To prevent incidental overwrite, only the first non-null assignment is honored. + * + * @since 1.20 + */ + public MockHttpURLConnection setInputStream(InputStream is) { + Preconditions.checkNotNull(is); + if (inputStream == null) { + inputStream = is; + } + return this; + } + + /** + * Sets the error stream. + * + *
To prevent incidental overwrite, only the first non-null assignment is honored.
+ *
+ * @since 1.20
+ */
+ public MockHttpURLConnection setErrorStream(InputStream is) {
+ Preconditions.checkNotNull(is);
+ if (errorStream == null) {
+ errorStream = is;
+ }
+ return this;
+ }
+
@Override
public InputStream getInputStream() throws IOException {
if (responseCode < 400) {
@@ -142,4 +201,15 @@ public InputStream getInputStream() throws IOException {
public InputStream getErrorStream() {
return errorStream;
}
+
+ @Override
+ public Map