websockets icon indicating copy to clipboard operation
websockets copied to clipboard

Accessing HTTP connection response body for debugging

Open DrPyser opened this issue 5 years ago • 7 comments

Hi!

I'm trying to debug failing connection attempts, probably an authentication issue with our API gateway. The gateway server responds with a 400, refusing the connection. The websockets debug logs show the response status and headers, but not the response body, which in this case should contain an error message.

Is there a way to have the library output the response body as well?

I would have tried using a proxy like https://docs.mitmproxy.org to monitor the http exchange, but the library doesn't support connecting through such a proxy.

Thanks!

DrPyser avatar Jul 08 '20 18:07 DrPyser

Probably, the fastest solution will be to patch the library around here:

https://github.com/aaugustin/websockets/blob/017a072705408d3df945e333e5edd93e0aa8c706/src/websockets/client.py#L101

Add something along the lines of await self.reader.read(...) and see if you get something.

Definitely something worth improving, at least for simple cases (e.g. when there's a Content-Length header).

aaugustin avatar Jul 10 '20 12:07 aaugustin

Thanks!

DrPyser avatar Jul 10 '20 18:07 DrPyser

#676 (WIP) parses HTTP response bodies in the easy cases (no Transfer-Encoding) which is a good step towards getting this done.

aaugustin avatar Jul 26 '20 11:07 aaugustin

If anyone is interested I'm patching it in the way below. Just call patch_websockets_lib_in_order_to_see_unsuccessful_websocket_connection_response_body

from typing import Tuple

import websockets
import websockets.client as websockets_client_for_patching
from websockets.http import Headers


class InvalidResponse(Exception):
    def __init__(self, status_code: int, body: str) -> None:
        self.status_code = status_code
        self.body = body

    def __str__(self) -> str:
        return f"Code {self.status_code}, Body {self.body}"


async def read_http_response_patched(self) -> Tuple[int, Headers]:
    status_code: int
    body: str
    try:
        status_code, reason, headers = await websockets.http.read_response(self.reader)
    except Exception as exc:
        raise websockets.InvalidMessage("did not receive a valid HTTP response") from exc

    if status_code != 101:
        body = await self.reader.read()
        body = body.decode()
        body = body.replace('\n', "").replace('\r', "")
        raise InvalidResponse(status_code, body)

    websockets.client.logger.debug("%s < HTTP/1.1 %d %s", self.side, status_code, reason)
    websockets.client.logger.debug("%s < %r", self.side, headers)

    self.response_headers = headers

    return status_code, self.response_headers


def patch_websockets_lib_in_order_to_see_unsuccessful_websocket_connection_response_body():
    websockets_client_for_patching.WebSocketClientProtocol.read_http_response = read_http_response_patched

nosalan avatar Aug 26 '20 08:08 nosalan

#676 (WIP) parses HTTP response bodies in the easy cases (no Transfer-Encoding) which is a good step towards getting this done.

Out of curiosity, what is the reasoning for not supporting chunked Transfer-Encoding? I notice you reference section 3.3.3 of the HTTP RFC in the code, but I didn't see anything in the RFC that would prevent you from processing the body of the response (as long as the final encoding is chunked). Maybe something like this...

if 100 <= status_code < 200 or status_code == 204 or status_code == 304:
          body = None
else:
    if "Transfer-Encoding" in headers:
        if headers["Transfer-Encoding"] == "chunked":
            body = b""
            while True:
                # The first line contains the chunk size
                chunksize = yield from parse_line(read_line)
                n = int(chunksize, 16)
                if n > 0:
                    body += yield from read_exact(n + 2)
                else:
                    # Chunksize is 0, consume the last line
                    data = yield from parse_line(read_line)
                    assert not data
                    break
        else:
            raise NotImplementedError(
                f"'{headers["Transfer-Encoding"]}' transfer codings aren't supported"
            )
    else:
        # Do content length stuff...

newvicx avatar Apr 20 '23 17:04 newvicx

The reason is that I'm maintaining a WebSocket library, not a HTTP library.

Deciding not to implement a HTTP library is purely a decision of where I want to spend my free time :-)

aaugustin avatar Apr 21 '23 09:04 aaugustin

It will always be possible to move the line "just a bit".

At some point I have to draw the line. I believe that the place where it's currently drawn covers reasonable use cases of websockets as a WebSocket client.

If you come across a use case where support for Transfer-Encoding would be needed, I will assess if I deem that use case reasonable to support :-)

aaugustin avatar Apr 21 '23 09:04 aaugustin