Skip to content

Add support for HTTP/1.1 protocol upgrades #8849

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

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

gesellix
Copy link

@gesellix gesellix commented Jun 10, 2025

Based on the discussion in #5874

See RFC 7230 section 6.7 and RFC 9110 section 15.2.2.

The first two commits are not related and can be submitted as separate pull request.

This is only a draft to continue the discussion started in #5874 and not ready for an actual merge, yet.

// https://learn.microsoft.com/en-us/windows/win32/ipc/named-pipe-client
// docker run --rm -it --name attach alpine:edge top -b
@Test
fun upgradeConnection() {
Copy link
Author

Choose a reason for hiding this comment

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

I'm not happy with this one, maybe you have better ideas how to test the connection upgrade?

@gesellix
Copy link
Author

This is now rebased on the current master to use the new okio.Socket.

if (response.socketHandler != null) {
response.socketHandler.handle(
object : okio.Socket {
override val source: BufferedSource
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not clear who can access this as a BufferedSource for an anonymous object impl. Wouldn't clients still have to Buffer again to be sure?

Copy link
Author

Choose a reason for hiding this comment

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

I guess I don't understand your question. Are you implying that this should be changed to a non-buffered Source?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, are you relying on this being buffered here? Or also when used it's applied

Copy link
Author

Choose a reason for hiding this comment

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

I don't rely on this being buffered, so I commited a cleanup 👍

@@ -82,7 +82,7 @@ class MockSocketHandler : SocketHandler {
@JvmOverloads
fun sendResponse(
s: String,
responseSent: CountDownLatch = CountDownLatch(0),
responseSent: CountDownLatch = CountDownLatch(1),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this fixing an unrelated bug?

Copy link
Author

Choose a reason for hiding this comment

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

According to the Javadoc, a CountDownLatch with a zero value doesn't have any effect. I didn't experience a bug.

This is unrelated to the current pull request, though. I referred to this one as one of the two commits which are both unrelated and can easily be moved to a dedicated pull request.

get() = socket.sink().buffer()

override fun cancel() {
socket.sink().flush()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Copy link
Author

Choose a reason for hiding this comment

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

You're right, I didn't check the api thoroughly. I'm going to remove this one.

Copy link
Author

Choose a reason for hiding this comment

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

This has been removed in RealConnection as well as in the MockWebServer.

Copy link
Author

Choose a reason for hiding this comment

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

... is it correct and sufficient to cancel the Exchange?

/**
* Non-null if this response is a successful upgrade ...
*/
@get:JvmName("socket") val socket: Socket?,
Copy link
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
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
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.

if (code == HTTP_SWITCHING_PROTOCOLS &&
"upgrade".equals(response.request.header("Connection"), ignoreCase = true) &&
"upgrade".equals(response.header("Connection"), ignoreCase = true) &&
"tcp".equals(response.request.header("Upgrade"), ignoreCase = true) &&
Copy link
Collaborator

Choose a reason for hiding this comment

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

The PR title feels like it's about all possible protocols, but in practice seems tied to solving TCP specifically for docker?

Is there a spec for this TCP upgrade?

Copy link
Author

Choose a reason for hiding this comment

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

You're right, I wasn't aware of the more general possibilities for a connection upgrade, and the case for Websockets hasn't changed with my pull request.

My current aim is in fact to add support for TCP upgrades, because my use case is driven by the Docker engine api.
There's no spec for the TCP upgrade per se, at least not known to me.

Given the issue at #5874 I would suggest to start with support for tcp and websocket. Other protocols might be added in the future.

@yschimke
Copy link
Collaborator

yschimke commented Jun 16, 2025

I'm struggling to find any implementations of the TCP upgrade in popular clients like Curl, Netty, Requests etc.

Can you link to any?

And wondering whether this really needs a more thorough implementation of the spec, which allows for negotation.

Example from Netty https://github.com/netty/netty/blob/42d83b8081ab3d018f5d7611973e5d361cdd28d1/codec-http/src/main/java/io/netty/handler/codec/http/HttpClientUpgradeHandler.java#L40

@gesellix
Copy link
Author

I'm struggling to find any implementations of the TCP upgrade in popular clients like Curl, Netty, Requests etc.

Can you link to any?

And wondering whether this really needs a more thorough implementation of the spec, which allows for negotation.

Example from Netty https://github.com/netty/netty/blob/42d83b8081ab3d018f5d7611973e5d361cdd28d1/codec-http/src/main/java/io/netty/handler/codec/http/HttpClientUpgradeHandler.java#L40

There is a use case with the Apache Http Client, so not exactly an example in a client: https://github.com/docker-java/docker-java/blob/20f08311d1613d8819c4548f34046b0557a43117/docker-java-transport-httpclient5/src/main/java/com/github/dockerjava/httpclient5/HijackingHttpRequestExecutor.java
The official Docker Clients (docker-client, docker-py) implement the TCP upgrade described at https://docs.docker.com/reference/api/engine/version/v1.50/#tag/Container/operation/ContainerAttach

Looking at https://datatracker.ietf.org/doc/html/rfc9110#field.upgrade there's at least the Expect header mentioned. Is this what you mean by negotiation?

As far as I understand the Netty implementation, it handles the connection upgrade more or less by "passing through" and only checking response headers for the requested protocol. I didn't dig deeper into how exactly it would allow the client to handle the upgraded connection.
Maybe OkHttp could select some of well-known or "popular" protocols (e.g. Websocket) for specific upgrade handling and fall back to the more generic Socket for unknown or non-officially supported protocols?

@yschimke
Copy link
Collaborator

Thanks, that's useful.

Yeah there are maybe three paths

  1. special case each transport we support (TCP, websocket)
  2. websocket or okio.Socket (TCP + other)
  3. design a proper upgrade API

@yschimke yschimke added this to the 5.x milestone Jun 22, 2025
@gesellix
Copy link
Author

gesellix commented Jul 3, 2025

Given the release of OkHttp 5, and this one probably changing the api, is there still a chance to have some kind of minimal implementation for the use case of OkHttp as client for a Docker Engine and the tcp upgrade? I guess there's no other chance of using OkHttp without changing its api? I'm not afraid of maintaining some kind of extension or module so that you wouldn't have to officially support such a feature, but I have no idea how I could implement such a module without maintaining a full OkHttp fork by myself.
A minimal support for specific use cases of the HTTP/1.1 protcol upgrade would be really great. I'm such a big fan of OkHttp :)

@yschimke
Copy link
Collaborator

yschimke commented Jul 3, 2025

@gesellix I think it's worth supporting, but I'd like some other opinions on it also.

I guess what is complicated here is the PR is a partial implementation that covers a non-standardised but valid case. While the standardized cases aren't supported.

@gesellix
Copy link
Author

gesellix commented Jul 3, 2025

So, how much of a standard use case (which ones?) would be required to put some weight or clarity into it? Maybe this is not the right timing to push such a feature and I assume that you don't have other users asking for anything with a connection upgrade (on top of existing implementations for Websocket and HTTP/2).

Do you have an indication which standard use cases make sense? I could try to prepare the required changes either with this pull request or in a separate one. I might only need some guidance on the interfaces and testing if that would work for you.

@yschimke
Copy link
Collaborator

yschimke commented Jul 3, 2025

If we have a clean mechanism, and this is currently the only supported protocol, but we can add more. That is probably ok.

But if the public API of things like Response change in a way that has a bunch of assumptions that only hold for this TCP docker case, it would be a mistake.

@swankjesse if you have opinions, feel free to weigh in.

@swankjesse
Copy link
Collaborator

I’d like to ship this.

I’d like to review this PR and the RFCs to figure out exactly what’s best for our next step and I intend to do so.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants