1 // Copyright 2015 The Chromium Authors 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.net.urlconnection; 6 7 import static com.google.common.truth.Truth.assertThat; 8 9 import static org.junit.Assert.assertThrows; 10 11 import androidx.test.ext.junit.runners.AndroidJUnit4; 12 import androidx.test.filters.SmallTest; 13 14 import org.junit.After; 15 import org.junit.Before; 16 import org.junit.Rule; 17 import org.junit.Test; 18 import org.junit.runner.RunWith; 19 20 import org.chromium.base.test.util.Batch; 21 import org.chromium.net.CronetEngine; 22 import org.chromium.net.CronetTestRule; 23 import org.chromium.net.CronetTestRule.CronetImplementation; 24 import org.chromium.net.CronetTestRule.IgnoreFor; 25 import org.chromium.net.NativeTestServer; 26 import org.chromium.net.NetworkException; 27 import org.chromium.net.impl.CallbackExceptionImpl; 28 29 import java.io.IOException; 30 import java.io.OutputStream; 31 import java.net.HttpRetryException; 32 import java.net.HttpURLConnection; 33 import java.net.URL; 34 35 /** Tests {@code getOutputStream} when {@code setFixedLengthStreamingMode} is enabled. */ 36 @Batch(Batch.UNIT_TESTS) 37 @IgnoreFor( 38 implementations = {CronetImplementation.FALLBACK}, 39 reason = "See crrev.com/c/4590329") 40 @RunWith(AndroidJUnit4.class) 41 public class CronetFixedModeOutputStreamTest { 42 @Rule public final CronetTestRule mTestRule = CronetTestRule.withManualEngineStartup(); 43 44 private HttpURLConnection mConnection; 45 46 private CronetEngine mCronetEngine; 47 48 @Before setUp()49 public void setUp() throws Exception { 50 mTestRule 51 .getTestFramework() 52 .applyEngineBuilderPatch( 53 (builder) -> mTestRule.getTestFramework().enableDiskCache(builder)); 54 mCronetEngine = mTestRule.getTestFramework().startEngine(); 55 assertThat( 56 NativeTestServer.startNativeTestServer( 57 mTestRule.getTestFramework().getContext())) 58 .isTrue(); 59 } 60 61 @After tearDown()62 public void tearDown() throws Exception { 63 if (mConnection != null) { 64 mConnection.disconnect(); 65 } 66 NativeTestServer.shutdownNativeTestServer(); 67 } 68 69 @Test 70 @SmallTest testConnectBeforeWrite()71 public void testConnectBeforeWrite() throws Exception { 72 URL url = new URL(NativeTestServer.getEchoBodyURL()); 73 mConnection = (HttpURLConnection) mCronetEngine.openConnection(url); 74 mConnection.setDoOutput(true); 75 mConnection.setRequestMethod("POST"); 76 mConnection.setFixedLengthStreamingMode(TestUtil.UPLOAD_DATA.length); 77 OutputStream out = mConnection.getOutputStream(); 78 mConnection.connect(); 79 out.write(TestUtil.UPLOAD_DATA); 80 assertThat(mConnection.getResponseCode()).isEqualTo(200); 81 assertThat(mConnection.getResponseMessage()).isEqualTo("OK"); 82 assertThat(TestUtil.getResponseAsString(mConnection)) 83 .isEqualTo(TestUtil.UPLOAD_DATA_STRING); 84 } 85 86 @Test 87 @SmallTest 88 // Regression test for crbug.com/687600. testZeroLengthWriteWithNoResponseBody()89 public void testZeroLengthWriteWithNoResponseBody() throws Exception { 90 URL url = new URL(NativeTestServer.getEchoBodyURL()); 91 mConnection = (HttpURLConnection) mCronetEngine.openConnection(url); 92 mConnection.setDoOutput(true); 93 mConnection.setRequestMethod("POST"); 94 mConnection.setFixedLengthStreamingMode(0); 95 OutputStream out = mConnection.getOutputStream(); 96 out.write(new byte[] {}); 97 assertThat(mConnection.getResponseCode()).isEqualTo(200); 98 assertThat(mConnection.getResponseMessage()).isEqualTo("OK"); 99 } 100 101 @Test 102 @SmallTest testWriteAfterRequestFailed()103 public void testWriteAfterRequestFailed() throws Exception { 104 URL url = new URL(NativeTestServer.getEchoBodyURL()); 105 mConnection = (HttpURLConnection) mCronetEngine.openConnection(url); 106 mConnection.setDoOutput(true); 107 mConnection.setRequestMethod("POST"); 108 byte[] largeData = TestUtil.getLargeData(); 109 mConnection.setFixedLengthStreamingMode(largeData.length); 110 OutputStream out = mConnection.getOutputStream(); 111 out.write(largeData, 0, 10); 112 NativeTestServer.shutdownNativeTestServer(); 113 IOException e = 114 assertThrows( 115 IOException.class, () -> out.write(largeData, 10, largeData.length - 10)); 116 // TODO(crbug.com/1495774): Consider whether we should be checking this in the first place. 117 if (mTestRule.implementationUnderTest().equals(CronetImplementation.STATICALLY_LINKED)) { 118 assertThat(e).isInstanceOf(NetworkException.class); 119 NetworkException networkException = (NetworkException) e; 120 assertThat(networkException.getErrorCode()) 121 .isEqualTo(NetworkException.ERROR_CONNECTION_REFUSED); 122 } 123 } 124 125 @Test 126 @SmallTest testGetResponseAfterWriteFailed()127 public void testGetResponseAfterWriteFailed() throws Exception { 128 URL url = new URL(NativeTestServer.getEchoBodyURL()); 129 NativeTestServer.shutdownNativeTestServer(); 130 mConnection = (HttpURLConnection) mCronetEngine.openConnection(url); 131 mConnection.setDoOutput(true); 132 mConnection.setRequestMethod("POST"); 133 // Set content-length as 1 byte, so Cronet will upload once that 1 byte 134 // is passed to it. 135 mConnection.setFixedLengthStreamingMode(1); 136 OutputStream out = mConnection.getOutputStream(); 137 // Forces OutputStream implementation to flush. crbug.com/653072 138 IOException e = assertThrows(IOException.class, () -> out.write(1)); 139 // TODO(crbug.com/1495774): Consider whether we should be checking this in the first place. 140 if (mTestRule.implementationUnderTest().equals(CronetImplementation.STATICALLY_LINKED)) { 141 assertThat(e).isInstanceOf(NetworkException.class); 142 NetworkException networkException = (NetworkException) e; 143 assertThat(networkException.getErrorCode()) 144 .isEqualTo(NetworkException.ERROR_CONNECTION_REFUSED); 145 } 146 // Make sure NetworkException is reported again when trying to read response 147 // from the mConnection. 148 e = assertThrows(IOException.class, mConnection::getResponseCode); 149 // TODO(crbug.com/1495774): Consider whether we should be checking this in the first place. 150 if (mTestRule.implementationUnderTest().equals(CronetImplementation.STATICALLY_LINKED)) { 151 assertThat(e).isInstanceOf(NetworkException.class); 152 NetworkException networkException = (NetworkException) e; 153 assertThat(networkException.getErrorCode()) 154 .isEqualTo(NetworkException.ERROR_CONNECTION_REFUSED); 155 } 156 // Restarting server to run the test for a second time. 157 assertThat( 158 NativeTestServer.startNativeTestServer( 159 mTestRule.getTestFramework().getContext())) 160 .isTrue(); 161 } 162 163 @Test 164 @SmallTest testFixedLengthStreamingModeZeroContentLength()165 public void testFixedLengthStreamingModeZeroContentLength() throws Exception { 166 // Check content length is set. 167 URL echoLength = new URL(NativeTestServer.getEchoHeaderURL("Content-Length")); 168 mConnection = (HttpURLConnection) echoLength.openConnection(); 169 mConnection.setDoOutput(true); 170 mConnection.setRequestMethod("POST"); 171 mConnection.setFixedLengthStreamingMode(0); 172 assertThat(mConnection.getResponseCode()).isEqualTo(200); 173 assertThat(mConnection.getResponseMessage()).isEqualTo("OK"); 174 assertThat(TestUtil.getResponseAsString(mConnection)).isEqualTo("0"); 175 mConnection.disconnect(); 176 177 // Check body is empty. 178 URL echoBody = new URL(NativeTestServer.getEchoBodyURL()); 179 mConnection = (HttpURLConnection) echoBody.openConnection(); 180 mConnection.setDoOutput(true); 181 mConnection.setRequestMethod("POST"); 182 mConnection.setFixedLengthStreamingMode(0); 183 assertThat(mConnection.getResponseCode()).isEqualTo(200); 184 assertThat(mConnection.getResponseMessage()).isEqualTo("OK"); 185 assertThat(TestUtil.getResponseAsString(mConnection)).isEmpty(); 186 } 187 188 @Test 189 @SmallTest testWriteLessThanContentLength()190 public void testWriteLessThanContentLength() throws Exception { 191 URL url = new URL(NativeTestServer.getEchoBodyURL()); 192 mConnection = (HttpURLConnection) mCronetEngine.openConnection(url); 193 mConnection.setDoOutput(true); 194 mConnection.setRequestMethod("POST"); 195 // Set a content length that's 1 byte more. 196 mConnection.setFixedLengthStreamingMode(TestUtil.UPLOAD_DATA.length + 1); 197 OutputStream out = mConnection.getOutputStream(); 198 out.write(TestUtil.UPLOAD_DATA); 199 assertThrows(IOException.class, mConnection::getResponseCode); 200 } 201 202 @Test 203 @SmallTest testWriteMoreThanContentLength()204 public void testWriteMoreThanContentLength() throws Exception { 205 URL url = new URL(NativeTestServer.getEchoBodyURL()); 206 mConnection = (HttpURLConnection) mCronetEngine.openConnection(url); 207 mConnection.setDoOutput(true); 208 mConnection.setRequestMethod("POST"); 209 // Set a content length that's 1 byte short. 210 mConnection.setFixedLengthStreamingMode(TestUtil.UPLOAD_DATA.length - 1); 211 OutputStream out = mConnection.getOutputStream(); 212 IOException e = assertThrows(IOException.class, () -> out.write(TestUtil.UPLOAD_DATA)); 213 assertThat(e) 214 .hasMessageThat() 215 .isEqualTo( 216 "expected " 217 + (TestUtil.UPLOAD_DATA.length - 1) 218 + " bytes but received " 219 + TestUtil.UPLOAD_DATA.length); 220 } 221 222 @Test 223 @SmallTest testWriteMoreThanContentLengthWriteOneByte()224 public void testWriteMoreThanContentLengthWriteOneByte() throws Exception { 225 URL url = new URL(NativeTestServer.getEchoBodyURL()); 226 mConnection = (HttpURLConnection) mCronetEngine.openConnection(url); 227 mConnection.setDoOutput(true); 228 mConnection.setRequestMethod("POST"); 229 // Set a content length that's 1 byte short. 230 mConnection.setFixedLengthStreamingMode(TestUtil.UPLOAD_DATA.length - 1); 231 OutputStream out = mConnection.getOutputStream(); 232 for (int i = 0; i < TestUtil.UPLOAD_DATA.length - 1; i++) { 233 out.write(TestUtil.UPLOAD_DATA[i]); 234 } 235 // Try upload an extra byte. 236 IOException e = 237 assertThrows( 238 IOException.class, 239 () -> out.write(TestUtil.UPLOAD_DATA[TestUtil.UPLOAD_DATA.length - 1])); 240 String expectedVariant = "expected 0 bytes but received 1"; 241 String expectedVariantOnLollipop = 242 "expected " 243 + (TestUtil.UPLOAD_DATA.length - 1) 244 + " bytes but received " 245 + TestUtil.UPLOAD_DATA.length; 246 assertThat(e).hasMessageThat().isAnyOf(expectedVariant, expectedVariantOnLollipop); 247 } 248 249 @Test 250 @SmallTest testFixedLengthStreamingMode()251 public void testFixedLengthStreamingMode() throws Exception { 252 URL url = new URL(NativeTestServer.getEchoBodyURL()); 253 mConnection = (HttpURLConnection) mCronetEngine.openConnection(url); 254 mConnection.setDoOutput(true); 255 mConnection.setRequestMethod("POST"); 256 mConnection.setFixedLengthStreamingMode(TestUtil.UPLOAD_DATA.length); 257 OutputStream out = mConnection.getOutputStream(); 258 out.write(TestUtil.UPLOAD_DATA); 259 assertThat(mConnection.getResponseCode()).isEqualTo(200); 260 assertThat(mConnection.getResponseMessage()).isEqualTo("OK"); 261 assertThat(TestUtil.getResponseAsString(mConnection)) 262 .isEqualTo(TestUtil.UPLOAD_DATA_STRING); 263 } 264 265 @Test 266 @SmallTest testFixedLengthStreamingModeWriteOneByte()267 public void testFixedLengthStreamingModeWriteOneByte() throws Exception { 268 URL url = new URL(NativeTestServer.getEchoBodyURL()); 269 mConnection = (HttpURLConnection) mCronetEngine.openConnection(url); 270 mConnection.setDoOutput(true); 271 mConnection.setRequestMethod("POST"); 272 mConnection.setFixedLengthStreamingMode(TestUtil.UPLOAD_DATA.length); 273 OutputStream out = mConnection.getOutputStream(); 274 for (int i = 0; i < TestUtil.UPLOAD_DATA.length; i++) { 275 // Write one byte at a time. 276 out.write(TestUtil.UPLOAD_DATA[i]); 277 } 278 assertThat(mConnection.getResponseCode()).isEqualTo(200); 279 assertThat(mConnection.getResponseMessage()).isEqualTo("OK"); 280 assertThat(TestUtil.getResponseAsString(mConnection)) 281 .isEqualTo(TestUtil.UPLOAD_DATA_STRING); 282 } 283 284 @Test 285 @SmallTest testFixedLengthStreamingModeLargeData()286 public void testFixedLengthStreamingModeLargeData() throws Exception { 287 URL url = new URL(NativeTestServer.getEchoBodyURL()); 288 mConnection = (HttpURLConnection) mCronetEngine.openConnection(url); 289 mConnection.setDoOutput(true); 290 mConnection.setRequestMethod("POST"); 291 // largeData is 1.8 MB. 292 byte[] largeData = TestUtil.getLargeData(); 293 mConnection.setFixedLengthStreamingMode(largeData.length); 294 OutputStream out = mConnection.getOutputStream(); 295 int totalBytesWritten = 0; 296 // Number of bytes to write each time. It is doubled each time 297 // to make sure that the implementation can handle large writes. 298 int bytesToWrite = 683; 299 while (totalBytesWritten < largeData.length) { 300 if (bytesToWrite > largeData.length - totalBytesWritten) { 301 // Do not write out of bound. 302 bytesToWrite = largeData.length - totalBytesWritten; 303 } 304 out.write(largeData, totalBytesWritten, bytesToWrite); 305 totalBytesWritten += bytesToWrite; 306 // About 5th iteration of this loop, bytesToWrite will be bigger than 16384. 307 bytesToWrite *= 2; 308 } 309 assertThat(mConnection.getResponseCode()).isEqualTo(200); 310 assertThat(mConnection.getResponseMessage()).isEqualTo("OK"); 311 TestUtil.checkLargeData(TestUtil.getResponseAsString(mConnection)); 312 } 313 314 @Test 315 @SmallTest testFixedLengthStreamingModeLargeDataWriteOneByte()316 public void testFixedLengthStreamingModeLargeDataWriteOneByte() throws Exception { 317 URL url = new URL(NativeTestServer.getEchoBodyURL()); 318 mConnection = (HttpURLConnection) mCronetEngine.openConnection(url); 319 mConnection.setDoOutput(true); 320 mConnection.setRequestMethod("POST"); 321 byte[] largeData = TestUtil.getLargeData(); 322 mConnection.setFixedLengthStreamingMode(largeData.length); 323 OutputStream out = mConnection.getOutputStream(); 324 for (int i = 0; i < largeData.length; i++) { 325 // Write one byte at a time. 326 out.write(largeData[i]); 327 } 328 assertThat(mConnection.getResponseCode()).isEqualTo(200); 329 assertThat(mConnection.getResponseMessage()).isEqualTo("OK"); 330 TestUtil.checkLargeData(TestUtil.getResponseAsString(mConnection)); 331 } 332 333 @Test 334 @SmallTest testJavaBufferSizeLargerThanNativeBufferSize()335 public void testJavaBufferSizeLargerThanNativeBufferSize() throws Exception { 336 // Set an internal buffer of size larger than the buffer size used 337 // in network stack internally. 338 // Normal stream uses 16384, QUIC uses 14520, and SPDY uses 16384. 339 // Try two different buffer lengths. 17384 will make the last write 340 // smaller than the native buffer length; 18384 will make the last write 341 // bigger than the native buffer length 342 // (largeData.length % 17384 = 9448, largeData.length % 18384 = 16752). 343 int[] bufferLengths = new int[] {17384, 18384}; 344 for (int length : bufferLengths) { 345 CronetFixedModeOutputStream.setDefaultBufferLengthForTesting(length); 346 // Run the following three tests with this custom buffer size. 347 testFixedLengthStreamingModeLargeDataWriteOneByte(); 348 testFixedLengthStreamingModeLargeData(); 349 testOneMassiveWrite(); 350 } 351 } 352 353 @Test 354 @SmallTest testOneMassiveWrite()355 public void testOneMassiveWrite() throws Exception { 356 URL url = new URL(NativeTestServer.getEchoBodyURL()); 357 mConnection = (HttpURLConnection) mCronetEngine.openConnection(url); 358 mConnection.setDoOutput(true); 359 mConnection.setRequestMethod("POST"); 360 byte[] largeData = TestUtil.getLargeData(); 361 mConnection.setFixedLengthStreamingMode(largeData.length); 362 OutputStream out = mConnection.getOutputStream(); 363 // Write everything at one go, so the data is larger than the buffer 364 // used in CronetFixedModeOutputStream. 365 out.write(largeData); 366 assertThat(mConnection.getResponseCode()).isEqualTo(200); 367 assertThat(mConnection.getResponseMessage()).isEqualTo("OK"); 368 TestUtil.checkLargeData(TestUtil.getResponseAsString(mConnection)); 369 } 370 371 @Test 372 @SmallTest testRewindWithCronet()373 public void testRewindWithCronet() throws Exception { 374 // Post preserving redirect should fail. 375 URL url = new URL(NativeTestServer.getRedirectToEchoBody()); 376 mConnection = (HttpURLConnection) mCronetEngine.openConnection(url); 377 mConnection.setDoOutput(true); 378 mConnection.setRequestMethod("POST"); 379 mConnection.setFixedLengthStreamingMode(TestUtil.UPLOAD_DATA.length); 380 381 OutputStream out = mConnection.getOutputStream(); 382 out.write(TestUtil.UPLOAD_DATA); 383 IOException e = assertThrows(IOException.class, mConnection::getResponseCode); 384 // TODO(crbug.com/1495774): Consider whether we should be checking this in the first place. 385 if (mTestRule.implementationUnderTest().equals(CronetImplementation.STATICALLY_LINKED)) { 386 assertThat(e).isInstanceOf(CallbackExceptionImpl.class); 387 } 388 389 assertThat(e).hasMessageThat().isEqualTo("Exception received from UploadDataProvider"); 390 assertThat(e).hasCauseThat().isInstanceOf(HttpRetryException.class); 391 assertThat(e).hasCauseThat().hasMessageThat().isEqualTo("Cannot retry streamed Http body"); 392 } 393 } 394