Skip to content
This repository was archived by the owner on Apr 22, 2023. It is now read-only.

More useful web socket upgrade API #4813

Closed
isaacs opened this issue Feb 20, 2013 · 26 comments
Closed

More useful web socket upgrade API #4813

isaacs opened this issue Feb 20, 2013 · 26 comments

Comments

@isaacs
Copy link

isaacs commented Feb 20, 2013

It's inconvenient right now that we automatically upgrade all upgradeable requests and do server.emit('upgrade', req, req.socket, bodyHead);

  • It presumes that you want to upgrade all upgradeable connections, rather than only some of them.
  • There is no way to do res.statusCode = 403; res.end('Sorry, no websockets for you!'), since a response object is never created.
  • It breaks the assumptions of systems like Restify and Express that want to have a stack of functions doing stuff like auth and such.
  • It emits on the server for servers, but on the request for clients. Kind of odd.
  • It prevents the server.emit('request') or request.emit('response').

And basically, this is all just to prevent the overhead of creating a response object, which is cheap anyway.

However, changing this interface too dramatically is going to break some stuff pretty badly.

Idea:

  • Server:
    1. Change the 'request' event signature to add the socket as well: server.emit('request', req, res, socket)
    2. Do not automatically upgrade requests. Instead, add request.upgradeable = true flag.
    3. If the user calls req.upgrade() then unhook all the HTTP machinery, and now it's an upgraded connection.
    4. For backwards compatibility, when the user calls req.upgrade(), emit the 'upgrade' event on the server with the req, socket, and the bit of the body that has already been read in.
  • Client:
    1. Change the 'response' event signature to add the socket as well: request.emit('response', response, socket)
    2. Do not automatically upgrade responses. Instead, add response.upgradeable = true flag.
    3. If the user calls response.upgrade(), then unhook all the HTTP machinery, and now it's an upgraded connection.
    4. For backwards compatibility, when the user calls response.upgrade(), emit the 'upgrade' event on the request with the response, socket, and the bit of the body that has already been read in.

This should be changed in v0.12. The bodyHead change might not be an issue, if we refactor the parser/etc to take advantage of streams2 more effectively, so that we don't automatically pull everything out of the socket readable stream object.

/cc @substack @jclulow

@isaacs
Copy link
Author

isaacs commented Feb 20, 2013

For simplicity, req.upgrade() and res.upgrade() could both be aliased to the same code path. But I consider that an implementation detail.

@ghost
Copy link

ghost commented Feb 20, 2013

This change would really simplify things and would greatly reduce the surface area that websocket libraries would need to touch such that you could just pass them a (req, res) pair instead of passing them the whole server instance so they can patch in their custom upgrade listeners.

req.upgrade() is especially interesting because it would make it very simple to write transparent http proxies with the core http lib. If userland had access to the entire buffer of the header content (bodyHead) for all requests and not just upgraded requests, transparent http proxies could be written in ~3 lines. Perhaps bodyHead could be stored directly on the req?

@jclulow
Copy link

jclulow commented Feb 21, 2013

This seems good to me, except that:

  • I think it makes sense to return the socket from response.upgrade(), rather than add it to the event.
  • We ought to throw if you attempt response.upgrade() but have already tried to use response in the regular -- that is, non-upgraded -- fashion.
  • Doing anything active (e.g. writing headers, status code, etc) to the response after an upgrade should also throw.

I'm thinking primarily of the case of debugging a long chain of Restify handlers, where we may (unintentionally) try to use a connection for both kinds of operation.

@isaacs
Copy link
Author

isaacs commented Feb 21, 2013

I think it makes sense to return the socket from response.upgrade(), rather than add it to the event.

Meh. Sure, why not. We could also already have stashed it on req.socket and res.connection and everywhere else you can imagine.

And yes, upgrading after writing/etc, or writing/etc after upgrading, shall emit an error event.

@isaacs
Copy link
Author

isaacs commented Feb 21, 2013

Also cc: @einaros @guille

@adammw
Copy link

adammw commented Mar 19, 2013

@isaacs: Please don't take this as a complaint or anything negative, I'm genuinely interested but am having difficulty understanding how your idea works.

Do not automatically upgrade requests. Instead, add request.upgradeable = true flag.

By 'automatically upgrade', does this mean just treating HTTP requests with the Upgrade header as regular responses with a special flag instead of a separate event, or something else?

If the former, Legacy code will not know the difference between regular requests and upgrades, and may accidentally attempt to use the response in the regular, non-upgraded, fashion, which then throws and potentially causes instability in server code from any 'Upgrade' header on a request.

For backwards compatibility, when the user calls req.upgrade(), emit the 'upgrade' event on the server with the req, socket, and the bit of the body that has already been read in.

I'm a bit confused as to how this will work either, I was under the impression that under your proposal, res.upgrade() would write the HTTP 101 Switching Protocols, but then if the 'upgrade' event is emitted then, the socket will already have that written (differing from 0.10.x and earlier).

Or does the 101 status code need to be manually sent with res.writeHead before res.upgrade is called?

@creationix
Copy link

I very much like this proposal and second @jclulow's suggestion of not adding a third arg to the request handler. This would make it easier to continue using connect style code that already has it's own third parameter. I don't really care if it's returned from upgrade or available as a property on req.

@rauchg
Copy link

rauchg commented Mar 19, 2013

This is a great change. We underwent a lot of pain in engine.io to make sure we don't claim upgrade requests that other handlers might be expecting (since we only want to handle the ones that have a certain /path), while retaining the semantics of "destroying the socket if the upgrade goes unhandled".

More information of our approach here:
socketio/engine.io#144

@shtylman who worked with me on that patch might have other comments about this API change.

@rauchg
Copy link

rauchg commented Mar 19, 2013

I also don't see the need for the socket in the event signature considering that pretty much everyone is used to req.socket, and like @creationix said it can be confusing with the popular req, res, next signature.

@rauchg
Copy link

rauchg commented Mar 19, 2013

For backwards compatibility, when the user calls req.upgrade(), emit the 'upgrade' event on the server with the req, socket, and the bit of the body that has already been read in.

How would this be BC? Maybe if .upgrade() is not called in the same tick as the request event we emit it?

@stephank
Copy link

This is pretty much what I tried to accomplish with #3036, but I suppose I never worded it quite right. (Thanks for referencing, @adammw.)

Biggest issue with #3036 was, if I understood correctly, backwards compatibility in the API, which is a bit difficult to accomplish in the API proposal I made.

But now regarding this issue...

As @adammw already pointed out, there's the subtlety of what exactly .upgrade() does. I think there are actually three code paths, we want here?

  • Use a response object to write a non-upgrade response,
  • Use a response object to write an upgrade with headers, then claim the socket, and
  • Immediately claim the socket, then manually write headers (for legacy code).

I'm also wondering if upgradeable should just be a flag isUpgrade. Why have a different API at all depending on the kind of request? Make .upgrade() work on all requests, avoid unnecessary complexity, add adequate docs, and trust the user to make a sensible decision here.

@jclulow
Copy link

jclulow commented Mar 19, 2013

On 19 March 2013 04:50, Adam Malcontenti-Wilson
notifications@github.com wrote:

Or does the 101 status code need to be manually sent with res.writeHead
before res.upgrade is called?

It would be good to be able to use the regular Response machinery to
write the "HTTP 101 Switching Protocols", including some arbitrary set
of response headers like "Sec-WebSocket-Protocol", prior to performing
the upgrade() itself.

Perhaps you would call res.upgrade() in place of res.end(), which
would flush out the Response headers and then give you the socket.

@defunctzombie
Copy link

I am +1 on not upgrading by default and allowing the user to handle that. I am -1 on the third parameter (as noted by some others). I think req.socket is fine (unless someone can show a case where it is inflexible?)

As for responding with headers. I don't see why .upgrade() can't take an opt argument to set headers? Open to hearing more clever upgrade solutions :)

@adammw
Copy link

adammw commented Mar 20, 2013

As for responding with headers. I don't see why .upgrade() can't take an
opt argument to set headers? Open to hearing more clever upgrade
solutions :)

I personally don't think headers should be an optional argument with
upgrade, but actually customisable with the standard res.statusCode or
res.writeHead function, buffered then written when upgrade or end is
called, similar to standard http where headers are buffered until the first
write.

With regards to backwards compat. I don't really see any good automatic
way, the simplest way I see that happening is with an explicit opt-in to
the new behaviour, turning into an explicit opt-out eventually when the old
style becomes deprecated.

