Python websocket server failing to start

I have a python websocket server that won’t properly start up due to it getting an invalid HTTP request.

The server is being ran with this code:

async with websockets.serve(self.proxy, self.ip, self.port, process_request=health_check):
    await asyncio.Future()

self.ip is set to “0.0.0.0” while self.port is set to 1300

The log outputs the following around once per second:

opening handshake failed
Traceback (most recent call last):
  File "/opt/render/project/src/.venv/lib/python3.13/site-packages/websockets/server.py", line 545, in parse
    request = yield from Request.parse(
              ^^^^^^^^^^^^^^^^^^^^^^^^^
        self.reader.read_line,
        ^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/opt/render/project/src/.venv/lib/python3.13/site-packages/websockets/http11.py", line 151, in parse
    raise ValueError(f"unsupported HTTP method; expected GET; got {d(method)}")
ValueError: unsupported HTTP method; expected GET; got HEAD
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
  File "/opt/render/project/src/.venv/lib/python3.13/site-packages/websockets/asyncio/server.py", line 356, in conn_handler
    await connection.handshake(
    ...<3 lines>...
    )
  File "/opt/render/project/src/.venv/lib/python3.13/site-packages/websockets/asyncio/server.py", line 207, in handshake
    raise self.protocol.handshake_exc
websockets.exceptions.InvalidMessage: did not receive a valid HTTP request

The server runs completely fine locally, but Render seems to mess with it. I wonder if the issue has something to do with the process_request=health_check argument as it’s the only thing that should involve HTTP. Attempting to connect to the server while it is, essentially bootlooping, results in the connection timing out.

Thanks!

UPDATE: I tested it out without the process_request=health_check argument and there was no difference so that’s not the issue.

I was bit by this too. It was surprising to see the barrage of HEAD requests to my service…

My service is a simple python-websockets based server.

I implemented a healthcheck in the process_request handler, but AFAIK there is no clean way to avoid the error messages caused by non GET requests (such as HEAD/POST/&c). So instead I ended up suppressing the messages via setting the server’s log level [yuck!].

Not sure why render keeps sending these HEAD requests, esp. since there’s a valid/working healthcheck defined???

anyhow, here’s a snippet of my handler + startup (I can’t post link to PR);

async def switchboard_connect(
    connection: websockets.asyncio.server.ServerConnection,
    request: websockets.http11.Request,
):
    if request.path == "/health":
        return connection.respond(HTTPStatus.OK, "OK\n")

    if request.headers.get("Upgrade") is None:
        return connection.respond(
            HTTPStatus.BAD_REQUEST, "Missing websocket Upgrade header\n"
        )

    if request.headers.get("User-Agent", "").startswith("RadioPad/"):
        setattr(connection, "is_radio_pad", True)

    return None


async def main():
    stop_event = asyncio.Event()

    def ask_exit():
        stop_event.set()

    loop = asyncio.get_running_loop()
    loop.add_signal_handler(signal.SIGINT, ask_exit)
    loop.add_signal_handler(signal.SIGTERM, ask_exit)

    host = os.environ.get("SWITCHBOARD_HOST", "localhost")
    port = int(os.environ.get("SWITCHBOARD_PORT", 1980))

    async with serve(
        switchboard,
        host,
        port,
        process_request=switchboard_connect,
    ) as server:
        logging.info("Switchboard running. Press Ctrl+C to stop.")
        logging.info(f"listening on {host}:{port}")
        await stop_event.wait()
        logging.info("Switchboard shutting down...")
        
if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    # websockets 14.0+? logs an error if a connection is opened and closed
    # before data is sent. e.g. when platforms send HEAD requests.
    # Suppress these warnings.
    logging.getLogger("websockets.server").setLevel(logging.CRITICAL)
    try:
        asyncio.run(main())
    except Exception as e:
        logging.error(f"Switchboard exited with error: {e}")

I am starting to think the HEAD requests are related to port scanning functionality of “web services” on the render platform.

From the service logs:

==> No open HTTP ports detected on 0.0.0.0, continuing to scan...
     ==> No open HTTP ports detected on 0.0.0.0, continuing to scan...
     ==> Port scan timeout reached, no open HTTP ports detected. If you don't need to receive public HTTP traffic, create a private service instead.

so this leads me to believe that since it’s a web service, the platform performs HEAD requests [regardless of / in addition to healthchecks] in order to detect running HTTP services.

My service appears unavailable to render (returning 503s) I think because it doesn’t respond to these HEAD requests. even after all these changes :sad_but_relieved_face:

will have to dig in (this is my first time evaluating render…), or serve on a different platform.

I had seen those messages in my logs about the HTTP ports, so I just added a html file and used python http.server to run it. That got rid of those messages but didn’t stop the websocket errors. After restarting my service or after waking it up after it had gone to sleep, the errors stopped appearing, but my server was still no reachable. I read up a bit about exposing ports on Render and I don’t think it’s possible to expose any ports I want, which means I pretty much can’t use Render because I need like 10 websocket ports open to the internet.

If anyone with more experience using Render with websockets is reading this, please let us know what we’re doing wrong and if what I’m trying to do is even possible. Thanks!