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

Send code 1012 on shutdown for websockets #1816

Merged
merged 5 commits into from Jan 6, 2023
Merged

Conversation

Kludex
Copy link
Sponsor Member

@Kludex Kludex commented Dec 26, 2022

Changes

  • Send 500 or 1012 to the client on server shutdown, instead of the previous 1006.
  • Improve test suite to check what the client receives, instead of only the ASGI application.
  • Increase coverage from 98.5% to 98.8%.

@@ -90,7 +90,6 @@ def __init__(
self.connect_sent = False
self.lost_connection_before_handshake = False
self.accepted_subprotocol: Optional[Subprotocol] = None
self.transfer_data_task: asyncio.Task = None # type: ignore[assignment]
Copy link
Sponsor Member Author

@Kludex Kludex Dec 26, 2022

Choose a reason for hiding this comment

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

This is necessary to call fail_connection(), because internally there's a check like if hasattr(self, "transfer_data_task"), which should succeed.

Comment on lines -235 to +242
if self.conn.connection is None:
output = self.conn.send(wsproto.events.RejectConnection(status_code=500))
else:
msg = h11.Response(
status_code=500, headers=headers, reason="Internal Server Error"
output = self.conn.send(
wsproto.events.RejectConnection(
status_code=500, headers=headers, has_body=True
)
output = self.conn.send(msg)
msg = h11.Data(data=b"Internal Server Error")
output += self.conn.send(msg)
msg = h11.EndOfMessage()
output += self.conn.send(msg)
)
output += self.conn.send(
wsproto.events.RejectData(data=b"Internal Server Error")
)
Copy link
Sponsor Member Author

Choose a reason for hiding this comment

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

Don't be afraid here. I'll explain what's happening.

  1. We were sending RejectConnection(status_code=500) without a body, but on websockets implementation we were sending the "Internal Server Error" body on the analogous behavior. The RejectData matches the behavior.
  2. We are removing the conditional because it's never reached, and the reason for it is that we only call send_500_response is we didn't complete the handshake - which makes a lot of sense to remove it.

Hope it's clear.

Copy link
Sponsor Member Author

Choose a reason for hiding this comment

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

Am I missing EndOfMessage? I need to check this.

Copy link
Sponsor Member Author

Choose a reason for hiding this comment

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

No, I'm not missing it. wsproto internally adds the h11.EndOfMessage. 🙏

Comment on lines +147 to +150
if self.handshake_completed_event.is_set():
self.fail_connection(1012)
else:
self.send_500_response()
Copy link
Sponsor Member Author

Choose a reason for hiding this comment

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

This is the change that motivated this PR.

We were only sending 1006 to the client, even when the handshake was not completed.

Copy link

Choose a reason for hiding this comment

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

Why self.fail_connection(1012) instead of just calling self.close(1012)?

I've been look at websockets server.close() and procotol.close() to see why uvicorn isn't calling `close(), but haven't gotten terribly far yet.

Copy link
Sponsor Member Author

@Kludex Kludex Dec 26, 2022

Choose a reason for hiding this comment

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

Because close() is async.

Copy link

Choose a reason for hiding this comment

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

Ahh, missed the lack of async here.

Looking deeper at the difference, it seems like the primary difference is fail_connection will proactively cancel the data transfer task but close will simply send the close frame and then wait.

Is there any consequence to that distinction here? My very, very basic testing seems to suggest with uvicorn tasks are given a chance to shutdown cleanly, but I'm still testing.

Copy link
Sponsor Member Author

Choose a reason for hiding this comment

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

Yeah, this code still waits for it to finish. 👍

Comment on lines +749 to +751
assert response is not None
assert response.status_code == 500, response.text
assert response.text == "Internal Server Error"
Copy link
Sponsor Member Author

Choose a reason for hiding this comment

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

We are now testing what the client receives, instead of only the ASGI application.

@Kludex Kludex requested review from tomchristie and a team December 26, 2022 20:59
task.cancel()
assert response is not None
assert response.status_code == 500, response.text
assert response.text == "Internal Server Error"
assert disconnect_message == {"type": "websocket.disconnect", "code": 1006}
Copy link
Sponsor Member Author

@Kludex Kludex Dec 26, 2022

Choose a reason for hiding this comment

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

Hmm... On a second thought... This doesn't make sense, does it? Why are we even sending a websocket.disconnect when the handshake was not even completed? 🤔

I think this was on purpose because then the application could receive a websocket event, but thinking about it again, does it make sense?

Copy link
Sponsor Member Author

Choose a reason for hiding this comment

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

I need to check the issues/PRs about this decision. It shouldn't be a blocker for this PR tho.

Copy link
Sponsor Member Author

Choose a reason for hiding this comment

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

According to django/asgiref#364 it's fine.

@jalaziz
Copy link

jalaziz commented Dec 26, 2022

Can confirm that wscat, Chrome, and Firefox all show the correct close codes. I also checked that the client receives the correct close code if an app sends {"type": "websocket.close", "code": code} and that works too.

Just trying to figure out what's going on with the websockets client.
Figured it out, websockets is fine, it's just the __main__.py script doesn't show the correct code when the server initiates the disconnect.
I'm an idiot and was accidentally using an testing with an older version of websockets. The latest version (10.4) shows the correct codes.

Copy link
Member

@tomchristie tomchristie left a comment

Choose a reason for hiding this comment

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

So PRs like this do highlight to me how awkward it is to try to follow the flow when using protocols rather than streams.

Anyways.

@Kludex
Copy link
Sponsor Member Author

Kludex commented Jan 6, 2023

So PRs like this do highlight to me how awkward it is to try to follow the flow when using protocols rather than streams.

Anyways.

I agree. 🤷

@Kludex Kludex merged commit 23b9f05 into master Jan 6, 2023
@Kludex Kludex deleted the send-1012-on-shutdown branch January 6, 2023 12:07
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.

Incorrect websocket close code on server shutdown
3 participants