Unlike a previous upgrade-ex proposal/module, reusing the request (while it
makes sense) will cause a lot of backwards compatibility issues compared to
just adding a new event (which can easily be checked for listeners ala old
vs new style streams in streams2).

But anyhow, please discuss any other bc ideas I may have missed.

Adam Malcontenti-Wilson

@creationix
Copy link

How bad of a backwards compat issue would be simply not auto-upgrading be? Yes all websockets servers would need to add a line to their code somewhere, but once they added that one line things would work. I don't want to have to opt-in to the preferred behavior for all future APIs just because it was inconvenient once to add a line when making a major version upgrade once.

@defunctzombie
Copy link

I am against breaking upgrades. The upgrade path should let people use the
exact same code on new node and then update the code after updating the
platform. When you try to change too much at once I find it doesn't end so
well. It makes the deployment situation harder as well.
On Mar 20, 2013 7:07 PM, "Tim Caswell" notifications@github.com wrote:

How bad of a backwards compat issue would be simply not auto-upgrading be?
Yes all websockets servers would need to add a line to their code
somewhere, but once they added that one line things would work. I don't
want to have to opt-in to the preferred behavior for all future APIs just
because it was inconvenient once to add a line when making a major version
upgrade once.


Reply to this email directly or view it on GitHubhttps://github.com//issues/4813#issuecomment-15209652
.

@adammw
Copy link

adammw commented Mar 21, 2013

I didn't mean to hijack the discussion like this, perhaps this question should have been posted on the mailing list instead.

I still don't understand what "auto-upgrading" means in this context - could someone please explain it?

@jclulow
Copy link

jclulow commented Mar 21, 2013

On 20 March 2013 18:14, Adam Malcontenti-Wilson
notifications@github.com wrote:

I still don't understand what "auto-upgrading" means in this context - could someone please explain it?

At present, in response to a "HTTP 101 Switching Protocols", the HTTP
Server will immediately detach the Request/Response machinery from the
underlying socket and emit an 'upgrade' event (instead of 'request')
including the Socket and any already-read data. If there are no
listeners for this event, it simply closes the connection.

This behaviour assumes that you wish to Upgrade, which is clearly not
strictly true, and automatically completes the necessary steps to
relinquish the connection. That's what we mean, here, by
"auto-upgrading."

@adammw
Copy link

adammw commented Jun 25, 2013

@isaacs - Is this still on the roadmap for v0.12? do you have a sample implementation of your proposal?
I'm looking forward to using middleware with the connect/express on websockets, but it looks like they can't really make much progress forward until upstream (node) decides which API / direction to take in implementing this feature.
I'm wanting to chip in and play with / write some experimental code branches, is there much I can do to move this topic forward?

@indutny
Copy link
Member

indutny commented Feb 28, 2014

@tjfontaine, @isaacs ping? Would you please mind sharing your final decisions on the API here?

@Fishrock123
Copy link

Is this still on the schedule for 0.12?

@creationix
Copy link

For what's it worth. I have pure-js HTTP and Websocket codecs that perform pretty well. With these you can define whatever API you want and not be dependent on what node core provides. You only need TCP or TLS streams to transform.

The down side is you lose compat with existing code that depends on the node APIs. I'm currently using my codec to spawn HTTP servers in chrome apps and create smart-protocol node servers that serve HTTP and two other custom protocols on the same port by guessing the protocol via the first byte.

The HTTP codec is open at https://github.com/creationix/http-codec.
The websocket part was written for some consulting work, but I'm pretty confident they will allow me to open source it. If not, it's pretty trivial for me to implement it again when I get time if there is interest.

@tjfontaine tjfontaine modified the milestones: v0.13, v0.12 May 8, 2014
@tjfontaine
Copy link

Unfortunately this is not on target for 0.12, I have moved it to 0.13 for now.

@stuartpb
Copy link

Any plans for this to be integrated into node (io.js) next.0? I'm not seeing any pingbacks to this issue apart from nodejs/node#550 which seems to addressing a completely different feature.

@stuartpb
Copy link

Also, to make sure I'm understanding this correctly, this would make it possible to mount WebSocket-based libraries like ws and engine.io as middleware (rather than having to pass them the http server)?

@ThisIsMissEm
Copy link

From the brief amount I've read on this, it sounds good…

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

No branches or pull requests