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

Websocket shouldUpgrade() fail causes empty reply from server #3155

Open
challfry opened this issue Mar 3, 2024 · 4 comments
Open

Websocket shouldUpgrade() fail causes empty reply from server #3155

challfry opened this issue Mar 3, 2024 · 4 comments
Labels
bug Something isn't working

Comments

@challfry
Copy link

challfry commented Mar 3, 2024

Describe the bug

When the shouldUpgrade() closure in a webSocket route returns nil or throws an error, Vapor returns a zero length response.

Expected: When an attempt to open a WebSocket fails the server should send a 400 or 500 level HTTP response.

To Reproduce

  1. Add the following 3 lines to "routes.swift" in a template Vapor project created with vapor new hello -n.
	app.webSocket("successsocket", shouldUpgrade: { req in return [:] }, onUpgrade: { req, socket in })
	app.webSocket("failsocket", shouldUpgrade: { req in return nil }, onUpgrade: { req, socket in })
	app.webSocket("throwsocket", shouldUpgrade: { req in throw Abort(.unauthorized) }, onUpgrade: { req, socket in })
  1. run curl, this opens a WebSocket connection:
curl --include \
     --header "Connection: Upgrade" \
     --header "Upgrade: websocket" \
     --header "Host: 127.0.0.1" \
     --header "Origin: http://127.0.0.1:8080" \
     --header "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" \
     --header "Sec-WebSocket-Version: 13" \
     http://127.0.0.1:8080/successsocket
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Sec-WebSocket-Accept: qGEgH3En71di5rrssAZTmtRTyFk=
Connection: upgrade
date: Sat, 02 Mar 2024 23:18:56 GMT
^C
  1. run curl again, this time using the route that returns nil from shouldUpgrade():
curl --include \
     --header "Connection: Upgrade" \
     --header "Upgrade: websocket" \
     --header "Host: 127.0.0.1" \
     --header "Origin: http://127.0.0.1:8080" \
     --header "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" \
     --header "Sec-WebSocket-Version: 13" \
     http://127.0.0.1:8080/failsocket
curl: (52) Empty reply from server
  1. And, with the route that throws a 401 Unauthorized error:
curl --include \
     --header "Connection: Upgrade" \
     --header "Upgrade: websocket" \
     --header "Host: 127.0.0.1" \
     --header "Origin: http://127.0.0.1:8080" \
     --header "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" \
     --header "Sec-WebSocket-Version: 13" \
     http://127.0.0.1:8080/throwsocket
curl: (52) Empty reply from server

Expected behavior

I believe HTTP servers are always supposed to return at least a status line in responses. A zero-length response leaves clients no way to communicate why the WebSocket request failed.

Environment

  • Vapor Framework version: "4.92.4"
  • Vapor Toolbox version:
  • OS version: 14.3.1 (23D60)

Additional context

These curl commands use http:// instead of ws:// to make it easy to show the issue. Note that the success case opens the socket and gives the proper 101 response.

@challfry challfry added the bug Something isn't working label Mar 3, 2024
@gwynne
Copy link
Member

gwynne commented Mar 3, 2024

Without the -f flag, curl is not reporting the actual status code from the server in your example - try using the -v flag to see the full protocol exchange, including the actual response code for the empty replies.

@challfry
Copy link
Author

challfry commented Mar 3, 2024

Adding the -f and -v flags:

curl -f -v --include \
     --header "Connection: Upgrade" \
     --header "Upgrade: websocket" \
     --header "Host: 127.0.0.1" \
     --header "Origin: http://127.0.0.1:8080" \
     --header "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" \
     --header "Sec-WebSocket-Version: 13" \
     http://127.0.0.1:8080/throwsocket
*   Trying 127.0.0.1:8080...
* Connected to 127.0.0.1 (127.0.0.1) port 8080
> GET /throwsocket HTTP/1.1
> Host: 127.0.0.1
> User-Agent: curl/8.4.0
> Accept: */*
> Connection: Upgrade
> Upgrade: websocket
> Origin: http://127.0.0.1:8080
> Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==
> Sec-WebSocket-Version: 13
> 
* Empty reply from server
* Closing connection
curl: (52) Empty reply from server

challfry pushed a commit to jocosocial/swiftarr that referenced this issue Mar 3, 2024
Due to vapor/vapor#3155, the server-mediated path won't return the correct error message when attempting to call someone who hasn't favorited you (although the call does fail correctly and doesn't ring the callee's phone). Works correctly in the direct-call path.

Also, freaked out when I saw 5 "Sending incoming phonecall to callee." messages in a row. Turns out it's because I had 5 devices running logged in as that user. Still, this logging is better.
challfry added a commit to jocosocial/swiftarr that referenced this issue Mar 3, 2024
Due to vapor/vapor#3155, the server-mediated path won't return the correct error message when attempting to call someone who hasn't favorited you (although the call does fail correctly and doesn't ring the callee's phone). Works correctly in the direct-call path.

Also, freaked out when I saw 5 "Sending incoming phonecall to callee." messages in a row. Turns out it's because I had 5 devices running logged in as that user. Still, this logging is better.

Co-authored-by: Chall Fry <chall@challfry.com>
@gwynne
Copy link
Member

gwynne commented Mar 3, 2024

Ah ha, literally no response at all, not even an HTTP status line. My apologies for my partial misread of your original post! That is definitely incorrect behavior 😕.

@challfry
Copy link
Author

challfry commented Mar 4, 2024

More information that's likely related. When calling a ws route, the middleware futures (or async completions; the part that runs on the way out of the middleware stack) all fire before the "shouldUpgrade" closure is called. Oddly, as the middlewares unwind they all receive a Response object with a 101 status. This means that e.g. ErrorMiddleware never sees an error response when shouldUpgrade() returns an error, because ErrorMiddleware exits before shouldUpgrade gets to run.

Later, after shouldUpgrade throws an error or returns nil the future chain enters the .failure state, the Response object with the 101 status disappears, and eventually NIO closes the channel. I wish I could understand NIO better to figure out which part was where things go wrong, but alas.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants