Skip to content
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

Client Protocol Upgrades #1395

Closed
seanmonstar opened this issue Dec 7, 2017 · 10 comments
Closed

Client Protocol Upgrades #1395

seanmonstar opened this issue Dec 7, 2017 · 10 comments
Labels
A-client Area: client. C-feature Category: feature. This is adding a new feature. E-medium Effort: medium. Some knowledge of how hyper internal works would be useful.

Comments

@seanmonstar
Copy link
Member

seanmonstar commented Dec 7, 2017

(For handling server protocol upgrades, see #1323)

HTTP/1 allows starting a different protocol with an HTTP/1 request, and the desired protocol in an Upgrade header. We should support this.

Edit: removed bad API proposal.

@seanmonstar seanmonstar added A-client Area: client. E-medium Effort: medium. Some knowledge of how hyper internal works would be useful. C-feature Category: feature. This is adding a new feature. labels Dec 7, 2017
@letmutx
Copy link

letmutx commented Dec 7, 2017

This looks great!

The channel based approach we discussed also seems to be equally ergonomic and a looks much simpler to implement without adding another type parameter to virtually all types in proto. Is there any reason you prefer this over that?

@seanmonstar
Copy link
Member Author

Well, shoot. The reason I liked this was for the end user API, but I think it might be impossible to do in 0.11 because:

  • Body is Send + Sync
  • We can't enforce the transport is Send + Sync, so we can't add Box<AsyncRead + AsyncWrite> to the internals of Body.

What exact user API were you thinking with channels?

@letmutx
Copy link

letmutx commented Dec 8, 2017

struct Upgrade<T: AsyncRead + AsyncWrite> {
    io: T,
    response: MessageHead<RawStatus>
    handle: Handle
}

fn main() {
    let client = Client::configure().enable_upgrades().build();
    let upgrade_rx = client.upgrades();
    let upgrade_task = upgrade_rx.then(|upgrade: Upgrade| {
        do_something(upgrade);
        Ok(())
    });
    let handle = client.handle();
    handle.spawn(upgrade_task);
    let req = Request::new(Method::Get, "https://example.domain/".parse()?)
    .with_header(Connection::upgrade())
    .with_header(Upgrade::websocket());
    let req = client.request(req).and_then(|res| {
        handle_req(res);
        Ok(())
    });
    core.run(req).unwrap();
}

But come to think of it, that is a lot of boilerplate for a single request. My use case is kind of like a proxy I would make a stream of requests, so I am biased.

@seanmonstar
Copy link
Member Author

If there is an upgrade, what does res look like? Does a Response get generated and called? What goes in the Upgrade struct?

What I preferred about the initial proposed API is that handling an upgrade was local to the Response, and so obvious. My concern with configuring the Client is that it can happen in different places, and thus it might not be obvious that upgrades are handled...

@letmutx
Copy link

letmutx commented Dec 9, 2017

Does a Response get generated and called?

Yes.

What goes in the Upgrade struct?

Upgrade struct is right at the top of the example.

@seanmonstar
Copy link
Member Author

Upgrade struct is right at the top of the example.

Ah OK, it wasn't in the original message I got in email, didn't notice the edit.


So, a user would receive the Response like normal, and would be able to inspect it, and if it wasn't an upgrade, try to read the body. Regardless, if it is an upgrade, the upgrades receiver would also receive an Upgrade...

I suppose it allows working without a breaking change, but my personal opinion is that it kind of feels far away from the rest of the code. Hm, does that even matter? I suppose that the underlying connection is being used in an upgrade perhaps doesn't matter to the other place that received a Response...

@seanmonstar
Copy link
Member Author

Ok, sorry it's taken so long to get back here, I haven't had much time to focus on this feature, but it's now needed in Conduit, so it's got my attention again.

I've started with the idea that the client could use a lower-level API of running HTTP over a single connection, and there is where upgrades can be handled at first (as well as custom pools and whatever else). I feel that designing an API to work directly with the higher level Client, which manages connections for you, is holding back this feature work, so that's why I've gone this direction. I think it could be possible to eventually design an API that works with the higher level Client, but here's a proposal to get things started: #1449 (comment)

@letmutx
Copy link

letmutx commented Feb 25, 2018

I too tried to take a stab at it a while ago. The problem seems to be with the pool. At the dispatcher level, I could extract the io object out of the Conn to the dispatcher(this was ~2 months ago). But the fact that the pool knows it is only as a KeepAlive was a big hurdle. I had to remove the pool or rewrite it. Eventually I gave up.

@seanmonstar
Copy link
Member Author

A solution for #1449 was just merged to master, that introduces a lower-level connection API, where it is possible to make CONNECT and upgrade requests on it.

The Client uses it internally, but still specifically errors on CONNECT and upgrades since there is no exposed way to handle them correctly.

@seanmonstar
Copy link
Member Author

There's a proposed PR at #1563 to add this feature (and for servers).

seanmonstar added a commit that referenced this issue Jun 14, 2018
…er (#1563)

- Adds `Body::on_upgrade()` that returns an `OnUpgrade` future.
- Adds `hyper::upgrade` module containing types for dealing with
  upgrades.
- Adds `server::conn::Connection::with_upgrades()` method to enable
  these upgrades when using lower-level API (because of a missing
  `Send` bound on the transport generic).
- Client connections are automatically enabled.
- Optimizes request parsing, to make up for extra work to look for
  upgrade requests.
  - Returns a smaller `DecodedLength` type instead of the fatter
    `Decoder`, which should also allow a couple fewer branches.
  - Removes the `Decode::Ignore` wrapper enum, and instead ignoring
    1xx responses is handled directly in the response parsing code.

Ref #1563 

Closes #1395
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-client Area: client. C-feature Category: feature. This is adding a new feature. E-medium Effort: medium. Some knowledge of how hyper internal works would be useful.
Projects
None yet
Development

No branches or pull requests

2 participants