Skip to content

Commit abe5d62

Browse files
dave-r12swankjesse
authored andcommitted
Recover from an already-allocated connection that restricts new streams. (square#3525)
This is an edge case that can occur with HTTP/2. Since multiple requests use the same connection, it's possible for one request to restrict the connection from creating new streams during a follow-up request. square#3521
1 parent 314b40d commit abe5d62

File tree

2 files changed

+110
-13
lines changed

2 files changed

+110
-13
lines changed

okhttp-tests/src/test/java/okhttp3/internal/http2/HttpOverHttp2Test.java

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
import java.util.concurrent.ExecutorService;
3030
import java.util.concurrent.Executors;
3131
import java.util.concurrent.SynchronousQueue;
32-
3332
import javax.net.ssl.HostnameVerifier;
3433
import okhttp3.Cache;
3534
import okhttp3.Call;
@@ -46,6 +45,7 @@
4645
import okhttp3.Request;
4746
import okhttp3.RequestBody;
4847
import okhttp3.Response;
48+
import okhttp3.Route;
4949
import okhttp3.TestUtil;
5050
import okhttp3.internal.DoubleInetAddressDns;
5151
import okhttp3.internal.RecordingOkAuthenticator;
@@ -736,6 +736,74 @@ private void noRecoveryFromErrorWithRetryDisabled(ErrorCode errorCode) throws Ex
736736
}
737737
}
738738

739+
@Test public void recoverFromConnectionNoNewStreamsOnFollowUp() throws InterruptedException {
740+
server.enqueue(new MockResponse()
741+
.setResponseCode(401));
742+
server.enqueue(new MockResponse()
743+
.setSocketPolicy(SocketPolicy.RESET_STREAM_AT_START)
744+
.setHttp2ErrorCode(ErrorCode.CANCEL.httpCode));
745+
server.enqueue(new MockResponse()
746+
.setBody("DEF"));
747+
server.enqueue(new MockResponse()
748+
.setResponseCode(301)
749+
.addHeader("Location", "/foo"));
750+
server.enqueue(new MockResponse()
751+
.setBody("ABC"));
752+
753+
final CountDownLatch latch = new CountDownLatch(1);
754+
final BlockingQueue<String> responses = new SynchronousQueue<>();
755+
okhttp3.Authenticator authenticator = new okhttp3.Authenticator() {
756+
@Override public Request authenticate(Route route, Response response) throws IOException {
757+
responses.offer(response.body().string());
758+
try {
759+
latch.await();
760+
} catch (InterruptedException e) {
761+
throw new AssertionError();
762+
}
763+
return response.request();
764+
}
765+
};
766+
767+
OkHttpClient blockingAuthClient = client.newBuilder()
768+
.authenticator(authenticator)
769+
.build();
770+
771+
Callback callback = new Callback() {
772+
@Override public void onFailure(Call call, IOException e) {
773+
fail();
774+
}
775+
776+
@Override public void onResponse(Call call, Response response) throws IOException {
777+
responses.offer(response.body().string());
778+
}
779+
};
780+
781+
// Make the first request waiting until we get our auth challenge.
782+
Request request = new Request.Builder()
783+
.url(server.url("/"))
784+
.build();
785+
blockingAuthClient.newCall(request).enqueue(callback);
786+
String response1 = responses.take();
787+
assertEquals("", response1);
788+
assertEquals(0, server.takeRequest().getSequenceNumber());
789+
790+
// Now make the second request which will restrict the first HTTP/2 connection from creating new
791+
// streams.
792+
client.newCall(request).enqueue(callback);
793+
String response2 = responses.take();
794+
assertEquals("DEF", response2);
795+
assertEquals(1, server.takeRequest().getSequenceNumber());
796+
assertEquals(0, server.takeRequest().getSequenceNumber());
797+
798+
// Let the first request proceed. It should discard the the held HTTP/2 connection and get a new
799+
// one.
800+
latch.countDown();
801+
String response3 = responses.take();
802+
assertEquals("ABC", response3);
803+
assertEquals(1, server.takeRequest().getSequenceNumber());
804+
assertEquals(2, server.takeRequest().getSequenceNumber());
805+
}
806+
739807
@Test public void nonAsciiResponseHeader() throws Exception {
740808
server.enqueue(new MockResponse()
741809
.addHeaderLenient("Alpha", "α")

okhttp/src/main/java/okhttp3/internal/connection/StreamAllocation.java

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -160,30 +160,44 @@ private RealConnection findConnection(int connectTimeout, int readTimeout, int w
160160
boolean foundPooledConnection = false;
161161
RealConnection result = null;
162162
Route selectedRoute = null;
163+
Connection releasedConnection;
164+
Socket toClose;
163165
synchronized (connectionPool) {
164166
if (released) throw new IllegalStateException("released");
165167
if (codec != null) throw new IllegalStateException("codec != null");
166168
if (canceled) throw new IOException("Canceled");
167169

168-
// Attempt to use an already-allocated connection.
169-
RealConnection allocatedConnection = this.connection;
170-
if (allocatedConnection != null && !allocatedConnection.noNewStreams) {
171-
return allocatedConnection;
170+
// Attempt to use an already-allocated connection. We need to be careful here because our
171+
// already-allocated connection may have been restricted from creating new streams.
172+
releasedConnection = this.connection;
173+
toClose = releaseIfNoNewStreams();
174+
if (this.connection != null) {
175+
// We had an already-allocated connection and it's good.
176+
result = this.connection;
177+
releasedConnection = null;
172178
}
173179

174-
// Attempt to get a connection from the pool.
175-
Internal.instance.get(connectionPool, address, this, null);
176-
if (connection != null) {
177-
foundPooledConnection = true;
178-
result = connection;
179-
} else {
180-
selectedRoute = route;
180+
if (result == null) {
181+
// Attempt to get a connection from the pool.
182+
Internal.instance.get(connectionPool, address, this, null);
183+
if (connection != null) {
184+
foundPooledConnection = true;
185+
result = connection;
186+
} else {
187+
selectedRoute = route;
188+
}
181189
}
182190
}
191+
closeQuietly(toClose);
183192

184-
// If we found a pooled connection, we're done.
193+
if (releasedConnection != null) {
194+
eventListener.connectionReleased(call, releasedConnection);
195+
}
185196
if (foundPooledConnection) {
186197
eventListener.connectionAcquired(call, result);
198+
}
199+
if (result != null) {
200+
// If we found an already-allocated or pooled connection, we're done.
187201
return result;
188202
}
189203

@@ -257,6 +271,21 @@ private RealConnection findConnection(int connectTimeout, int readTimeout, int w
257271
return result;
258272
}
259273

274+
/**
275+
* Releases the currently held connection and returns a socket to close if the held connection
276+
* restricts new streams from being created. With HTTP/2 multiple requests share the same
277+
* connection so it's possible that our connection is restricted from creating new streams during
278+
* a follow-up request.
279+
*/
280+
private Socket releaseIfNoNewStreams() {
281+
assert (Thread.holdsLock(connectionPool));
282+
RealConnection allocatedConnection = this.connection;
283+
if (allocatedConnection != null && allocatedConnection.noNewStreams) {
284+
return deallocate(false, false, true);
285+
}
286+
return null;
287+
}
288+
260289
public void streamFinished(boolean noNewStreams, HttpCodec codec, long bytesRead, IOException e) {
261290
eventListener.responseBodyEnd(call, bytesRead);
262291

0 commit comments

Comments
 (0)