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