Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions mockwebserver/src/main/kotlin/mockwebserver3/MockWebServer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ import okio.BufferedSource
import okio.ByteString
import okio.Sink
import okio.Timeout
import okio.asOkioSocket
import okio.buffer
import okio.sink
import okio.source
Expand Down Expand Up @@ -604,9 +605,16 @@ public class MockWebServer : Closeable {
"Upgrade".equals(request.headers["Connection"], ignoreCase = true) &&
"websocket".equals(request.headers["Upgrade"], ignoreCase = true)
val responseWantsWebSockets = response.webSocketListener != null
val requestWantsTcp =
"Upgrade".equals(request.headers["Connection"], ignoreCase = true) &&
"tcp".equals(request.headers["Upgrade"], ignoreCase = true)
val responseWantsStream = response.socketHandler != null
if (requestWantsWebSockets && responseWantsWebSockets) {
handleWebSocketUpgrade(socket, source, sink, request, response)
reuseSocket = false
} else if (requestWantsTcp && responseWantsStream) {
writeHttpResponse(socket, sink, response)
reuseSocket = false
} else {
writeHttpResponse(socket, sink, response)
}
Expand Down Expand Up @@ -865,6 +873,11 @@ public class MockWebServer : Closeable {

writeHeaders(sink, response.headers)

if (response.socketHandler != null) {
response.socketHandler.handle(socket.asOkioSocket())
return
}

val body = response.body ?: return
socket.sleepWhileOpen(response.bodyDelayNanos)
val responseBodySink =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ class MockSocketHandler : SocketHandler {
@JvmOverloads
fun sendResponse(
s: String,
responseSent: CountDownLatch = CountDownLatch(0),
responseSent: CountDownLatch = CountDownLatch(1),
) = apply {
actions += { stream ->
stream.sink.writeUtf8(s)
Expand Down
2 changes: 2 additions & 0 deletions okhttp/api/android/okhttp.api
Original file line number Diff line number Diff line change
Expand Up @@ -1120,6 +1120,7 @@ public final class okhttp3/Response : java/io/Closeable {
public final fun receivedResponseAtMillis ()J
public final fun request ()Lokhttp3/Request;
public final fun sentRequestAtMillis ()J
public final fun socket ()Lokio/Socket;
public fun toString ()Ljava/lang/String;
public final fun trailers ()Lokhttp3/Headers;
}
Expand All @@ -1142,6 +1143,7 @@ public class okhttp3/Response$Builder {
public fun removeHeader (Ljava/lang/String;)Lokhttp3/Response$Builder;
public fun request (Lokhttp3/Request;)Lokhttp3/Response$Builder;
public fun sentRequestAtMillis (J)Lokhttp3/Response$Builder;
public fun socket (Lokio/Socket;)Lokhttp3/Response$Builder;
public fun trailers (Lokhttp3/TrailersSource;)Lokhttp3/Response$Builder;
}

Expand Down
2 changes: 2 additions & 0 deletions okhttp/api/jvm/okhttp.api
Original file line number Diff line number Diff line change
Expand Up @@ -1120,6 +1120,7 @@ public final class okhttp3/Response : java/io/Closeable {
public final fun receivedResponseAtMillis ()J
public final fun request ()Lokhttp3/Request;
public final fun sentRequestAtMillis ()J
public final fun socket ()Lokio/Socket;
public fun toString ()Ljava/lang/String;
public final fun trailers ()Lokhttp3/Headers;
}
Expand All @@ -1142,6 +1143,7 @@ public class okhttp3/Response$Builder {
public fun removeHeader (Ljava/lang/String;)Lokhttp3/Response$Builder;
public fun request (Lokhttp3/Request;)Lokhttp3/Response$Builder;
public fun sentRequestAtMillis (J)Lokhttp3/Response$Builder;
public fun socket (Lokio/Socket;)Lokhttp3/Response$Builder;
public fun trailers (Lokhttp3/TrailersSource;)Lokhttp3/Response$Builder;
}

Expand Down
13 changes: 13 additions & 0 deletions okhttp/src/commonJvmAndroid/kotlin/okhttp3/Response.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import okhttp3.internal.http.HTTP_PERM_REDIRECT
import okhttp3.internal.http.HTTP_TEMP_REDIRECT
import okhttp3.internal.http.parseChallenges
import okio.Buffer
import okio.Socket

/**
* An HTTP response. Instances of this class are not immutable: the response body is a one-shot
Expand Down Expand Up @@ -77,6 +78,10 @@ class Response internal constructor(
* all instances of [ResponseBody].
*/
@get:JvmName("body") val body: ResponseBody,
/**
* Non-null if this response is a successful upgrade ...
*/
@get:JvmName("socket") val socket: Socket?,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit unclear why this is enough to consider upgrades handled. Is this only for TCP? or do you think that all protocols would be handled as a Socket?

https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Upgrade#upgrade_header_with_multiple_protocols

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I must admit that I only considered TCP upgrades and ignored other options. I suppose that the socket will only be necessary (and non-null) for upgrades which are not supported through another implementation (e.g. HTTP2). Returning the plain Socket might be considered as a generic way of supporting any protocol. That said: I'm unsure if this would make sense for OkHttp in general or if you prefer to keep supported protocols explicitly stated anywhere.
So, would it be enough to improve the Javadoc to be more specific?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Jesse should weigh in. I don't think this is a simple feature to add especially given your TCP case is a poorly specced docker variant of a more general feature.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is that on a protocol upgrade, OkHttp should hand off the okio.Socket to the application layer, and it can do whatever new protocol it wants on top. Our responsibility for the upgrade ends and handing off the socket.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Websockets might be the only exception?

/**
* Returns the raw response received from the network. Will be null if this response didn't use
* the network, such as when the response is fully cached. The body of the returned response
Expand Down Expand Up @@ -353,6 +358,7 @@ class Response internal constructor(
internal var handshake: Handshake? = null
internal var headers: Headers.Builder
internal var body: ResponseBody = ResponseBody.EMPTY
internal var socket: Socket? = null
internal var networkResponse: Response? = null
internal var cacheResponse: Response? = null
internal var priorResponse: Response? = null
Expand All @@ -373,6 +379,7 @@ class Response internal constructor(
this.handshake = response.handshake
this.headers = response.headers.newBuilder()
this.body = response.body
this.socket = response.socket
this.networkResponse = response.networkResponse
this.cacheResponse = response.cacheResponse
this.priorResponse = response.priorResponse
Expand Down Expand Up @@ -446,6 +453,11 @@ class Response internal constructor(
this.body = body
}

open fun socket(socket: Socket) =
apply {
this.socket = socket
}

open fun networkResponse(networkResponse: Response?) =
apply {
checkSupportResponse("networkResponse", networkResponse)
Expand Down Expand Up @@ -503,6 +515,7 @@ class Response internal constructor(
handshake,
headers.build(),
body,
socket,
networkResponse,
cacheResponse,
priorResponse,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import okio.Buffer
import okio.ForwardingSink
import okio.ForwardingSource
import okio.Sink
import okio.Socket
import okio.Source
import okio.buffer

Expand Down Expand Up @@ -157,6 +158,11 @@ class Exchange(
)
}

fun newHttpSocket(): Socket {
call.timeoutEarlyExit()
return (codec.carrier as RealConnection).newHttpSocket()
}

fun noNewExchangesOnConnection() {
codec.carrier.noNewExchanges()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import okio.BufferedSource
import okio.Sink
import okio.Source
import okio.Timeout
import okio.asOkioSocket
import okio.buffer

/**
Expand Down Expand Up @@ -218,7 +219,7 @@ class RealConnection internal constructor(
// 2. The routes must share an IP address.
if (routes == null || !routeMatchesAny(routes)) return false

// 3. This connection's server certificate's must cover the new host.
// 3. This connection's server certificates must cover the new host.
if (address.hostnameVerifier !== OkHostnameVerifier) return false
if (!supportsUrl(address.url)) return false

Expand Down Expand Up @@ -312,6 +313,12 @@ class RealConnection internal constructor(
}
}

internal fun newHttpSocket(): okio.Socket {
socket.soTimeout = 0
noNewExchanges()
return socket.asOkioSocket()
}

override fun route(): Route = route

override fun cancel() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,21 @@ class CallServerInterceptor(
exchange.responseHeadersEnd(response)

response =
if (forWebSocket && code == 101) {
// Connection is upgrading, but we need to ensure interceptors see a non-null response body.
response.stripBody()
if (code == HTTP_SWITCHING_PROTOCOLS &&
"upgrade".equals(response.request.header("Connection"), ignoreCase = true) &&
"upgrade".equals(response.header("Connection"), ignoreCase = true)
) {
if (forWebSocket) {
// Connection is upgrading, but we need to ensure interceptors see a non-null response body.
response.stripBody()
} else {
// Generic case to return the raw socket.
response
.stripBody()
.newBuilder()
.socket(exchange.newHttpSocket())
.build()
}
} else {
val responseBody = exchange.openResponseBody(response)
response
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ fun Response.promisesBody(): Boolean {
}

val responseCode = code
if ((responseCode < HTTP_CONTINUE || responseCode >= 200) &&
if ((responseCode < HTTP_CONTINUE || responseCode == HTTP_SWITCHING_PROTOCOLS || responseCode >= 200) &&

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t think I agree that a protocol change is a body? This is used to drive stuff like the gzip interceptor, which seems inappropriate here?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The protocol change itself doesn't promise a body, but would depend on the actual protocl after a successful upgrade.
I think this change was some kind of workaround to run into the "newUnknownLengthSource(response.request.url)" case at https://github.com/gesellix/okhttp/blob/0984bc3fc5d41c4814b953194d10e5ee77361f7d/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http1/Http1ExchangeCodec.kt#L145
Maybe there's a better way?

responseCode != HTTP_NO_CONTENT &&
responseCode != HTTP_NOT_MODIFIED
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ package okhttp3.internal.http
/** `100 Continue` (HTTP/1.1 - RFC 7231) */
const val HTTP_CONTINUE = 100

/** `101 Switching Protocols` (HTTP/1.1 - RFC 9110) */
const val HTTP_SWITCHING_PROTOCOLS = 101

/** `102 Processing` (WebDAV - RFC 2518) */
const val HTTP_PROCESSING = 102

Expand Down
Loading
Loading