/* * Copyright (C) 2009 The Android Open Source Project * * Licensed 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 libcore.java.net; import static com.google.mockwebserver.SocketPolicy.DISCONNECT_AT_END; import static com.google.mockwebserver.SocketPolicy.DISCONNECT_AT_START; import static com.google.mockwebserver.SocketPolicy.SHUTDOWN_INPUT_AT_END; import static com.google.mockwebserver.SocketPolicy.SHUTDOWN_OUTPUT_AT_END; import com.google.mockwebserver.Dispatcher; import com.google.mockwebserver.MockResponse; import com.google.mockwebserver.MockWebServer; import com.google.mockwebserver.RecordedRequest; import com.google.mockwebserver.SocketPolicy; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Authenticator; import java.net.CacheRequest; import java.net.CacheResponse; import java.net.HttpRetryException; import java.net.HttpURLConnection; import java.net.InetAddress; import java.net.PasswordAuthentication; import java.net.ProtocolException; import java.net.Proxy; import java.net.ProxySelector; import java.net.ResponseCache; import java.net.SocketAddress; import java.net.SocketTimeoutException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.URLConnection; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import junit.framework.TestCase; public final class URLConnectionTest extends TestCase { private MockWebServer server; private String hostName; private Object savedUrlCache; @Override protected void setUp() throws Exception { super.setUp(); savedUrlCache = setNewUrlCache(); server = new MockWebServer(); hostName = server.getHostName(); } @Override protected void tearDown() throws Exception { ResponseCache.setDefault(null); Authenticator.setDefault(null); System.clearProperty("proxyHost"); System.clearProperty("proxyPort"); System.clearProperty("http.proxyHost"); System.clearProperty("http.proxyPort"); System.clearProperty("https.proxyHost"); System.clearProperty("https.proxyPort"); server.shutdown(); restoreUrlCache(savedUrlCache); server = null; super.tearDown(); } // Make sure each test runs with a clean cache. Otherwise, a test may get a cache hit from the // previous test. private static native Object setNewUrlCache() /*-[ NSURLCache *oldCache = [NSURLCache sharedURLCache]; NSURLCache *newCache = [[NSURLCache alloc] initWithMemoryCapacity:0 diskCapacity:0 diskPath:nil]; [NSURLCache setSharedURLCache:newCache]; [newCache release]; return oldCache; ]-*/; private static native void restoreUrlCache(Object savedCache) /*-[ [NSURLCache setSharedURLCache:savedCache]; ]-*/; // JVM failure. // public void testRequestHeaderValidation() throws Exception { // // Android became more strict after M about which characters were allowed in request header // // names and values: previously almost anything was allowed if it didn't contain \0. // // assertForbiddenRequestHeaderName(null); // assertForbiddenRequestHeaderName(""); // assertForbiddenRequestHeaderName("\n"); // assertForbiddenRequestHeaderName("a\nb"); // assertForbiddenRequestHeaderName("\u0000"); // assertForbiddenRequestHeaderName("\r"); // assertForbiddenRequestHeaderName("\t"); // assertForbiddenRequestHeaderName("\u001f"); // assertForbiddenRequestHeaderName("\u007f"); // assertForbiddenRequestHeaderName("\u0080"); // assertForbiddenRequestHeaderName("\ud83c\udf69"); // // assertEquals(null, setAndReturnRequestHeaderValue(null)); // assertEquals("", setAndReturnRequestHeaderValue("")); // assertForbiddenRequestHeaderValue("\u0000"); // // // Workaround for http://b/26422335 , http://b/26889631 , http://b/27606665 : // // allow (but strip) trailing \n, \r and \r\n // // assertForbiddenRequestHeaderValue("\r"); // // End of workaround // assertForbiddenRequestHeaderValue("\t"); // assertForbiddenRequestHeaderValue("\u001f"); // assertForbiddenRequestHeaderValue("\u007f"); // assertForbiddenRequestHeaderValue("\u0080"); // assertForbiddenRequestHeaderValue("\ud83c\udf69"); // // // Workaround for http://b/26422335 , http://b/26889631 , http://b/27606665 : // // allow (but strip) trailing \n, \r and \r\n // assertEquals("", setAndReturnRequestHeaderValue("\n")); // assertEquals("a", setAndReturnRequestHeaderValue("a\n")); // assertEquals("", setAndReturnRequestHeaderValue("\r")); // assertEquals("a", setAndReturnRequestHeaderValue("a\r")); // assertEquals("", setAndReturnRequestHeaderValue("\r\n")); // assertEquals("a", setAndReturnRequestHeaderValue("a\r\n")); // assertForbiddenRequestHeaderValue("a\nb"); // assertForbiddenRequestHeaderValue("\nb"); // assertForbiddenRequestHeaderValue("a\rb"); // assertForbiddenRequestHeaderValue("\rb"); // assertForbiddenRequestHeaderValue("a\r\nb"); // assertForbiddenRequestHeaderValue("\r\nb"); // // End of workaround // } private static void assertForbiddenRequestHeaderName(String name) throws Exception { URL url = new URL("http://www.google.com/"); HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); try { urlConnection.addRequestProperty(name, "value"); fail("Expected exception"); } catch (IllegalArgumentException expected) { } catch (NullPointerException expectedIfNull) { assertTrue(name == null); } } private static void assertForbiddenRequestHeaderValue(String value) throws Exception { URL url = new URL("http://www.google.com/"); HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); try { urlConnection.addRequestProperty("key", value); fail("Expected exception"); } catch (IllegalArgumentException expected) { } } private static String setAndReturnRequestHeaderValue(String value) throws Exception { URL url = new URL("http://www.google.com/"); HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); urlConnection.addRequestProperty("key", value); return urlConnection.getRequestProperty("key"); } // TODO(tball): b/28067294 // public void testRequestHeaders() throws IOException, InterruptedException { // server.enqueue(new MockResponse()); // server.play(); // // HttpURLConnection urlConnection = (HttpURLConnection) server.getUrl("/").openConnection(); // urlConnection.addRequestProperty("D", "e"); // urlConnection.addRequestProperty("D", "f"); // assertEquals("f", urlConnection.getRequestProperty("D")); // assertEquals("f", urlConnection.getRequestProperty("d")); // Map<String, List<String>> requestHeaders = urlConnection.getRequestProperties(); // assertEquals(newSet("e", "f"), new HashSet<String>(requestHeaders.get("D"))); //// assertEquals(newSet("e", "f"), new HashSet<String>(requestHeaders.get("d"))); // JVM failure // try { // requestHeaders.put("G", Arrays.asList("h")); // fail("Modified an unmodifiable view."); // } catch (UnsupportedOperationException expected) { // } // try { // requestHeaders.get("D").add("i"); // fail("Modified an unmodifiable view."); // } catch (UnsupportedOperationException expected) { // } // try { // urlConnection.setRequestProperty(null, "j"); // fail(); // } catch (NullPointerException expected) { // } // try { // urlConnection.addRequestProperty(null, "k"); // fail(); // } catch (NullPointerException expected) { // } // urlConnection.setRequestProperty("NullValue", null); // should fail silently! // assertNull(urlConnection.getRequestProperty("NullValue")); // urlConnection.addRequestProperty("AnotherNullValue", null); // should fail silently! // assertNull(urlConnection.getRequestProperty("AnotherNullValue")); // // urlConnection.getResponseCode(); // RecordedRequest request = server.takeRequest(); // assertContains(request.getHeaders(), "D: e"); // assertContains(request.getHeaders(), "D: f"); //// assertContainsNoneMatching(request.getHeaders(), "NullValue.*"); // JVM failure //// assertContainsNoneMatching(request.getHeaders(), "AnotherNullValue.*"); // JVM failure // assertContainsNoneMatching(request.getHeaders(), "G:.*"); // assertContainsNoneMatching(request.getHeaders(), "null:.*"); // // try { // urlConnection.addRequestProperty("N", "o"); // fail("Set header after connect"); // } catch (IllegalStateException expected) { // } // try { // urlConnection.setRequestProperty("P", "q"); // fail("Set header after connect"); // } catch (IllegalStateException expected) { // } //// JVM failure. //// try { //// urlConnection.getRequestProperties(); //// fail(); //// } catch (IllegalStateException expected) { //// } // } public void testGetRequestPropertyReturnsLastValue() throws Exception { server.play(); HttpURLConnection urlConnection = (HttpURLConnection) server.getUrl("/").openConnection(); urlConnection.addRequestProperty("A", "value1"); urlConnection.addRequestProperty("A", "value2"); assertEquals("value2", urlConnection.getRequestProperty("A")); } // JVM failure. // public void testResponseHeaders() throws IOException, InterruptedException { // server.enqueue(new MockResponse() // .setStatus("HTTP/1.0 200 Fantastic") // .addHeader("A: c") // .addHeader("B: d") // .addHeader("A: e") // .setChunkedBody("ABCDE\nFGHIJ\nKLMNO\nPQR", 8)); // server.play(); // // HttpURLConnection urlConnection = (HttpURLConnection) server.getUrl("/").openConnection(); // assertEquals(200, urlConnection.getResponseCode()); // assertEquals("Fantastic", urlConnection.getResponseMessage()); // assertEquals("HTTP/1.0 200 Fantastic", urlConnection.getHeaderField(null)); // Map<String, List<String>> responseHeaders = urlConnection.getHeaderFields(); // assertEquals(Arrays.asList("HTTP/1.0 200 Fantastic"), responseHeaders.get(null)); // assertEquals(newSet("c", "e"), new HashSet<String>(responseHeaders.get("A"))); // assertEquals(newSet("c", "e"), new HashSet<String>(responseHeaders.get("a"))); // try { // responseHeaders.put("N", Arrays.asList("o")); // fail("Modified an unmodifiable view."); // } catch (UnsupportedOperationException expected) { // } // try { // responseHeaders.get("A").add("f"); // fail("Modified an unmodifiable view."); // } catch (UnsupportedOperationException expected) { // } // assertEquals("A", urlConnection.getHeaderFieldKey(0)); // assertEquals("c", urlConnection.getHeaderField(0)); // assertEquals("B", urlConnection.getHeaderFieldKey(1)); // assertEquals("d", urlConnection.getHeaderField(1)); // assertEquals("A", urlConnection.getHeaderFieldKey(2)); // assertEquals("e", urlConnection.getHeaderField(2)); // } public void testGetErrorStreamOnSuccessfulRequest() throws Exception { server.enqueue(new MockResponse().setBody("A")); server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); assertEquals(200, connection.getResponseCode()); assertNull(connection.getErrorStream()); } public void testGetErrorStreamOnUnsuccessfulRequest() throws Exception { server.enqueue(new MockResponse().setResponseCode(404).setBody("A")); server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); assertEquals(404, connection.getResponseCode()); assertEquals("A", readAscii(connection.getErrorStream(), Integer.MAX_VALUE)); } // Check that if we don't read to the end of a response, the next request on the // recycled connection doesn't get the unread tail of the first request's response. // http://code.google.com/p/android/issues/detail?id=2939 public void test_2939() throws Exception { MockResponse response = new MockResponse().setChunkedBody("ABCDE\nFGHIJ\nKLMNO\nPQR", 8); server.enqueue(response); server.enqueue(response); server.play(); assertContent("ABCDE", server.getUrl("/").openConnection(), 5); assertContent("ABCDE", server.getUrl("/").openConnection(), 5); } // Check that we recognize a few basic mime types by extension. // http://code.google.com/p/android/issues/detail?id=10100 public void test_10100() throws Exception { assertEquals("image/jpeg", URLConnection.guessContentTypeFromName("someFile.jpg")); assertEquals("application/pdf", URLConnection.guessContentTypeFromName("stuff.pdf")); } // TODO(tball): b/28067294 // public void testConnectionsArePooled() throws Exception { // MockResponse response = new MockResponse().setBody("ABCDEFGHIJKLMNOPQR"); // // server.enqueue(response); // server.enqueue(response); // server.enqueue(response); // server.play(); // // assertContent("ABCDEFGHIJKLMNOPQR", server.getUrl("/foo").openConnection()); // assertEquals(0, server.takeRequest().getSequenceNumber()); // assertContent("ABCDEFGHIJKLMNOPQR", server.getUrl("/bar?baz=quux").openConnection()); // assertEquals(1, server.takeRequest().getSequenceNumber()); // assertContent("ABCDEFGHIJKLMNOPQR", server.getUrl("/z").openConnection()); // assertEquals(2, server.takeRequest().getSequenceNumber()); // } // TODO(tball): b/28067294 // public void testChunkedConnectionsArePooled() throws Exception { // MockResponse response = new MockResponse().setChunkedBody("ABCDEFGHIJKLMNOPQR", 5); // // server.enqueue(response); // server.enqueue(response); // server.enqueue(response); // server.play(); // // assertContent("ABCDEFGHIJKLMNOPQR", server.getUrl("/foo").openConnection()); // assertEquals(0, server.takeRequest().getSequenceNumber()); // assertContent("ABCDEFGHIJKLMNOPQR", server.getUrl("/bar?baz=quux").openConnection()); // assertEquals(1, server.takeRequest().getSequenceNumber()); // assertContent("ABCDEFGHIJKLMNOPQR", server.getUrl("/z").openConnection()); // assertEquals(2, server.takeRequest().getSequenceNumber()); // } // TODO(tball): b/28067294 // /** // * Test that connections are added to the pool as soon as the response has // * been consumed. // */ // public void testConnectionsArePooledWithoutExplicitDisconnect() throws Exception { // server.enqueue(new MockResponse().setBody("ABC")); // server.enqueue(new MockResponse().setBody("DEF")); // server.play(); // // URLConnection connection1 = server.getUrl("/").openConnection(); // assertEquals("ABC", readAscii(connection1.getInputStream(), Integer.MAX_VALUE)); // assertEquals(0, server.takeRequest().getSequenceNumber()); // URLConnection connection2 = server.getUrl("/").openConnection(); // assertEquals("DEF", readAscii(connection2.getInputStream(), Integer.MAX_VALUE)); // assertEquals(1, server.takeRequest().getSequenceNumber()); // } public void testServerClosesSocket() throws Exception { testServerClosesSocket(DISCONNECT_AT_END); } public void testServerShutdownInput() throws Exception { testServerClosesSocket(SHUTDOWN_INPUT_AT_END); } private void testServerClosesSocket(SocketPolicy socketPolicy) throws Exception { server.enqueue(new MockResponse() .setBody("This connection won't pool properly") .setSocketPolicy(socketPolicy)); server.enqueue(new MockResponse().setBody("This comes after a busted connection")); server.play(); assertContent("This connection won't pool properly", server.getUrl("/a").openConnection()); assertEquals(0, server.takeRequest().getSequenceNumber()); assertContent("This comes after a busted connection", server.getUrl("/b").openConnection()); // sequence number 0 means the HTTP socket connection was not reused assertEquals(0, server.takeRequest().getSequenceNumber()); } // TODO(tball): b/28067294 // public void testServerShutdownOutput() throws Exception { // // This test causes MockWebServer to log a "connection failed" stack trace // // // Setting the server workerThreads to 1 ensures the responses are generated in the order // // the requests are accepted by the server. Without this the second and third requests made // // by the client (the request for "/b" and the retry for "/b" when the bad socket is // // detected) can be handled by the server out of order leading to test failure. // server.setWorkerThreads(1); // server.enqueue(new MockResponse() // .setBody("Output shutdown after this response") // .setSocketPolicy(SHUTDOWN_OUTPUT_AT_END)); // server.enqueue(new MockResponse().setBody("This response will fail to write")); // server.enqueue(new MockResponse().setBody("This comes after a busted connection")); // server.play(); // // assertContent("Output shutdown after this response", server.getUrl("/a").openConnection()); // assertEquals(0, server.takeRequest().getSequenceNumber()); // assertContent("This comes after a busted connection", server.getUrl("/b").openConnection()); // assertEquals(1, server.takeRequest().getSequenceNumber()); // assertEquals(0, server.takeRequest().getSequenceNumber()); // } enum WriteKind { BYTE_BY_BYTE, SMALL_BUFFERS, LARGE_BUFFERS } // j2objc: this test case is disabled because CFReadStream (which is the NSInputStream we give // to the NSMutableURLRequest when we make a streamed POST request) logs a message // "Stream [address] is sending an event before being opened", even though it does not affect // test correctness. This only happens when small chunks are written to the piped // (NS)OutputStream before the connection is fully established, and the message is never seen // in the next two tests. Since the log message only adds noise, we disable this test. // public void test_chunkedUpload_byteByByte() throws Exception { // doUpload(TransferKind.CHUNKED, WriteKind.BYTE_BY_BYTE); // } public void test_chunkedUpload_smallBuffers() throws Exception { doUpload(TransferKind.CHUNKED, WriteKind.SMALL_BUFFERS); } public void test_chunkedUpload_largeBuffers() throws Exception { doUpload(TransferKind.CHUNKED, WriteKind.LARGE_BUFFERS); } public void test_fixedLengthUpload_byteByByte() throws Exception { doUpload(TransferKind.FIXED_LENGTH, WriteKind.BYTE_BY_BYTE); } public void test_fixedLengthUpload_smallBuffers() throws Exception { doUpload(TransferKind.FIXED_LENGTH, WriteKind.SMALL_BUFFERS); } public void test_fixedLengthUpload_largeBuffers() throws Exception { doUpload(TransferKind.FIXED_LENGTH, WriteKind.LARGE_BUFFERS); } private void doUpload(TransferKind uploadKind, WriteKind writeKind) throws Exception { int n = 512*1024; server.setBodyLimit(0); server.enqueue(new MockResponse()); server.play(); HttpURLConnection conn = (HttpURLConnection) server.getUrl("/").openConnection(); conn.setDoOutput(true); conn.setRequestMethod("POST"); // j2objc: URLConnection does not time out by default (i.e. getReadTimeout() returns 0). // When we create the native NSMutableURLRequest, we set its timeoutInterval to // JavaLangDouble_MAX_VALUE to make it never time out. This is problematic if the request // uses chunked transfer encoding, as it causes the NSURLSessionDataTask to send malformed // chunks and never terminate the request. A definite timeout is therefore needed here. conn.setReadTimeout(1000); if (uploadKind == TransferKind.CHUNKED) { conn.setChunkedStreamingMode(-1); } else { conn.setFixedLengthStreamingMode(n); } OutputStream out = conn.getOutputStream(); if (writeKind == WriteKind.BYTE_BY_BYTE) { for (int i = 0; i < n; ++i) { out.write('x'); } } else { byte[] buf = new byte[writeKind == WriteKind.SMALL_BUFFERS ? 256 : 64*1024]; Arrays.fill(buf, (byte) 'x'); for (int i = 0; i < n; i += buf.length) { out.write(buf, 0, Math.min(buf.length, n - i)); } } out.close(); assertEquals(200, conn.getResponseCode()); RecordedRequest request = server.takeRequest(); assertEquals(n, request.getBodySize()); if (uploadKind == TransferKind.CHUNKED) { assertTrue(request.getChunkSizes().size() > 0); } else { assertTrue(request.getChunkSizes().isEmpty()); } } // JVM failure. // public void testGetResponseCodeNoResponseBody() throws Exception { // server.enqueue(new MockResponse() // .addHeader("abc: def")); // server.play(); // // URL url = server.getUrl("/"); // HttpURLConnection conn = (HttpURLConnection) url.openConnection(); // conn.setDoInput(false); // assertEquals("def", conn.getHeaderField("abc")); // assertEquals(200, conn.getResponseCode()); // try { // conn.getInputStream(); // fail(); // } catch (ProtocolException expected) { // } // } // TODO(tball): b/28067294 // public void testConnectViaProxyUsingProxyArg() throws Exception { // testConnectViaProxy(ProxyConfig.CREATE_ARG); // } // TODO(tball): b/28067294 // public void testConnectViaProxyUsingProxySystemProperty() throws Exception { // testConnectViaProxy(ProxyConfig.PROXY_SYSTEM_PROPERTY); // } private void testConnectViaProxy(ProxyConfig proxyConfig) throws Exception { MockResponse mockResponse = new MockResponse().setBody("this response comes via a proxy"); server.enqueue(mockResponse); server.play(); URL url = new URL("http://android.com/foo"); HttpURLConnection connection = proxyConfig.connect(server, url); assertContent("this response comes via a proxy", connection); RecordedRequest request = server.takeRequest(); assertEquals("GET http://android.com/foo HTTP/1.1", request.getRequestLine()); assertContains(request.getHeaders(), "Host: android.com"); } public void testContentDisagreesWithContentLengthHeader() throws IOException { server.enqueue(new MockResponse() .setBody("abc\r\nYOU SHOULD NOT SEE THIS") .clearHeaders() .addHeader("Content-Length: 3")); server.play(); assertContent("abc", server.getUrl("/").openConnection()); } public void testContentDisagreesWithChunkedHeader() throws IOException { MockResponse mockResponse = new MockResponse(); mockResponse.setChunkedBody("abc", 3); ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); bytesOut.write(mockResponse.getBody()); bytesOut.write("\r\nYOU SHOULD NOT SEE THIS".getBytes()); mockResponse.setBody(bytesOut.toByteArray()); mockResponse.clearHeaders(); mockResponse.addHeader("Transfer-encoding: chunked"); server.enqueue(mockResponse); server.play(); assertContent("abc", server.getUrl("/").openConnection()); } // TODO(tball): b/28067294 // /** // * Test Etag headers are returned correctly when a client-side cache is not installed. // * https://code.google.com/p/android/issues/detail?id=108949 // */ // public void testEtagHeaders_uncached() throws Exception { // final String etagValue1 = "686897696a7c876b7e"; // final String body1 = "Response with etag 1"; // final String etagValue2 = "686897696a7c876b7f"; // final String body2 = "Response with etag 2"; // // server.enqueue( // new MockResponse() // .setBody(body1) // .setHeader("Content-Type", "text/plain") // .setHeader("Etag", etagValue1)); // server.enqueue( // new MockResponse() // .setBody(body2) // .setHeader("Content-Type", "text/plain") // .setHeader("Etag", etagValue2)); // server.play(); // // URL url = server.getUrl("/"); // HttpURLConnection connection1 = (HttpURLConnection) url.openConnection(); // assertEquals(etagValue1, connection1.getHeaderField("Etag")); // assertContent(body1, connection1); // connection1.disconnect(); // // // Discard the server-side record of the request made. // server.takeRequest(); // // HttpURLConnection connection2 = (HttpURLConnection) url.openConnection(); // assertEquals(etagValue2, connection2.getHeaderField("Etag")); // assertContent(body2, connection2); // connection2.disconnect(); // // // Check the client did not cache. // RecordedRequest request = server.takeRequest(); // assertNull(request.getHeader("If-None-Match")); // } // TODO(tball): b/28067294 // public void testDisconnectedConnection() throws IOException { // server.enqueue(new MockResponse() // .throttleBody(2, 100, TimeUnit.MILLISECONDS) // .setBody("ABCD")); // server.play(); // // HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); // InputStream in = connection.getInputStream(); // assertEquals('A', (char) in.read()); // connection.disconnect(); // try { // // Reading 'B' may succeed if it's buffered. // in.read(); // // But 'C' shouldn't be buffered (the response is throttled) and this should fail. // in.read(); // fail("Expected a connection closed exception"); // } catch (IOException expected) { // } // } public void testDisconnectBeforeConnect() throws IOException { server.enqueue(new MockResponse().setBody("A")); server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); connection.disconnect(); assertContent("A", connection); assertEquals(200, connection.getResponseCode()); } public void testDisconnectAfterOnlyResponseCodeCausesNoCloseGuardWarning() throws IOException { server.enqueue(new MockResponse() .setBody(gzip("ABCABCABC".getBytes("UTF-8"))) .addHeader("Content-Encoding: gzip")); server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); try { assertEquals(200, connection.getResponseCode()); } finally { connection.disconnect(); } } /** * Reads {@code count} characters from the stream. If the stream is * exhausted before {@code count} characters can be read, the remaining * characters are returned and the stream is closed. */ private String readAscii(InputStream in, int count) throws IOException { StringBuilder result = new StringBuilder(); for (int i = 0; i < count; i++) { int value = in.read(); if (value == -1) { in.close(); break; } result.append((char) value); } return result.toString(); } public void testMarkAndResetWithContentLengthHeader() throws IOException { testMarkAndReset(TransferKind.FIXED_LENGTH); } public void testMarkAndResetWithChunkedEncoding() throws IOException { testMarkAndReset(TransferKind.CHUNKED); } // JVM failure. // public void testMarkAndResetWithNoLengthHeaders() throws IOException { // testMarkAndReset(TransferKind.END_OF_STREAM); // } private void testMarkAndReset(TransferKind transferKind) throws IOException { MockResponse response = new MockResponse(); transferKind.setBody(response, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 1024); server.enqueue(response); server.enqueue(response); server.play(); InputStream in = server.getUrl("/").openConnection().getInputStream(); assertFalse("This implementation claims to support mark().", in.markSupported()); in.mark(5); assertEquals("ABCDE", readAscii(in, 5)); try { in.reset(); fail(); } catch (IOException expected) { } assertEquals("FGHIJKLMNOPQRSTUVWXYZ", readAscii(in, Integer.MAX_VALUE)); assertContent("ABCDEFGHIJKLMNOPQRSTUVWXYZ", server.getUrl("/").openConnection()); } // TODO(tball): b/28067294 // /** // * We've had a bug where we forget the HTTP response when we see response // * code 401. This causes a new HTTP request to be issued for every call into // * the URLConnection. // */ // public void testUnauthorizedResponseHandling() throws IOException { // MockResponse response = new MockResponse() // .addHeader("WWW-Authenticate: Basic realm=\"protected area\"") // .setResponseCode(401) // UNAUTHORIZED // .setBody("Unauthorized"); // server.enqueue(response); // server.enqueue(response); // server.enqueue(response); // server.play(); // // URL url = server.getUrl("/"); // HttpURLConnection conn = (HttpURLConnection) url.openConnection(); // // assertEquals(401, conn.getResponseCode()); // assertEquals(401, conn.getResponseCode()); // assertEquals(401, conn.getResponseCode()); // assertEquals(1, server.getRequestCount()); // } public void testNonHexChunkSize() throws IOException { server.enqueue(new MockResponse() .setBody("5\r\nABCDE\r\nG\r\nFGHIJKLMNOPQRSTU\r\n0\r\n\r\n") .clearHeaders() .addHeader("Transfer-encoding: chunked")); server.play(); URLConnection connection = server.getUrl("/").openConnection(); // j2objc: URLConnection does not time out by default (i.e. getReadTimeout() returns 0). // When we create the native NSMutableURLRequest, we set its timeoutInterval to // JavaLangDouble_MAX_VALUE to make it never time out. This is problematic if the response // uses chunked transfer encoding, as the NSURLSessionDataTask is unable to make progress // and the read will never finish. A definite timeout is therefore needed here. connection.setReadTimeout(1000); try { readAscii(connection.getInputStream(), Integer.MAX_VALUE); fail(); } catch (IOException e) { } } public void testMissingChunkBody() throws IOException { server.enqueue(new MockResponse() .setBody("5") .clearHeaders() .addHeader("Transfer-encoding: chunked") .setSocketPolicy(DISCONNECT_AT_END)); server.play(); URLConnection connection = server.getUrl("/").openConnection(); // j2objc: See testNonHexChunkSize() above for why the timeout is needed. connection.setReadTimeout(1000); try { readAscii(connection.getInputStream(), Integer.MAX_VALUE); fail(); } catch (IOException e) { } } // JVM failure. // /** // * This test checks whether connections are gzipped by default. This // * behavior in not required by the API, so a failure of this test does not // * imply a bug in the implementation. // */ // public void testGzipEncodingEnabledByDefault() throws IOException, InterruptedException { // server.enqueue(new MockResponse() // .setBody(gzip("ABCABCABC".getBytes("UTF-8"))) // .addHeader("Content-Encoding: gzip")); // server.play(); // // URLConnection connection = server.getUrl("/").openConnection(); // assertEquals("ABCABCABC", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); // assertNull(connection.getContentEncoding()); // assertEquals(-1, connection.getContentLength()); // // RecordedRequest request = server.takeRequest(); // assertContains(request.getHeaders(), "Accept-Encoding: gzip"); // } // TODO(tball): b/28067294 // public void testClientConfiguredGzipContentEncoding() throws Exception { // byte[] bodyBytes = gzip("ABCDEFGHIJKLMNOPQRSTUVWXYZ".getBytes("UTF-8")); // server.enqueue(new MockResponse() // .setBody(bodyBytes) // .addHeader("Content-Encoding: gzip") // .addHeader("Content-Length: " + bodyBytes.length)); // server.play(); // // URLConnection connection = server.getUrl("/").openConnection(); // connection.addRequestProperty("Accept-Encoding", "gzip"); // InputStream gunzippedIn = new GZIPInputStream(connection.getInputStream()); // assertEquals("ABCDEFGHIJKLMNOPQRSTUVWXYZ", readAscii(gunzippedIn, Integer.MAX_VALUE)); // assertEquals(bodyBytes.length, connection.getContentLength()); // // RecordedRequest request = server.takeRequest(); // assertContains(request.getHeaders(), "Accept-Encoding: gzip"); // assertEquals("gzip", connection.getContentEncoding()); // } // TODO(tball): b/28067294 // public void testGzipAndConnectionReuseWithFixedLength() throws Exception { // testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind.FIXED_LENGTH); // } // TODO(tball): b/28067294 // public void testGzipAndConnectionReuseWithChunkedEncoding() throws Exception { // testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind.CHUNKED); // } public void testClientConfiguredCustomContentEncoding() throws Exception { server.enqueue(new MockResponse() .setBody("ABCDE") .addHeader("Content-Encoding: custom")); server.play(); URLConnection connection = server.getUrl("/").openConnection(); connection.addRequestProperty("Accept-Encoding", "custom"); assertEquals("ABCDE", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); RecordedRequest request = server.takeRequest(); assertContains(request.getHeaders(), "Accept-Encoding: custom"); } /** * Test a bug where gzip input streams weren't exhausting the input stream, * which corrupted the request that followed. * http://code.google.com/p/android/issues/detail?id=7059 */ private void testClientConfiguredGzipContentEncodingAndConnectionReuse( TransferKind transferKind) throws Exception { MockResponse responseOne = new MockResponse(); responseOne.addHeader("Content-Encoding: gzip"); transferKind.setBody(responseOne, gzip("one (gzipped)".getBytes("UTF-8")), 5); server.enqueue(responseOne); MockResponse responseTwo = new MockResponse(); transferKind.setBody(responseTwo, "two (identity)", 5); server.enqueue(responseTwo); server.play(); URLConnection connection = server.getUrl("/").openConnection(); connection.addRequestProperty("Accept-Encoding", "gzip"); InputStream gunzippedIn = new GZIPInputStream(connection.getInputStream()); assertEquals("one (gzipped)", readAscii(gunzippedIn, Integer.MAX_VALUE)); assertEquals(0, server.takeRequest().getSequenceNumber()); connection = server.getUrl("/").openConnection(); assertEquals("two (identity)", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); assertEquals(1, server.takeRequest().getSequenceNumber()); } // JVM failure. // /** // * Test that HEAD requests don't have a body regardless of the response // * headers. http://code.google.com/p/android/issues/detail?id=24672 // */ // public void testHeadAndContentLength() throws Exception { // server.enqueue(new MockResponse() // .clearHeaders() // .addHeader("Content-Length: 100")); // server.enqueue(new MockResponse().setBody("A")); // server.play(); // // HttpURLConnection connection1 = (HttpURLConnection) server.getUrl("/").openConnection(); // connection1.setRequestMethod("HEAD"); // assertEquals("100", connection1.getHeaderField("Content-Length")); // assertContent("", connection1); // // HttpURLConnection connection2 = (HttpURLConnection) server.getUrl("/").openConnection(); // assertEquals("A", readAscii(connection2.getInputStream(), Integer.MAX_VALUE)); // // assertEquals(0, server.takeRequest().getSequenceNumber()); // assertEquals(1, server.takeRequest().getSequenceNumber()); // } // TODO(tball): b/28067294 // /** // * Test that request body chunking works. This test has been relaxed from treating // * the {@link java.net.HttpURLConnection#setChunkedStreamingMode(int)} // * chunk length as being fixed because OkHttp no longer guarantees // * the fixed chunk size. Instead, we check that chunking takes place // * and we force the chunk size with flushes. // */ // public void testSetChunkedStreamingMode() throws IOException, InterruptedException { // server.enqueue(new MockResponse()); // server.play(); // // HttpURLConnection urlConnection = (HttpURLConnection) server.getUrl("/").openConnection(); // // Later releases of Android ignore the value for chunkLength if it is > 0 and default to // // a fixed chunkLength. During the change-over period while the chunkLength indicates the // // chunk buffer size (inc. header) the chunkLength has to be >= 8. This enables the flush() // // to dictate the size of the chunks. // urlConnection.setChunkedStreamingMode(50 /* chunkLength */); // urlConnection.setDoOutput(true); // OutputStream outputStream = urlConnection.getOutputStream(); // String outputString = "ABCDEFGH"; // byte[] outputBytes = outputString.getBytes("US-ASCII"); // int targetChunkSize = 3; // for (int i = 0; i < outputBytes.length; i += targetChunkSize) { // int count = i + targetChunkSize < outputBytes.length ? 3 : outputBytes.length - i; // outputStream.write(outputBytes, i, count); // outputStream.flush(); // } // assertEquals(200, urlConnection.getResponseCode()); // // RecordedRequest request = server.takeRequest(); // assertEquals(outputString, new String(request.getBody(), "US-ASCII")); // assertEquals(Arrays.asList(3, 3, 2), request.getChunkSizes()); // } // TODO(tball): b/28067294 // public void testAuthenticateWithFixedLengthStreaming() throws Exception { // testAuthenticateWithStreamingPost(StreamingMode.FIXED_LENGTH); // } // TODO(tball): b/28067294 // public void testAuthenticateWithChunkedStreaming() throws Exception { // testAuthenticateWithStreamingPost(StreamingMode.CHUNKED); // } private void testAuthenticateWithStreamingPost(StreamingMode streamingMode) throws Exception { MockResponse pleaseAuthenticate = new MockResponse() .setResponseCode(401) .addHeader("WWW-Authenticate: Basic realm=\"protected area\"") .setBody("Please authenticate."); server.enqueue(pleaseAuthenticate); server.play(); Authenticator.setDefault(new SimpleAuthenticator()); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); connection.setDoOutput(true); byte[] requestBody = { 'A', 'B', 'C', 'D' }; if (streamingMode == StreamingMode.FIXED_LENGTH) { connection.setFixedLengthStreamingMode(requestBody.length); } else if (streamingMode == StreamingMode.CHUNKED) { connection.setChunkedStreamingMode(0); } OutputStream outputStream = connection.getOutputStream(); outputStream.write(requestBody); outputStream.close(); try { connection.getInputStream(); fail(); } catch (HttpRetryException expected) { } // no authorization header for the request... RecordedRequest request = server.takeRequest(); assertContainsNoneMatching(request.getHeaders(), "Authorization: Basic .*"); assertEquals(Arrays.toString(requestBody), Arrays.toString(request.getBody())); } public void testSetValidRequestMethod() throws Exception { server.play(); assertValidRequestMethod("GET"); assertValidRequestMethod("DELETE"); assertValidRequestMethod("HEAD"); assertValidRequestMethod("OPTIONS"); assertValidRequestMethod("POST"); assertValidRequestMethod("PUT"); assertValidRequestMethod("TRACE"); } private void assertValidRequestMethod(String requestMethod) throws Exception { HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); connection.setRequestMethod(requestMethod); assertEquals(requestMethod, connection.getRequestMethod()); } public void testSetInvalidRequestMethodLowercase() throws Exception { server.play(); assertInvalidRequestMethod("get"); } public void testSetInvalidRequestMethodConnect() throws Exception { server.play(); assertInvalidRequestMethod("CONNECT"); } private void assertInvalidRequestMethod(String requestMethod) throws Exception { HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); try { connection.setRequestMethod(requestMethod); fail(); } catch (ProtocolException expected) { } } public void testCannotSetNegativeFixedLengthStreamingMode() throws Exception { server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); try { connection.setFixedLengthStreamingMode(-2); fail(); } catch (IllegalArgumentException expected) { } } public void testCanSetNegativeChunkedStreamingMode() throws Exception { server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); connection.setChunkedStreamingMode(-2); } public void testCannotSetFixedLengthStreamingModeAfterConnect() throws Exception { server.enqueue(new MockResponse().setBody("A")); server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); assertEquals("A", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); try { connection.setFixedLengthStreamingMode(1); fail(); } catch (IllegalStateException expected) { } } public void testCannotSetChunkedStreamingModeAfterConnect() throws Exception { server.enqueue(new MockResponse().setBody("A")); server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); assertEquals("A", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); try { connection.setChunkedStreamingMode(1); fail(); } catch (IllegalStateException expected) { } } public void testCannotSetFixedLengthStreamingModeAfterChunkedStreamingMode() throws Exception { server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); connection.setChunkedStreamingMode(1); try { connection.setFixedLengthStreamingMode(1); fail(); } catch (IllegalStateException expected) { } } public void testCannotSetChunkedStreamingModeAfterFixedLengthStreamingMode() throws Exception { server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); connection.setFixedLengthStreamingMode(1); try { connection.setChunkedStreamingMode(1); fail(); } catch (IllegalStateException expected) { } } enum StreamingMode { FIXED_LENGTH, CHUNKED } // TODO(tball): b/28067294 // public void testAuthenticateWithPost() throws Exception { // MockResponse pleaseAuthenticate = new MockResponse() // .setResponseCode(401) // .addHeader("WWW-Authenticate: Basic realm=\"protected area\"") // .setBody("Please authenticate."); // // fail auth three times... // server.enqueue(pleaseAuthenticate); // server.enqueue(pleaseAuthenticate); // server.enqueue(pleaseAuthenticate); // // ...then succeed the fourth time // server.enqueue(new MockResponse().setBody("Successful auth!")); // server.play(); // // Authenticator.setDefault(new SimpleAuthenticator()); // HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); // connection.setDoOutput(true); // byte[] requestBody = { 'A', 'B', 'C', 'D' }; // OutputStream outputStream = connection.getOutputStream(); // outputStream.write(requestBody); // outputStream.close(); // assertEquals("Successful auth!", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); // // // no authorization header for the first request... // RecordedRequest request = server.takeRequest(); // assertContainsNoneMatching(request.getHeaders(), "Authorization: .*"); // // // ...but the three requests that follow include an authorization header // for (int i = 0; i < 3; i++) { // request = server.takeRequest(); // assertEquals("POST / HTTP/1.1", request.getRequestLine()); // assertContains(request.getHeaders(), "Authorization: Basic " // + SimpleAuthenticator.BASE_64_CREDENTIALS); // assertEquals(Arrays.toString(requestBody), Arrays.toString(request.getBody())); // } // } // JVM failure. // public void testAuthenticateWithGet() throws Exception { // MockResponse pleaseAuthenticate = new MockResponse() // .setResponseCode(401) // .addHeader("WWW-Authenticate: Basic realm=\"protected area\"") // .setBody("Please authenticate."); // // fail auth three times... // server.enqueue(pleaseAuthenticate); // server.enqueue(pleaseAuthenticate); // server.enqueue(pleaseAuthenticate); // // ...then succeed the fourth time // server.enqueue(new MockResponse().setBody("Successful auth!")); // server.play(); // // SimpleAuthenticator authenticator = new SimpleAuthenticator(); // Authenticator.setDefault(authenticator); // HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); // assertEquals("Successful auth!", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); // assertEquals(Authenticator.RequestorType.SERVER, authenticator.requestorType); // assertEquals(server.getPort(), authenticator.requestingPort); // assertEquals(InetAddress.getByName(server.getHostName()), authenticator.requestingSite); // assertEquals("protected area", authenticator.requestingPrompt); // assertEquals("http", authenticator.requestingProtocol); // assertEquals("Basic", authenticator.requestingScheme); // // // no authorization header for the first request... // RecordedRequest request = server.takeRequest(); // assertContainsNoneMatching(request.getHeaders(), "Authorization: .*"); // // // ...but the three requests that follow requests include an authorization header // for (int i = 0; i < 3; i++) { // request = server.takeRequest(); // assertEquals("GET / HTTP/1.1", request.getRequestLine()); // assertContains(request.getHeaders(), "Authorization: Basic " // + SimpleAuthenticator.BASE_64_CREDENTIALS); // } // } // TODO(tball): b/28067294 // // bug 11473660 // public void testAuthenticateWithLowerCaseHeadersAndScheme() throws Exception { // MockResponse pleaseAuthenticate = new MockResponse() // .setResponseCode(401) // .addHeader("www-authenticate: basic realm=\"protected area\"") // .setBody("Please authenticate."); // // fail auth three times... // server.enqueue(pleaseAuthenticate); // server.enqueue(pleaseAuthenticate); // server.enqueue(pleaseAuthenticate); // // ...then succeed the fourth time // server.enqueue(new MockResponse().setBody("Successful auth!")); // server.play(); // // SimpleAuthenticator authenticator = new SimpleAuthenticator(); // Authenticator.setDefault(authenticator); // HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); // assertEquals("Successful auth!", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); // assertEquals(Authenticator.RequestorType.SERVER, authenticator.requestorType); // assertEquals(server.getPort(), authenticator.requestingPort); // assertEquals(InetAddress.getByName(server.getHostName()), authenticator.requestingSite); // assertEquals("protected area", authenticator.requestingPrompt); // assertEquals("http", authenticator.requestingProtocol); // assertEquals("basic", authenticator.requestingScheme); // } // JVM failure. // // http://code.google.com/p/android/issues/detail?id=19081 // public void testAuthenticateWithCommaSeparatedAuthenticationMethods() throws Exception { // server.enqueue(new MockResponse() // .setResponseCode(401) // .addHeader("WWW-Authenticate: Scheme1 realm=\"a\", Basic realm=\"b\", " // + "Scheme3 realm=\"c\"") // .setBody("Please authenticate.")); // server.enqueue(new MockResponse().setBody("Successful auth!")); // server.play(); // // SimpleAuthenticator authenticator = new SimpleAuthenticator(); // authenticator.expectedPrompt = "b"; // Authenticator.setDefault(authenticator); // HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); // assertEquals("Successful auth!", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); // // assertContainsNoneMatching(server.takeRequest().getHeaders(), "Authorization: .*"); // assertContains(server.takeRequest().getHeaders(), // "Authorization: Basic " + SimpleAuthenticator.BASE_64_CREDENTIALS); // assertEquals("Basic", authenticator.requestingScheme); // } // JVM failure. // public void testAuthenticateWithMultipleAuthenticationHeaders() throws Exception { // server.enqueue(new MockResponse() // .setResponseCode(401) // .addHeader("WWW-Authenticate: Scheme1 realm=\"a\"") // .addHeader("WWW-Authenticate: Basic realm=\"b\"") // .addHeader("WWW-Authenticate: Scheme3 realm=\"c\"") // .setBody("Please authenticate.")); // server.enqueue(new MockResponse().setBody("Successful auth!")); // server.play(); // // SimpleAuthenticator authenticator = new SimpleAuthenticator(); // authenticator.expectedPrompt = "b"; // Authenticator.setDefault(authenticator); // HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); // assertEquals("Successful auth!", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); // // assertContainsNoneMatching(server.takeRequest().getHeaders(), "Authorization: .*"); // assertContains(server.takeRequest().getHeaders(), // "Authorization: Basic " + SimpleAuthenticator.BASE_64_CREDENTIALS); // assertEquals("Basic", authenticator.requestingScheme); // } // JVM failure. // public void testRedirectedWithChunkedEncoding() throws Exception { // testRedirected(TransferKind.CHUNKED, true); // } // JVM failure. // public void testRedirectedWithContentLengthHeader() throws Exception { // testRedirected(TransferKind.FIXED_LENGTH, true); // } public void testRedirectedWithNoLengthHeaders() throws Exception { testRedirected(TransferKind.END_OF_STREAM, false); } private void testRedirected(TransferKind transferKind, boolean reuse) throws Exception { MockResponse response = new MockResponse() .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) .addHeader("Location: /foo"); transferKind.setBody(response, "This page has moved!", 10); server.enqueue(response); server.enqueue(new MockResponse().setBody("This is the new location!")); server.play(); URLConnection connection = server.getUrl("/").openConnection(); assertEquals("This is the new location!", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); RecordedRequest first = server.takeRequest(); assertEquals("GET / HTTP/1.1", first.getRequestLine()); RecordedRequest retry = server.takeRequest(); assertEquals("GET /foo HTTP/1.1", retry.getRequestLine()); if (reuse) { assertEquals("Expected connection reuse", 1, retry.getSequenceNumber()); } } public void testNotRedirectedFromHttpToHttps() throws IOException, InterruptedException { server.enqueue(new MockResponse() .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) .addHeader("Location: https://anyhost/foo") .setBody("This page has moved!")); server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); assertEquals("This page has moved!", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); } public void testRedirectToAnotherOriginServer() throws Exception { MockWebServer server2 = new MockWebServer(); server2.enqueue(new MockResponse().setBody("This is the 2nd server!")); server2.play(); server.enqueue(new MockResponse() .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) .addHeader("Location: " + server2.getUrl("/").toString()) .setBody("This page has moved!")); server.enqueue(new MockResponse().setBody("This is the first server again!")); server.play(); URLConnection connection = server.getUrl("/").openConnection(); assertEquals("This is the 2nd server!", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); assertEquals(server2.getUrl("/"), connection.getURL()); // make sure the first server was careful to recycle the connection assertEquals("This is the first server again!", readAscii(server.getUrl("/").openStream(), Integer.MAX_VALUE)); RecordedRequest first = server.takeRequest(); assertContains(first.getHeaders(), "Host: " + hostName + ":" + server.getPort()); RecordedRequest second = server2.takeRequest(); assertContains(second.getHeaders(), "Host: " + hostName + ":" + server2.getPort()); RecordedRequest third = server.takeRequest(); // assertEquals("Expected connection reuse", 1, third.getSequenceNumber()); // JVM failure server2.shutdown(); } // TODO(tball): b/28067294 // // http://b/27590872 - assert we do not throw a runtime exception if a server responds with // // a location that cannot be represented directly by URI. // public void testRedirectWithInvalidRedirectUrl() throws Exception { // // The first server hosts a redirect to a second. We need two so that the ProxySelector // // installed is used for the redirect. Otherwise the second request will be handled via the // // existing keep-alive connection. // server.play(); // // MockWebServer server2 = new MockWebServer(); // server2.play(); // // String targetPath = "/target"; // // The "%0" in the suffix is invalid without a second digit. // String invalidSuffix = "?foo=%0&bar=%00"; // // String redirectPath = server2.getUrl(targetPath).toString(); // String invalidRedirectUri = redirectPath + invalidSuffix; // // // Redirect to the invalid URI. // server.enqueue(new MockResponse() // .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) // .addHeader("Location: " + invalidRedirectUri)); // // server2.enqueue(new MockResponse().setBody("Target")); // // // Assert the target URI is actually invalid. // try { // new URI(invalidRedirectUri); // fail("Target URL is expected to be invalid"); // } catch (URISyntaxException expected) {} // // // The ProxySelector requires a URI object, which forces the HttpURLConnectionImpl to create // // a URI object containing a string based on the redirect address, regardless of what it is // // using internally to hold the target address. // ProxySelector originalSelector = ProxySelector.getDefault(); // final List<URI> proxySelectorUris = new ArrayList<>(); // ProxySelector.setDefault(new ProxySelector() { // @Override // public List<Proxy> select(URI uri) { // if (uri.getScheme().equals("http")) { // // Ignore socks proxy lookups. // proxySelectorUris.add(uri); // } // return Collections.singletonList(Proxy.NO_PROXY); // } // // @Override // public void connectFailed(URI uri, SocketAddress sa, IOException ioe) { // // no-op // } // }); // // try { // HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); // assertEquals("Target", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); // // // Inspect the redirect request to see what request was actually made. // RecordedRequest actualRequest = server2.takeRequest(); // assertEquals(targetPath + invalidSuffix, actualRequest.getPath()); // // // The first URI will be the initial request. We want to inspect the redirect. // URI uri = proxySelectorUris.get(1); // // The HttpURLConnectionImpl converts %0 -> %250. i.e. it escapes the %. // assertEquals(redirectPath + "?foo=%250&bar=%00", uri.toString()); // } finally { // ProxySelector.setDefault(originalSelector); // server2.shutdown(); // } // } public void testInstanceFollowsRedirects() throws Exception { testInstanceFollowsRedirects("http://www.google.com/"); testInstanceFollowsRedirects("https://www.google.com/"); } private void testInstanceFollowsRedirects(String spec) throws Exception { URL url = new URL(spec); HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); urlConnection.setInstanceFollowRedirects(true); assertTrue(urlConnection.getInstanceFollowRedirects()); urlConnection.setInstanceFollowRedirects(false); assertFalse(urlConnection.getInstanceFollowRedirects()); } public void testFollowRedirects() throws Exception { testFollowRedirects("http://www.google.com/"); testFollowRedirects("https://www.google.com/"); } private void testFollowRedirects(String spec) throws Exception { URL url = new URL(spec); boolean originalValue = HttpURLConnection.getFollowRedirects(); try { HttpURLConnection.setFollowRedirects(false); assertFalse(HttpURLConnection.getFollowRedirects()); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); assertFalse(connection.getInstanceFollowRedirects()); HttpURLConnection.setFollowRedirects(true); assertTrue(HttpURLConnection.getFollowRedirects()); HttpURLConnection connection2 = (HttpURLConnection) url.openConnection(); assertTrue(connection2.getInstanceFollowRedirects()); } finally { HttpURLConnection.setFollowRedirects(originalValue); } } // TODO(tball): b/28067294 // public void testResponse300MultipleChoiceWithPost() throws Exception { // // Chrome doesn't follow the redirect, but Firefox and the RI both do // testResponseRedirectedWithPost(HttpURLConnection.HTTP_MULT_CHOICE); // } public void testResponse301MovedPermanentlyWithPost() throws Exception { testResponseRedirectedWithPost(HttpURLConnection.HTTP_MOVED_PERM); } public void testResponse302MovedTemporarilyWithPost() throws Exception { testResponseRedirectedWithPost(HttpURLConnection.HTTP_MOVED_TEMP); } public void testResponse303SeeOtherWithPost() throws Exception { testResponseRedirectedWithPost(HttpURLConnection.HTTP_SEE_OTHER); } private void testResponseRedirectedWithPost(int redirectCode) throws Exception { server.enqueue(new MockResponse() .setResponseCode(redirectCode) .addHeader("Location: /page2") .setBody("This page has moved!")); server.enqueue(new MockResponse().setBody("Page 2")); server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/page1").openConnection(); connection.setDoOutput(true); byte[] requestBody = { 'A', 'B', 'C', 'D' }; OutputStream outputStream = connection.getOutputStream(); outputStream.write(requestBody); outputStream.close(); assertEquals("Page 2", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); assertTrue(connection.getDoOutput()); RecordedRequest page1 = server.takeRequest(); assertEquals("POST /page1 HTTP/1.1", page1.getRequestLine()); assertEquals(Arrays.toString(requestBody), Arrays.toString(page1.getBody())); RecordedRequest page2 = server.takeRequest(); assertEquals("GET /page2 HTTP/1.1", page2.getRequestLine()); } // JVM failure. // public void testResponse305UseProxy() throws Exception { // server.play(); // server.enqueue(new MockResponse() // .setResponseCode(HttpURLConnection.HTTP_USE_PROXY) // .addHeader("Location: " + server.getUrl("/")) // .setBody("This page has moved!")); // server.enqueue(new MockResponse().setBody("Proxy Response")); // // HttpURLConnection connection = (HttpURLConnection) server.getUrl("/foo").openConnection(); // // Fails on the RI, which gets "Proxy Response" // assertEquals("This page has moved!", // readAscii(connection.getInputStream(), Integer.MAX_VALUE)); // // RecordedRequest page1 = server.takeRequest(); // assertEquals("GET /foo HTTP/1.1", page1.getRequestLine()); // assertEquals(1, server.getRequestCount()); // } // TODO(tball): b/28067294 // public void testReadTimeouts() throws IOException { // /* // * This relies on the fact that MockWebServer doesn't close the // * connection after a response has been sent. This causes the client to // * try to read more bytes than are sent, which results in a timeout. // */ // MockResponse timeout = new MockResponse() // .setBody("ABC") // .clearHeaders() // .addHeader("Content-Length: 4"); // server.enqueue(timeout); // server.enqueue(new MockResponse().setBody("unused")); // to keep the server alive // server.play(); // // URLConnection urlConnection = server.getUrl("/").openConnection(); // urlConnection.setReadTimeout(1000); // InputStream in = urlConnection.getInputStream(); // assertEquals('A', in.read()); // assertEquals('B', in.read()); // assertEquals('C', in.read()); // try { // in.read(); // if Content-Length was accurate, this would return -1 immediately // fail(); // } catch (SocketTimeoutException expected) { // } // } public void testSetChunkedEncodingAsRequestProperty() throws IOException, InterruptedException { server.enqueue(new MockResponse()); server.play(); HttpURLConnection urlConnection = (HttpURLConnection) server.getUrl("/").openConnection(); urlConnection.setRequestProperty("Transfer-encoding", "chunked"); urlConnection.setDoOutput(true); urlConnection.getOutputStream().write("ABC".getBytes("UTF-8")); assertEquals(200, urlConnection.getResponseCode()); RecordedRequest request = server.takeRequest(); assertEquals("ABC", new String(request.getBody(), "UTF-8")); } // JVM failure. // public void testConnectionCloseInRequest() throws IOException, InterruptedException { // server.enqueue(new MockResponse()); // server doesn't honor the connection: close header! // server.enqueue(new MockResponse()); // server.play(); // // HttpURLConnection a = (HttpURLConnection) server.getUrl("/").openConnection(); // a.setRequestProperty("Connection", "close"); // assertEquals(200, a.getResponseCode()); // // HttpURLConnection b = (HttpURLConnection) server.getUrl("/").openConnection(); // assertEquals(200, b.getResponseCode()); // // assertEquals(0, server.takeRequest().getSequenceNumber()); // assertEquals("When connection: close is used, each request should get its own connection", // 0, server.takeRequest().getSequenceNumber()); // } public void testConnectionCloseInResponse() throws IOException, InterruptedException { server.enqueue(new MockResponse().addHeader("Connection: close")); server.enqueue(new MockResponse()); server.play(); HttpURLConnection a = (HttpURLConnection) server.getUrl("/").openConnection(); assertEquals(200, a.getResponseCode()); HttpURLConnection b = (HttpURLConnection) server.getUrl("/").openConnection(); assertEquals(200, b.getResponseCode()); assertEquals(0, server.takeRequest().getSequenceNumber()); assertEquals("When connection: close is used, each request should get its own connection", 0, server.takeRequest().getSequenceNumber()); } public void testConnectionCloseWithRedirect() throws IOException, InterruptedException { MockResponse response = new MockResponse() .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) .addHeader("Location: /foo") .addHeader("Connection: close"); server.enqueue(response); server.enqueue(new MockResponse().setBody("This is the new location!")); server.play(); URLConnection connection = server.getUrl("/").openConnection(); assertEquals("This is the new location!", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); assertEquals(0, server.takeRequest().getSequenceNumber()); assertEquals("When connection: close is used, each request should get its own connection", 0, server.takeRequest().getSequenceNumber()); } // JVM failure. // public void testResponseCodeDisagreesWithHeaders() throws IOException, InterruptedException { // server.enqueue(new MockResponse() // .setResponseCode(HttpURLConnection.HTTP_NO_CONTENT) // .setBody("This body is not allowed!")); // server.play(); // // URLConnection connection = server.getUrl("/").openConnection(); // assertEquals("This body is not allowed!", // readAscii(connection.getInputStream(), Integer.MAX_VALUE)); // } public void testSingleByteReadIsSigned() throws IOException { server.enqueue(new MockResponse().setBody(new byte[] { -2, -1 })); server.play(); URLConnection connection = server.getUrl("/").openConnection(); InputStream in = connection.getInputStream(); assertEquals(254, in.read()); assertEquals(255, in.read()); assertEquals(-1, in.read()); } // TODO(tball): b/28067294 // public void testFlushAfterStreamTransmittedWithChunkedEncoding() throws IOException { // testFlushAfterStreamTransmitted(TransferKind.CHUNKED); // } // TODO(tball): b/28067294 // public void testFlushAfterStreamTransmittedWithFixedLength() throws IOException { // testFlushAfterStreamTransmitted(TransferKind.FIXED_LENGTH); // } // JVM failure. // public void testFlushAfterStreamTransmittedWithNoLengthHeaders() throws IOException { // testFlushAfterStreamTransmitted(TransferKind.END_OF_STREAM); // } /** * We explicitly permit apps to close the upload stream even after it has * been transmitted. We also permit flush so that buffered streams can * do a no-op flush when they are closed. http://b/3038470 */ private void testFlushAfterStreamTransmitted(TransferKind transferKind) throws IOException { server.enqueue(new MockResponse().setBody("abc")); server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); connection.setDoOutput(true); byte[] upload = "def".getBytes("UTF-8"); if (transferKind == TransferKind.CHUNKED) { connection.setChunkedStreamingMode(0); } else if (transferKind == TransferKind.FIXED_LENGTH) { connection.setFixedLengthStreamingMode(upload.length); } OutputStream out = connection.getOutputStream(); out.write(upload); assertEquals("abc", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); out.flush(); // dubious but permitted try { out.write("ghi".getBytes("UTF-8")); fail(); } catch (IOException expected) { } } // Hangs when run on JVM, iOS. // public void testGetHeadersThrows() throws IOException { // server.enqueue(new MockResponse().setSocketPolicy(DISCONNECT_AT_START)); // server.play(); // // HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); // try { // connection.getInputStream(); // fail(); // } catch (IOException expected) { // } // // try { // connection.getInputStream(); // fail(); // } catch (IOException expected) { // } // } public void testReadTimeoutsOnRecycledConnections() throws Exception { server.enqueue(new MockResponse().setBody("ABC")); server.play(); // The request should work once and then fail URLConnection connection = server.getUrl("").openConnection(); // Read timeout of a day, sure to cause the test to timeout and fail. connection.setReadTimeout(24 * 3600 * 1000); InputStream input = connection.getInputStream(); assertEquals("ABC", readAscii(input, Integer.MAX_VALUE)); input.close(); try { connection = server.getUrl("").openConnection(); // Set the read timeout back to 100ms, this request will time out // because we've only enqueued one response. connection.setReadTimeout(100); connection.getInputStream(); fail(); } catch (IOException expected) { } } // JVM failure. // /** // * This test goes through the exhaustive set of interesting ASCII characters // * because most of those characters are interesting in some way according to // * RFC 2396 and RFC 2732. http://b/1158780 // * After M, Android's HttpURLConnection started canonicalizing hostnames to lower case, IDN // * encoding and being more strict about invalid characters. // */ // public void testUrlCharacterMapping() throws Exception { // server.setDispatcher(new Dispatcher() { // @Override public MockResponse dispatch(RecordedRequest request) // throws InterruptedException { // return new MockResponse(); // } // }); // server.play(); // // // alphanum // testUrlToUriMapping("abzABZ09", "abzabz09", "abzABZ09", "abzABZ09", "abzABZ09"); // testUrlToRequestMapping("abzABZ09", "abzABZ09", "abzABZ09"); // // // control characters // // // On JB-MR2 and below, we would allow a host containing \u0000 // // and then generate a request with a Host header that violated RFC2616. // // We now reject such hosts. // // // // The ideal behaviour here is to be "lenient" about the host and rewrite // // it, but attempting to do so introduces a new range of incompatible // // behaviours. // testUrlToUriMapping("\u0000", null, "%00", "%00", "%00"); // RI fails this // testUrlToRequestMapping("\u0000", "%00", "%00"); // // testUrlToUriMapping("\u0001", null, "%01", "%01", "%01"); // testUrlToRequestMapping("\u0001", "%01", "%01"); // // testUrlToUriMapping("\u001f", null, "%1F", "%1F", "%1F"); // testUrlToRequestMapping("\u001f", "%1F", "%1F"); // // // ascii characters // testUrlToUriMapping("%20", null, "%20", "%20", "%20"); // testUrlToRequestMapping("%20", "%20", "%20"); // testUrlToUriMapping(" ", null, "%20", "%20", "%20"); // testUrlToRequestMapping(" ", "%20", "%20"); // testUrlToUriMapping("!", "!", "!", "!", "!"); // testUrlToRequestMapping("!", "!", "!"); // testUrlToUriMapping("\"", null, "%22", "%22", "%22"); // testUrlToRequestMapping("\"", "%22", "%22"); // testUrlToUriMapping("#", null, null, null, "%23"); // testUrlToRequestMapping("#", null, null); // testUrlToUriMapping("$", "$", "$", "$", "$"); // testUrlToRequestMapping("$", "$", "$"); // testUrlToUriMapping("&", "&", "&", "&", "&"); // testUrlToRequestMapping("&", "&", "&"); // testUrlToUriMapping("'", "'", "'", "%27", "'"); // testUrlToRequestMapping("'", "'", "%27"); // testUrlToUriMapping("(", "(", "(", "(", "("); // testUrlToRequestMapping("(", "(", "("); // testUrlToUriMapping(")", ")", ")", ")", ")"); // testUrlToRequestMapping(")", ")", ")"); // testUrlToUriMapping("*", "*", "*", "*", "*"); // testUrlToRequestMapping("*", "*", "*"); // testUrlToUriMapping("+", "+", "+", "+", "+"); // testUrlToRequestMapping("+", "+", "+"); // testUrlToUriMapping(",", ",", ",", ",", ","); // testUrlToRequestMapping(",", ",", ","); // testUrlToUriMapping("-", "-", "-", "-", "-"); // testUrlToRequestMapping("-", "-", "-"); // testUrlToUriMapping(".", null, ".", ".", "."); // testUrlToRequestMapping(".", ".", "."); // testUrlToUriMapping(".foo", ".foo", ".foo", ".foo", ".foo"); // testUrlToRequestMapping(".foo", ".foo", ".foo"); // testUrlToUriMapping("/", null, "/", "/", "/"); // testUrlToRequestMapping("/", "/", "/"); // testUrlToUriMapping(":", null, ":", ":", ":"); // testUrlToRequestMapping(":", ":", ":"); // testUrlToUriMapping(";", ";", ";", ";", ";"); // testUrlToRequestMapping(";", ";", ";"); // testUrlToUriMapping("<", null, "%3C", "%3C", "%3C"); // testUrlToRequestMapping("<", "%3C", "%3C"); // testUrlToUriMapping("=", "=", "=", "=", "="); // testUrlToRequestMapping("=", "=", "="); // testUrlToUriMapping(">", null, "%3E", "%3E", "%3E"); // testUrlToRequestMapping(">", "%3E", "%3E"); // testUrlToUriMapping("?", null, null, "?", "?"); // testUrlToRequestMapping("?", null, "?"); // testUrlToUriMapping("@", null, "@", "@", "@"); // testUrlToRequestMapping("@", "@", "@"); // testUrlToUriMapping("[", null, null, null, "%5B"); // testUrlToRequestMapping("[", null, null); // testUrlToUriMapping("\\", null, null, null, "%5C"); // testUrlToRequestMapping("\\", null, null); // testUrlToUriMapping("]", null, null, null, "%5D"); // testUrlToRequestMapping("]", null, null); // testUrlToUriMapping("^", null, "%5E", null, "%5E"); // testUrlToRequestMapping("^", "%5E", null); // testUrlToUriMapping("_", "_", "_", "_", "_"); // testUrlToRequestMapping("_", "_", "_"); // testUrlToUriMapping("`", null, "%60", null, "%60"); // testUrlToRequestMapping("`", "%60", null); // testUrlToUriMapping("{", null, "%7B", null, "%7B"); // testUrlToRequestMapping("{", "%7B", null); // testUrlToUriMapping("|", null, "%7C", null, "%7C"); // testUrlToRequestMapping("|", "%7C", null); // testUrlToUriMapping("}", null, "%7D", null, "%7D"); // testUrlToRequestMapping("}", "%7D", null); // testUrlToUriMapping("~", "~", "~", "~", "~"); // testUrlToRequestMapping("~", "~", "~"); // testUrlToUriMapping("\u007f", null, "%7F", "%7F", "%7F"); // testUrlToRequestMapping("\u007f", "%7F", "%7F"); // // // beyond ASCII // // // 0x80 is the code point for the Euro sign in CP1252 (but not 8859-15 or Unicode). // // Unicode code point 0x80 is a control character and maps to {0xC2, 0x80} in UTF-8. // // 0x80 is outside of the ASCII range and is not supported by IDN in hostnames. // testUrlToUriMapping("\u0080", null, "%C2%80", "%C2%80", "%C2%80"); // testUrlToRequestMapping("\u0080", "%C2%80", "%C2%80"); // // // More complicated transformations for the authorities below. // // // 0x20AC is the code point for the Euro sign in Unicode. // // Unicode code point 0x20AC maps to {0xE2, 0x82, 0xAC} in UTF-8 // // 0x20AC is not supported by all registrars but there are some legacy domains that // // use it and Android currently supports IDN conversion for it. // testUrlToUriMapping("\u20ac", null /* skip */, "%E2%82%AC", "%E2%82%AC", "%E2%82%AC"); // testUrlToUriMappingAuthority("http://host\u20ac.tld/", "http://xn--host-yv7a.tld/"); // testUrlToRequestMapping("\u20ac", "%E2%82%AC", "%E2%82%AC"); // // // UTF-16 {0xD842, 0xDF9F} -> Unicode 0x20B9F (a Kanji character) // // Unicode code point 0x20B9F maps to {0xF0, 0xA0, 0xAE, 0x9F} in UTF-8 // // IDN can deal with this code point. // testUrlToUriMapping("\ud842\udf9f", null /* skip */, "%F0%A0%AE%9F", "%F0%A0%AE%9F", // "%F0%A0%AE%9F"); // testUrlToUriMappingAuthority("http://host\uD842\uDF9F.tld/", "http://xn--host-ov06c.tld/"); // testUrlToRequestMapping("\ud842\udf9f", "%F0%A0%AE%9F", "%F0%A0%AE%9F"); // } private void testUrlToUriMappingAuthority(String urlString, String expectedUriString) throws Exception { URI authorityUri = backdoorUrlToUri(new URL(urlString)); assertEquals(expectedUriString, authorityUri.toString()); } /** * Exercises HttpURLConnection to convert URL to a URI. Unlike URL#toURI, * HttpURLConnection recovers from URLs with unescaped but unsupported URI * characters like '{' and '|' by escaping these characters. */ private URI backdoorUrlToUri(URL url) throws Exception { final AtomicReference<URI> uriReference = new AtomicReference<URI>(); ResponseCache.setDefault(new ResponseCache() { @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException { return null; } @Override public CacheResponse get(URI uri, String requestMethod, Map<String, List<String>> requestHeaders) throws IOException { uriReference.set(uri); throw new UnsupportedOperationException(); } }); try { HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.getResponseCode(); } catch (Exception expected) { } return uriReference.get(); } /* * Test the request that would be made by making an actual request a MockWebServer and capturing * the request made. * * Any "as" values that are null are not tested. */ private void testUrlToRequestMapping( String string, String asFile, String asQuery) throws Exception { if (asFile != null) { URL fileUrl = server.getUrl("/file" + string + "/#discarded"); HttpURLConnection urlConnection = (HttpURLConnection) fileUrl.openConnection(); // Bypass the cache. urlConnection.setUseCaches(false); assertEquals(200, urlConnection.getResponseCode()); assertEquals("/file" + asFile + "/", server.takeRequest().getPath()); } if (asQuery != null) { URL queryUrl = server.getUrl("/file?q" + string + "=x#discarded"); HttpURLConnection urlConnection = (HttpURLConnection) queryUrl.openConnection(); // Bypass the cache. urlConnection.setUseCaches(false); assertEquals(200, urlConnection.getResponseCode()); assertEquals("/file?q" + asQuery + "=x", server.takeRequest().getPath()); } } /* * Test the request that would be made by looking at the URI presented to the cache. This * includes the likely host name that would be used if a request were made. The cache throws an * exception so no request is actually made. * * Any "as" values that are null are not tested. */ private void testUrlToUriMapping(String string, String asAuthority, String asFile, String asQuery, String asFragment) throws Exception { if (asAuthority != null) { URI authorityUri = backdoorUrlToUri(new URL("http://host" + string + ".tld/")); assertEquals("http://host" + asAuthority + ".tld/", authorityUri.toString()); } if (asFile != null) { URI fileUri = backdoorUrlToUri(new URL("http://host.tld/file" + string + "/")); assertEquals("http://host.tld/file" + asFile + "/", fileUri.toString()); } if (asQuery != null) { URI queryUri = backdoorUrlToUri(new URL("http://host.tld/file?q" + string + "=x")); assertEquals("http://host.tld/file?q" + asQuery + "=x", queryUri.toString()); } assertEquals("http://host.tld/file#" + asFragment + "-x", backdoorUrlToUri(new URL("http://host.tld/file#" + asFragment + "-x")).toString()); } // TODO(tball): b/28067294 // public void testHostWithNul() throws Exception { // URL url = new URL("http://host\u0000/"); // try { // url.openStream(); // fail(); // } catch (UnknownHostException expected) { // } catch (IllegalArgumentException expected) { // } // } /** * Don't explode if the cache returns a null body. http://b/3373699 */ public void testResponseCacheReturnsNullOutputStream() throws Exception { final AtomicBoolean aborted = new AtomicBoolean(); ResponseCache.setDefault(new ResponseCache() { @Override public CacheResponse get(URI uri, String requestMethod, Map<String, List<String>> requestHeaders) throws IOException { return null; } @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException { return new CacheRequest() { @Override public void abort() { aborted.set(true); } @Override public OutputStream getBody() throws IOException { return null; } }; } }); server.enqueue(new MockResponse().setBody("abcdef")); server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); InputStream in = connection.getInputStream(); assertEquals("abc", readAscii(in, 3)); in.close(); assertFalse(aborted.get()); // The best behavior is ambiguous, but RI 6 doesn't abort here } /** * http://code.google.com/p/android/issues/detail?id=14562 */ public void testReadAfterLastByte() throws Exception { server.enqueue(new MockResponse() .setBody("ABC") .clearHeaders() .addHeader("Connection: close") .setSocketPolicy(SocketPolicy.DISCONNECT_AT_END)); server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); InputStream in = connection.getInputStream(); assertEquals("ABC", readAscii(in, 3)); assertEquals(-1, in.read()); assertEquals(-1, in.read()); // throws IOException in Gingerbread } // JVM failure. // public void testGetContent() throws Exception { // server.enqueue(new MockResponse().setBody("A")); // server.play(); // HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); // InputStream in = (InputStream) connection.getContent(); // assertEquals("A", readAscii(in, Integer.MAX_VALUE)); // } // JVM failure. // public void testGetContentOfType() throws Exception { // server.enqueue(new MockResponse().setBody("A")); // server.play(); // HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); // try { // connection.getContent(null); // fail(); // } catch (NullPointerException expected) { // } // try { // connection.getContent(new Class[] { null }); // fail(); // } catch (NullPointerException expected) { // } // assertNull(connection.getContent(new Class[] { getClass() })); // connection.disconnect(); // } // TODO(tball): b/28067294 // public void testGetOutputStreamOnGetFails() throws Exception { // server.enqueue(new MockResponse()); // server.play(); // HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); // try { // connection.getOutputStream(); // fail(); // } catch (ProtocolException expected) { // } // } // TODO(tball): b/28067294 // public void testGetOutputAfterGetInputStreamFails() throws Exception { // server.enqueue(new MockResponse()); // server.play(); // HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); // connection.setDoOutput(true); // try { // connection.getInputStream(); // connection.getOutputStream(); // fail(); // } catch (ProtocolException expected) { // } // } public void testSetDoOutputOrDoInputAfterConnectFails() throws Exception { server.enqueue(new MockResponse()); server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); connection.connect(); try { connection.setDoOutput(true); fail(); } catch (IllegalStateException expected) { } try { connection.setDoInput(true); fail(); } catch (IllegalStateException expected) { } connection.disconnect(); } public void testLastModified() throws Exception { server.enqueue(new MockResponse() .addHeader("Last-Modified", "Wed, 27 Nov 2013 11:26:00 GMT") .setBody("Hello")); server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); connection.connect(); assertEquals(1385551560000L, connection.getLastModified()); assertEquals(1385551560000L, connection.getHeaderFieldDate("Last-Modified", -1)); } public void testClientSendsContentLength() throws Exception { server.enqueue(new MockResponse().setBody("A")); server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); connection.setDoOutput(true); OutputStream out = connection.getOutputStream(); out.write(new byte[] { 'A', 'B', 'C' }); out.close(); assertEquals("A", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); RecordedRequest request = server.takeRequest(); assertContains(request.getHeaders(), "Content-Length: 3"); } public void testGetContentLengthConnects() throws Exception { server.enqueue(new MockResponse().setBody("ABC")); server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); assertEquals(3, connection.getContentLength()); connection.disconnect(); } public void testGetContentTypeConnects() throws Exception { server.enqueue(new MockResponse() .addHeader("Content-Type: text/plain") .setBody("ABC")); server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); assertEquals("text/plain", connection.getContentType()); connection.disconnect(); } public void testGetContentEncodingConnects() throws Exception { server.enqueue(new MockResponse() .addHeader("Content-Encoding: identity") .setBody("ABC")); server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); assertEquals("identity", connection.getContentEncoding()); connection.disconnect(); } // http://b/4361656 public void testUrlContainsQueryButNoPath() throws Exception { server.enqueue(new MockResponse().setBody("A")); server.play(); URL url = new URL("http", server.getHostName(), server.getPort(), "?query"); assertEquals("A", readAscii(url.openConnection().getInputStream(), Integer.MAX_VALUE)); RecordedRequest request = server.takeRequest(); assertEquals("GET /?query HTTP/1.1", request.getRequestLine()); } // http://code.google.com/p/android/issues/detail?id=20442 public void testInputStreamAvailableWithChunkedEncoding() throws Exception { testInputStreamAvailable(TransferKind.CHUNKED); } public void testInputStreamAvailableWithContentLengthHeader() throws Exception { testInputStreamAvailable(TransferKind.FIXED_LENGTH); } public void testInputStreamAvailableWithNoLengthHeaders() throws Exception { testInputStreamAvailable(TransferKind.END_OF_STREAM); } private void testInputStreamAvailable(TransferKind transferKind) throws IOException { String body = "ABCDEFGH"; MockResponse response = new MockResponse(); transferKind.setBody(response, body, 4); server.enqueue(response); server.play(); URLConnection connection = server.getUrl("/").openConnection(); InputStream in = connection.getInputStream(); for (int i = 0; i < body.length(); i++) { assertTrue(in.available() >= 0); assertEquals(body.charAt(i), in.read()); } assertEquals(0, in.available()); assertEquals(-1, in.read()); } // TODO(tball): b/28067294 // // http://code.google.com/p/android/issues/detail?id=28095 // public void testInvalidIpv4Address() throws Exception { // try { // URI uri = new URI("http://1111.111.111.111/index.html"); // uri.toURL().openConnection().connect(); // fail(); // } catch (UnknownHostException expected) { // } // } // TODO(tball): b/28067294 // // http://code.google.com/p/android/issues/detail?id=16895 // public void testUrlWithSpaceInHost() throws Exception { // URLConnection urlConnection = new URL("http://and roid.com/").openConnection(); // try { // urlConnection.getInputStream(); // fail(); // } catch (UnknownHostException expected) { // } // } // JVM failure. // // http://code.google.com/p/android/issues/detail?id=16895 // public void testUrlWithSpaceInHostViaHttpProxy() throws Exception { // server.enqueue(new MockResponse()); // server.play(); // URLConnection urlConnection = new URL("http://and roid.com/") // .openConnection(server.toProxyAddress()); // // try { // // This test is to check that a NullPointerException is not thrown. // urlConnection.getInputStream(); // fail(); // the RI makes a bogus proxy request for "GET http://and roid.com/ HTTP/1.1" // } catch (UnknownHostException expected) { // } // } /** * Returns a gzipped copy of {@code bytes}. */ public byte[] gzip(byte[] bytes) throws IOException { ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); OutputStream gzippedOut = new GZIPOutputStream(bytesOut); gzippedOut.write(bytes); gzippedOut.close(); return bytesOut.toByteArray(); } /** * Reads at most {@code limit} characters from {@code in} and asserts that * content equals {@code expected}. */ private void assertContent(String expected, URLConnection connection, int limit) throws IOException { connection.connect(); assertEquals(expected, readAscii(connection.getInputStream(), limit)); ((HttpURLConnection) connection).disconnect(); } private void assertContent(String expected, URLConnection connection) throws IOException { assertContent(expected, connection, Integer.MAX_VALUE); } private void assertContains(List<String> list, String value) { assertTrue(list.toString(), list.contains(value)); } private void assertContainsNoneMatching(List<String> list, String pattern) { for (String header : list) { if (header.matches(pattern)) { fail("Header " + header + " matches " + pattern); } } } private Set<String> newSet(String... elements) { return new HashSet<String>(Arrays.asList(elements)); } enum TransferKind { CHUNKED() { @Override void setBody(MockResponse response, byte[] content, int chunkSize) throws IOException { response.setChunkedBody(content, chunkSize); } }, FIXED_LENGTH() { @Override void setBody(MockResponse response, byte[] content, int chunkSize) { response.setBody(content); } }, END_OF_STREAM() { @Override void setBody(MockResponse response, byte[] content, int chunkSize) { response.setBody(content); response.setSocketPolicy(DISCONNECT_AT_END); for (Iterator<String> h = response.getHeaders().iterator(); h.hasNext(); ) { if (h.next().startsWith("Content-Length:")) { h.remove(); break; } } } }; abstract void setBody(MockResponse response, byte[] content, int chunkSize) throws IOException; void setBody(MockResponse response, String content, int chunkSize) throws IOException { setBody(response, content.getBytes("UTF-8"), chunkSize); } } enum ProxyConfig { NO_PROXY() { @Override public HttpURLConnection connect(MockWebServer server, URL url) throws IOException { return (HttpURLConnection) url.openConnection(Proxy.NO_PROXY); } }, CREATE_ARG() { @Override public HttpURLConnection connect(MockWebServer server, URL url) throws IOException { return (HttpURLConnection) url.openConnection(server.toProxyAddress()); } }, PROXY_SYSTEM_PROPERTY() { @Override public HttpURLConnection connect(MockWebServer server, URL url) throws IOException { System.setProperty("proxyHost", "localhost"); System.setProperty("proxyPort", Integer.toString(server.getPort())); return (HttpURLConnection) url.openConnection(); } }, HTTP_PROXY_SYSTEM_PROPERTY() { @Override public HttpURLConnection connect(MockWebServer server, URL url) throws IOException { System.setProperty("http.proxyHost", "localhost"); System.setProperty("http.proxyPort", Integer.toString(server.getPort())); return (HttpURLConnection) url.openConnection(); } }, HTTPS_PROXY_SYSTEM_PROPERTY() { @Override public HttpURLConnection connect(MockWebServer server, URL url) throws IOException { System.setProperty("https.proxyHost", "localhost"); System.setProperty("https.proxyPort", Integer.toString(server.getPort())); return (HttpURLConnection) url.openConnection(); } }; public abstract HttpURLConnection connect(MockWebServer server, URL url) throws IOException; } private static class SimpleAuthenticator extends Authenticator { /** base64("username:password") */ private static final String BASE_64_CREDENTIALS = "dXNlcm5hbWU6cGFzc3dvcmQ="; private String expectedPrompt; private RequestorType requestorType; private int requestingPort; private InetAddress requestingSite; private String requestingPrompt; private String requestingProtocol; private String requestingScheme; protected PasswordAuthentication getPasswordAuthentication() { requestorType = getRequestorType(); requestingPort = getRequestingPort(); requestingSite = getRequestingSite(); requestingPrompt = getRequestingPrompt(); requestingProtocol = getRequestingProtocol(); requestingScheme = getRequestingScheme(); return (expectedPrompt == null || expectedPrompt.equals(requestingPrompt)) ? new PasswordAuthentication("username", "password".toCharArray()) : null; } } }