Skip to content

Commit 7b95ce8

Browse files
authored
Add write timeout for post/put requests (#485)
* Introduce API to set writeTimeout and use it in the NetHttp request * Surface the writeTimeout to HttpRequest * remove debug statements * Fix warnings * Fix syntax * DI for testing * Make field constant field final
1 parent d7f18a3 commit 7b95ce8

File tree

5 files changed

+205
-2
lines changed

5 files changed

+205
-2
lines changed

google-http-client/src/main/java/com/google/api/client/http/HttpRequest.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,11 @@ static String executeAndGetValueOfSomeCustomHeader(HttpRequest request) {
158158
*/
159159
private int readTimeout = 20 * 1000;
160160

161+
/**
162+
* Timeout in milliseconds to set POST/PUT data or {@code 0} for an infinite timeout.
163+
*/
164+
private int writeTimeout = 0;
165+
161166
/** HTTP unsuccessful (non-2XX) response handler or {@code null} for none. */
162167
private HttpUnsuccessfulResponseHandler unsuccessfulResponseHandler;
163168

@@ -493,6 +498,30 @@ public HttpRequest setReadTimeout(int readTimeout) {
493498
return this;
494499
}
495500

501+
/**
502+
* Returns the timeout in milliseconds to send POST/PUT data or {@code 0} for an infinite timeout.
503+
*
504+
* <p>
505+
* By default it is 0 (infinite).
506+
* </p>
507+
*
508+
* @since 1.26
509+
*/
510+
public int getWriteTimeout() {
511+
return writeTimeout;
512+
}
513+
514+
/**
515+
* Sets the timeout in milliseconds to send POST/PUT data or {@code 0} for an infinite timeout.
516+
*
517+
* @since 1.26
518+
*/
519+
public HttpRequest setWriteTimeout(int writeTimeout) {
520+
Preconditions.checkArgument(writeTimeout >= 0);
521+
this.writeTimeout = writeTimeout;
522+
return this;
523+
}
524+
496525
/**
497526
* Returns the HTTP request headers.
498527
*
@@ -977,6 +1006,7 @@ public HttpResponse execute() throws IOException {
9771006

9781007
// execute
9791008
lowLevelHttpRequest.setTimeout(connectTimeout, readTimeout);
1009+
lowLevelHttpRequest.setWriteTimeout(writeTimeout);
9801010
try {
9811011
LowLevelHttpResponse lowLevelHttpResponse = lowLevelHttpRequest.execute();
9821012
// Flag used to indicate if an exception is thrown before the response is constructed.

google-http-client/src/main/java/com/google/api/client/http/LowLevelHttpRequest.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,21 @@ public final StreamingContent getStreamingContent() {
159159
public void setTimeout(int connectTimeout, int readTimeout) throws IOException {
160160
}
161161

162+
/**
163+
* Sets the write timeout for POST/PUT requests.
164+
*
165+
* <p>
166+
* Default implementation does nothing, but subclasses should normally override.
167+
* </p>
168+
*
169+
* @param writeTimeout timeout in milliseconds to establish a connection or {@code 0} for an
170+
* infinite timeout
171+
* @throws IOException I/O exception
172+
* @since 1.26
173+
*/
174+
public void setWriteTimeout(int writeTimeout) throws IOException {
175+
}
176+
162177
/** Executes the request and returns a low-level HTTP response object. */
163178
public abstract LowLevelHttpResponse execute() throws IOException;
164179
}

google-http-client/src/main/java/com/google/api/client/http/javanet/NetHttpRequest.java

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,34 @@
1818
import com.google.api.client.http.LowLevelHttpResponse;
1919
import com.google.api.client.util.Preconditions;
2020

21+
import com.google.api.client.util.StreamingContent;
22+
import com.google.common.annotations.VisibleForTesting;
2123
import java.io.IOException;
2224
import java.io.OutputStream;
2325
import java.net.HttpURLConnection;
26+
import java.util.concurrent.Callable;
27+
import java.util.concurrent.ExecutionException;
28+
import java.util.concurrent.ExecutorService;
29+
import java.util.concurrent.Executors;
30+
import java.util.concurrent.Future;
31+
import java.util.concurrent.FutureTask;
32+
import java.util.concurrent.TimeUnit;
33+
import java.util.concurrent.TimeoutException;
2434

2535
/**
2636
* @author Yaniv Inbar
2737
*/
2838
final class NetHttpRequest extends LowLevelHttpRequest {
2939

3040
private final HttpURLConnection connection;
41+
private int writeTimeout;
3142

3243
/**
3344
* @param connection HTTP URL connection
3445
*/
3546
NetHttpRequest(HttpURLConnection connection) {
3647
this.connection = connection;
48+
this.writeTimeout = 0;
3749
connection.setInstanceFollowRedirects(false);
3850
}
3951

@@ -48,8 +60,32 @@ public void setTimeout(int connectTimeout, int readTimeout) {
4860
connection.setConnectTimeout(connectTimeout);
4961
}
5062

63+
@Override
64+
public void setWriteTimeout(int writeTimeout) throws IOException {
65+
this.writeTimeout = writeTimeout;
66+
}
67+
68+
interface OutputWriter {
69+
void write(OutputStream outputStream, StreamingContent content) throws IOException;
70+
}
71+
72+
static class DefaultOutputWriter implements OutputWriter {
73+
@Override
74+
public void write(OutputStream outputStream, final StreamingContent content)
75+
throws IOException {
76+
content.writeTo(outputStream);
77+
}
78+
}
79+
80+
private static final OutputWriter DEFAULT_CONNECTION_WRITER = new DefaultOutputWriter();
81+
5182
@Override
5283
public LowLevelHttpResponse execute() throws IOException {
84+
return execute(DEFAULT_CONNECTION_WRITER);
85+
}
86+
87+
@VisibleForTesting
88+
LowLevelHttpResponse execute(final OutputWriter outputWriter) throws IOException {
5389
HttpURLConnection connection = this.connection;
5490
// write content
5591
if (getStreamingContent() != null) {
@@ -74,10 +110,12 @@ public LowLevelHttpResponse execute() throws IOException {
74110
} else {
75111
connection.setChunkedStreamingMode(0);
76112
}
77-
OutputStream out = connection.getOutputStream();
113+
final OutputStream out = connection.getOutputStream();
114+
78115
boolean threw = true;
79116
try {
80-
getStreamingContent().writeTo(out);
117+
writeContentToOutputStream(outputWriter, out);
118+
81119
threw = false;
82120
} finally {
83121
try {
@@ -111,4 +149,38 @@ public LowLevelHttpResponse execute() throws IOException {
111149
}
112150
}
113151
}
152+
153+
private void writeContentToOutputStream(final OutputWriter outputWriter, final OutputStream out)
154+
throws IOException {
155+
if (writeTimeout == 0) {
156+
outputWriter.write(out, getStreamingContent());
157+
} else {
158+
// do it with timeout
159+
final StreamingContent content = getStreamingContent();
160+
final Callable<Boolean> writeContent = new Callable<Boolean>() {
161+
@Override
162+
public Boolean call() throws IOException {
163+
outputWriter.write(out, content);
164+
return Boolean.TRUE;
165+
}
166+
};
167+
168+
final ExecutorService executor = Executors.newSingleThreadExecutor();
169+
final Future<Boolean> future = executor.submit(new FutureTask<Boolean>(writeContent), null);
170+
executor.shutdown();
171+
172+
try {
173+
future.get(writeTimeout, TimeUnit.MILLISECONDS);
174+
} catch (InterruptedException e) {
175+
throw new IOException("Socket write interrupted", e);
176+
} catch (ExecutionException e) {
177+
throw new IOException("Exception in socket write", e);
178+
} catch (TimeoutException e) {
179+
throw new IOException("Socket write timed out", e);
180+
}
181+
if (!executor.isTerminated()) {
182+
executor.shutdown();
183+
}
184+
}
185+
}
114186
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package com.google.api.client.http.javanet;
2+
3+
import com.google.api.client.http.HttpContent;
4+
import com.google.api.client.http.InputStreamContent;
5+
import com.google.api.client.http.javanet.NetHttpRequest.OutputWriter;
6+
import com.google.api.client.testing.http.HttpTesting;
7+
import com.google.api.client.testing.http.javanet.MockHttpURLConnection;
8+
import com.google.api.client.util.StreamingContent;
9+
import java.io.IOException;
10+
import java.io.InputStream;
11+
import java.io.OutputStream;
12+
import java.net.URL;
13+
14+
import static org.junit.Assert.assertEquals;
15+
import static org.junit.Assert.assertTrue;
16+
import static org.junit.Assert.fail;
17+
18+
import java.util.concurrent.TimeoutException;
19+
import org.junit.Test;
20+
21+
public class NetHttpRequestTest {
22+
23+
static class SleepingOutputWriter implements OutputWriter {
24+
private long sleepTimeInMs;
25+
public SleepingOutputWriter(long sleepTimeInMs) {
26+
this.sleepTimeInMs = sleepTimeInMs;
27+
}
28+
@Override
29+
public void write(OutputStream outputStream, StreamingContent content) throws IOException {
30+
try {
31+
Thread.sleep(sleepTimeInMs);
32+
} catch (InterruptedException e) {
33+
throw new IOException("sleep interrupted", e);
34+
}
35+
}
36+
}
37+
38+
@Test
39+
public void testHangingWrite() throws InterruptedException {
40+
Thread thread = new Thread() {
41+
@Override
42+
public void run() {
43+
try {
44+
postWithTimeout(0);
45+
} catch (IOException e) {
46+
// expected to be interrupted
47+
assertEquals(e.getCause().getClass(), InterruptedException.class);
48+
return;
49+
} catch (Exception e) {
50+
fail();
51+
}
52+
fail("should be interrupted before here");
53+
}
54+
};
55+
56+
thread.start();
57+
Thread.sleep(1000);
58+
assertTrue(thread.isAlive());
59+
thread.interrupt();
60+
}
61+
62+
@Test(timeout = 1000)
63+
public void testOutputStreamWriteTimeout() throws Exception {
64+
try {
65+
postWithTimeout(100);
66+
fail("should have timed out");
67+
} catch (IOException e) {
68+
assertEquals(e.getCause().getClass(), TimeoutException.class);
69+
} catch (Exception e) {
70+
fail("Expected an IOException not a " + e.getCause().getClass().getName());
71+
}
72+
}
73+
74+
private static void postWithTimeout(int timeout) throws Exception {
75+
MockHttpURLConnection connection = new MockHttpURLConnection(new URL(HttpTesting.SIMPLE_URL));
76+
connection.setRequestMethod("POST");
77+
NetHttpRequest request = new NetHttpRequest(connection);
78+
InputStream is = NetHttpRequestTest.class.getClassLoader().getResourceAsStream("file.txt");
79+
HttpContent content = new InputStreamContent("text/plain", is);
80+
request.setStreamingContent(content);
81+
request.setWriteTimeout(timeout);
82+
request.execute(new SleepingOutputWriter(5000L));
83+
}
84+
85+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
some sample file

0 commit comments

Comments
 (0)