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 handlers may return an HTTP 204 response #455

Closed
Klavionik opened this issue Dec 25, 2023 · 6 comments · Fixed by #457
Closed

WebSocket handlers may return an HTTP 204 response #455

Klavionik opened this issue Dec 25, 2023 · 6 comments · Fixed by #457

Comments

@Klavionik
Copy link
Contributor

Another one about WebSockets. :)

This PR #446, while fixing important things, introduced an interesting bug I didn't expect to happen.

Apparently, if a route handler has a return type of None (like a typical WebSocket handler does), its result will be replaced with an HTTP 204 response. This is really handy for HTTP routes, but now that we have this line in Application._handle_websocket() it may produce an error:

try:
    return await route.handler(ws)  # This line returns HTTP 204.
except Exception as exc:
    ...

The error goes like ASGI callable should return None, but returned '<Response 204>'.

I see two ways to fix this:

  1. The implicit one. Just do this and no one will be hurt.
try:
    await route.handler(ws)
    return
except Exception as exc:
    ...
  1. The explicit one. Don't normalize handler's result to 204 if it's a WebSocket handler. Even more, ignore any result from a WebSocket handler and always return None. I guess it could be done in _get_async_wrapper_for_output like that:
def _get_async_wrapper_for_output(
    method: Callable[[Request], Any],
) -> Callable[[Request], Awaitable[Response]]:
    @wraps(method)
    async def handler(request: Request) -> Response | None:
        if isinstance(request, WebSocket):
            await method(request)
            return
        return ensure_response(await method(request))

    return handler

I like the explicit one better. I could create a PR to fix this, but I'm not sure if this solution is indeed right (though it fixes the error message and passes the tests).

@RobertoPrevato
Copy link
Member

I see, I also like the first option and I don't think it is an implicit behavior. I wouldn't accept option 2.

Should we maybe differentiate like we did when handling exceptions, if the WebSocket was accepted or not, and support controlling the response code before the handshake?

@Klavionik
Copy link
Contributor Author

I'm ok with the first solution, I'm just slightly worried about creating a 204 reponse and then ignoring it, all in vain. :)

Should we maybe differentiate like we did when handling exceptions, if the WebSocket was accepted or not, and support controlling the response code before the handshake?

Maybe, but for what purpose? As long as there is no exception raised inside the handler, we don't care... I guess?

@RobertoPrevato
Copy link
Member

RobertoPrevato commented Dec 27, 2023

For example, let's say we have an API that handles topics and comments about topics. We have a WebSocket endpoint where a user can subscribe to get new comments about a specific topic through WS (/chat/{topic_id}), passing a Topic ID in the route. The user of BlackSheep might want to return in the HTTP handshake request, a 404 Not Found response if someone tries to subscribe to a non-existent topic, or Bad Request is the topic ID is not valid. The user might return instances of Response and use the methods in blacksheep.server.responses without raising exceptions. 🤔

The MDN mentions: If any header is not understood or has an incorrect value, the server should send a 400 ("Bad Request") response and immediately close the socket. As usual, it may also give the reason why the handshake failed in the HTTP response body, but the message may never be displayed (browsers do not display it).

Even though the message may never be displayed, since we offer the ability to accept the WebSocket explicitly we should also support controlling what happens before accepting it.

@Klavionik
Copy link
Contributor Author

Klavionik commented Dec 27, 2023

As we discussed earlier, there is no way in the ASGI world to send a user-defined HTTP response during the handshake phase.

If we would try to send an HTTP response before accepting a WS connection, using Uvicorn we would see an error: ASGI callable returned without sending handshake. Using Daphne, there's no error message in the log, but a WSDISCONNECT message after 5 seconds.

It seems like raising an exception to make the ASGI server reject the connection with 403 status is the only option we have as application developers.

@RobertoPrevato
Copy link
Member

RobertoPrevato commented Dec 27, 2023

Sorry for my confusion. About the other problem you mentioned, creating a 204 response in vain, the most efficient way is to not normalize the web socket request handler in the same way like the others (so an if check is done only once at application start and not once per every web request). I can look into that.

RobertoPrevato added a commit that referenced this issue Dec 31, 2023
The ASGI callable is not supposed to return an object. Fix #455 and update the code to be more explicit about the fact that certain methods must return None (return in __call__ is used to interrupt code execution and not to return objects)
RobertoPrevato added a commit that referenced this issue Dec 31, 2023
- Removes the unused "active" property defined in the `Response` class.
- Fixes #455, reported by @Klavionik. This error caused the WebSocket handler to erroneously return an instance of BlackSheep response to the underlying ASGI server, causing an error to be logged in the console.
- Update type annotations in the `Application` class code to be more explicit about the fact that certain methods must return None (return in __call__ is used to interrupt code execution and not to return objects).
- Improve the normalization logic to not normalize the output for WebSocket requests (as ASGI servers do not allow controlling the response for WebSocket handshake requests).
- Improve the normalization logic to not normalize request handlers that are valid as they are, as asynchronous functions with a single parameter annotated as Request or WebSocket.
@RobertoPrevato
Copy link
Member

Hi @Klavionik
Thank You again for reporting this issue, I fixed it at 8a26922 - I also improved the normalization logic to not normalize the output of WebSocket handlers, as I mentioned in my previous message.

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 a pull request may close this issue.

2 participants