-
Notifications
You must be signed in to change notification settings - Fork 9.3k
Recover from REFUSED_STREAM errors in HTTP/2. #2571
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,17 +34,19 @@ | |
| import okhttp3.OkHttpClient; | ||
| import okhttp3.Protocol; | ||
| import okhttp3.RecordingCookieJar; | ||
| import okhttp3.RecordingHostnameVerifier; | ||
| import okhttp3.Request; | ||
| import okhttp3.RequestBody; | ||
| import okhttp3.Response; | ||
| import okhttp3.internal.DoubleInetAddressDns; | ||
| import okhttp3.internal.RecordingOkAuthenticator; | ||
| import okhttp3.internal.SingleInetAddressDns; | ||
| import okhttp3.internal.SslContextBuilder; | ||
| import okhttp3.internal.Util; | ||
| import okhttp3.mockwebserver.MockResponse; | ||
| import okhttp3.mockwebserver.MockWebServer; | ||
| import okhttp3.mockwebserver.RecordedRequest; | ||
| import okhttp3.mockwebserver.SocketPolicy; | ||
| import okhttp3.RecordingHostnameVerifier; | ||
| import okio.Buffer; | ||
| import okio.BufferedSink; | ||
| import okio.GzipSink; | ||
|
|
@@ -86,6 +88,7 @@ protected HttpOverSpdyTest(Protocol protocol) { | |
| cache = new Cache(tempDir.getRoot(), Integer.MAX_VALUE); | ||
| client = new OkHttpClient.Builder() | ||
| .protocols(Arrays.asList(protocol, Protocol.HTTP_1_1)) | ||
| .dns(new SingleInetAddressDns()) | ||
| .sslSocketFactory(sslContext.getSocketFactory()) | ||
| .hostnameVerifier(hostnameVerifier) | ||
| .build(); | ||
|
|
@@ -582,6 +585,99 @@ protected HttpOverSpdyTest(Protocol protocol) { | |
| assertEquals(0, server.takeRequest().getSequenceNumber()); | ||
| } | ||
|
|
||
| @Test public void recoverFromOneRefusedStreamReusesConnection() throws Exception { | ||
| server.enqueue(new MockResponse() | ||
| .setSocketPolicy(SocketPolicy.RESET_STREAM_AT_START) | ||
| .setHttp2ErrorCode(ErrorCode.REFUSED_STREAM.httpCode)); | ||
| server.enqueue(new MockResponse() | ||
| .setBody("abc")); | ||
|
|
||
| Call call = client.newCall(new Request.Builder() | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What happens if you use the
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You saw something I didn’t see! I turned that on and of course the test started to fail. I fixed it by changing
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, gotcha. I couldn't get those tests to run in intellij, but just got it working by upgrading my JDK version. |
||
| .url(server.url("/")) | ||
| .build()); | ||
| Response response = call.execute(); | ||
| assertEquals("abc", response.body().string()); | ||
|
|
||
| assertEquals(0, server.takeRequest().getSequenceNumber()); // New connection. | ||
| assertEquals(1, server.takeRequest().getSequenceNumber()); // Reused connection. | ||
| } | ||
|
|
||
| @Test public void recoverFromOneInternalErrorRequiresNewConnection() throws Exception { | ||
| server.enqueue(new MockResponse() | ||
| .setSocketPolicy(SocketPolicy.RESET_STREAM_AT_START) | ||
| .setHttp2ErrorCode(ErrorCode.INTERNAL_ERROR.httpCode)); | ||
| server.enqueue(new MockResponse() | ||
| .setBody("abc")); | ||
|
|
||
| client = client.newBuilder() | ||
| .dns(new DoubleInetAddressDns()) | ||
| .build(); | ||
|
|
||
| Call call = client.newCall(new Request.Builder() | ||
| .url(server.url("/")) | ||
| .build()); | ||
| Response response = call.execute(); | ||
| assertEquals("abc", response.body().string()); | ||
|
|
||
| assertEquals(0, server.takeRequest().getSequenceNumber()); // New connection. | ||
| assertEquals(0, server.takeRequest().getSequenceNumber()); // New connection. | ||
| } | ||
|
|
||
| @Test public void recoverFromMultipleRefusedStreamsRequiresNewConnection() throws Exception { | ||
| server.enqueue(new MockResponse() | ||
| .setSocketPolicy(SocketPolicy.RESET_STREAM_AT_START) | ||
| .setHttp2ErrorCode(ErrorCode.REFUSED_STREAM.httpCode)); | ||
| server.enqueue(new MockResponse() | ||
| .setSocketPolicy(SocketPolicy.RESET_STREAM_AT_START) | ||
| .setHttp2ErrorCode(ErrorCode.REFUSED_STREAM.httpCode)); | ||
| server.enqueue(new MockResponse() | ||
| .setBody("abc")); | ||
|
|
||
| client = client.newBuilder() | ||
| .dns(new DoubleInetAddressDns()) | ||
| .build(); | ||
|
|
||
| Call call = client.newCall(new Request.Builder() | ||
| .url(server.url("/")) | ||
| .build()); | ||
| Response response = call.execute(); | ||
| assertEquals("abc", response.body().string()); | ||
|
|
||
| assertEquals(0, server.takeRequest().getSequenceNumber()); // New connection. | ||
| assertEquals(1, server.takeRequest().getSequenceNumber()); // Reused connection. | ||
| assertEquals(0, server.takeRequest().getSequenceNumber()); // New connection. | ||
| } | ||
|
|
||
| @Test public void noRecoveryFromRefusedStreamWithRetryDisabled() throws Exception { | ||
| noRecoveryFromErrorWithRetryDisabled(ErrorCode.REFUSED_STREAM); | ||
| } | ||
|
|
||
| @Test public void noRecoveryFromInternalErrorWithRetryDisabled() throws Exception { | ||
| noRecoveryFromErrorWithRetryDisabled(ErrorCode.INTERNAL_ERROR); | ||
| } | ||
|
|
||
| private void noRecoveryFromErrorWithRetryDisabled(ErrorCode errorCode) throws Exception { | ||
| server.enqueue(new MockResponse() | ||
| .setSocketPolicy(SocketPolicy.RESET_STREAM_AT_START) | ||
| .setHttp2ErrorCode(errorCode.httpCode)); | ||
| server.enqueue(new MockResponse() | ||
| .setBody("abc")); | ||
|
|
||
| client = client.newBuilder() | ||
| .retryOnConnectionFailure(false) | ||
| .build(); | ||
|
|
||
| Call call = client.newCall(new Request.Builder() | ||
| .url(server.url("/")) | ||
| .build()); | ||
| try { | ||
| call.execute(); | ||
| fail(); | ||
| } catch (StreamResetException expected) { | ||
| assertEquals(errorCode, expected.errorCode); | ||
| } | ||
| } | ||
|
|
||
| public Buffer gzip(String bytes) throws IOException { | ||
| Buffer bytesOut = new Buffer(); | ||
| BufferedSink sink = Okio.buffer(new GzipSink(bytesOut)); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| /* | ||
| * Copyright (C) 2016 Square, Inc. | ||
| * | ||
| * 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 okhttp3.internal.framed; | ||
|
|
||
| import java.io.IOException; | ||
|
|
||
| /** Thrown when an HTTP/2 stream is canceled without damage to the socket that carries it. */ | ||
| public final class StreamResetException extends IOException { | ||
| public final ErrorCode errorCode; | ||
|
|
||
| public StreamResetException(ErrorCode errorCode) { | ||
| super("stream was reset: " + errorCode); | ||
| this.errorCode = errorCode; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,11 +17,16 @@ | |
| package okhttp3.internal.http; | ||
|
|
||
| import java.io.IOException; | ||
| import java.io.InterruptedIOException; | ||
| import java.net.ProtocolException; | ||
| import java.net.Proxy; | ||
| import java.net.SocketTimeoutException; | ||
| import java.security.cert.CertificateException; | ||
| import java.util.Date; | ||
| import java.util.List; | ||
| import javax.net.ssl.HostnameVerifier; | ||
| import javax.net.ssl.SSLHandshakeException; | ||
| import javax.net.ssl.SSLPeerUnverifiedException; | ||
| import javax.net.ssl.SSLSocketFactory; | ||
| import okhttp3.Address; | ||
| import okhttp3.CertificatePinner; | ||
|
|
@@ -352,12 +357,22 @@ public Connection getConnection() { | |
| * permanent. Requests with a body can only be recovered if the body is buffered. | ||
| */ | ||
| public HttpEngine recover(IOException e, boolean routeException, Sink requestBodyOut) { | ||
| if (!streamAllocation.recover(e, routeException, requestBodyOut)) { | ||
| return null; | ||
| } | ||
| streamAllocation.streamFailed(e); | ||
|
|
||
| if (!client.retryOnConnectionFailure()) { | ||
| return null; | ||
| return null; // The application layer has forbidden retries. | ||
| } | ||
|
|
||
| if (requestBodyOut != null && !(requestBodyOut instanceof RetryableSink)) { | ||
| return null; // The body on this request cannot be retried. | ||
| } | ||
|
|
||
| if (!isRecoverable(e, routeException)) { | ||
| return null; // This exception is fatal. | ||
| } | ||
|
|
||
| if (!streamAllocation.hasMoreRoutes()) { | ||
| return null; // No more routes to attempt. | ||
| } | ||
|
|
||
| StreamAllocation streamAllocation = close(); | ||
|
|
@@ -371,6 +386,38 @@ public HttpEngine recover(IOException e, boolean routeException) { | |
| return recover(e, routeException, requestBodyOut); | ||
| } | ||
|
|
||
| private boolean isRecoverable(IOException e, boolean routeException) { | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (This code is just moved) |
||
| // If there was a protocol problem, don't recover. | ||
| if (e instanceof ProtocolException) { | ||
| return false; | ||
| } | ||
|
|
||
| // If there was an interruption don't recover, but if there was a timeout connecting to a route | ||
| // we should try the next route (if there is one). | ||
| if (e instanceof InterruptedIOException) { | ||
| return e instanceof SocketTimeoutException && routeException; | ||
| } | ||
|
|
||
| // Look for known client-side or negotiation errors that are unlikely to be fixed by trying | ||
| // again with a different route. | ||
| if (e instanceof SSLHandshakeException) { | ||
| // If the problem was a CertificateException from the X509TrustManager, | ||
| // do not retry. | ||
| if (e.getCause() instanceof CertificateException) { | ||
| return false; | ||
| } | ||
| } | ||
| if (e instanceof SSLPeerUnverifiedException) { | ||
| // e.g. a certificate pinning error. | ||
| return false; | ||
| } | ||
|
|
||
| // An example of one we might want to retry with a different route is a problem connecting to a | ||
| // proxy and would manifest as a standard IOException. Unless it is one we know we should not | ||
| // retry, we return true and try a new route. | ||
| return true; | ||
| } | ||
|
|
||
| private void maybeCache() throws IOException { | ||
| InternalCache responseCache = Internal.instance.internalCache(client); | ||
| if (responseCache == null) return; | ||
|
|
@@ -428,7 +475,7 @@ public StreamAllocation close() { | |
| closeQuietly(userResponse.body()); | ||
| } else { | ||
| // If this engine never achieved a response body, its stream allocation is dead. | ||
| streamAllocation.connectionFailed(null); | ||
| streamAllocation.streamFailed(null); | ||
| } | ||
|
|
||
| return streamAllocation; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"FramedErrorCode"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+Javadoc explaining that this is used only for
SocketPolicy. RESET_STREAM_AT_STARTThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code is defined by HTTP/2. Definitely needs doc. Added.