Skip to content

(Bug) include the port in the websocket signing url - #114

Open
jamescamping wants to merge 1 commit into
TheFoundryVisionmongers:mainfrom
jamescamping:fix/ws-signature-default-port
Open

jamescamping wants to merge 1 commit into
TheFoundryVisionmongers:mainfrom
jamescamping:fix/ws-signature-default-port

Conversation

@jamescamping

Copy link
Copy Markdown

Problem

Websocket.signed_path builds the string it signs by formatting the endpoint yarl.URL:

path_to_sign = (
    f"{self.endpoint}?keyid={self._access_key.id}&expiretime={urllib.parse.quote(time)}"
)

endpoint is constructed as ws://{hostname}:{port}/ws, but yarl drops the port when it is the default for the scheme. So the port is silently lost from the signed string:

ws://host:80/ws     ->  ws://host/ws        (port dropped)
wss://host:443/ws   ->  wss://host/ws       (port dropped)
ws://host:8080/ws   ->  ws://host:8080/ws   (kept)

The server includes the port when it computes its own HMAC, so the signatures do not match and the handshake fails:

aiohttp.client_exceptions.WSServerHandshakeError: 401, message='Invalid response status',
url='ws://<server>/ws?keyid=...&expiretime=...&signature=...&id=...'

Note the URL in the error has no port.

This only affects servers running on a scheme-default port — http_port: 80, or 443 with TLS. On the default 8080 yarl keeps the port and everything works, which is presumably why it has gone unnoticed.

REST calls are unaffected, which makes this confusing to diagnose: sign_request signs METHOD\n\n\ndate\npath with no scheme, host, or port, so signed HTTP requests to the very same server on the very same port succeed while only the websocket 401s.

Reproduction

Against Flix Server 8.1.1 deployed with http_port: 80, pointing the client directly at a single server (no proxy or load balancer involved), using an access key that works fine for REST:

1) Stock SDK:
  REST get_all_shows -> OK (3 shows)
  WEBSOCKET -> FAILED 401 Invalid response status

2) Same SDK, signing string patched to keep the explicit port:
  REST get_all_shows -> OK (3 shows)
  WEBSOCKET -> CONNECTED

I also confirmed the mismatch directly by replaying the handshake by hand against the server: signing ws://<server>:80/ws?keyid=...&expiretime=... returns 101 Switching Protocols, while the identical string with the port removed returns 401. That is the only difference between the two requests.

Fix

Build the signing string straight from hostname and port rather than round-tripping it through yarl.URL, and derive endpoint from that same string so the two cannot drift apart.

The existing comment in signed_path already notes one yarl normalisation that had to be worked around by hand (: escaping); this is the same class of issue.

Behaviour on non-default ports is unchanged, and the URL actually used to connect is unchanged — only the string that gets signed is affected.

Testing

  • Websocket handshake now succeeds against a Flix Server 8.1.1 deployment on port 80; previously 401.

  • The signed string is byte-identical to the old one for every scheme/port combination except the two broken ones, so nothing else changes:

    scheme port before after same
    ws 80 ws://host/ws ws://host:80/ws no
    ws 443 ws://host:443/ws ws://host:443/ws yes
    ws 8080 ws://host:8080/ws ws://host:8080/ws yes
    wss 80 wss://host:80/ws wss://host:80/ws yes
    wss 443 wss://host/ws wss://host:443/ws no
    wss 8080 wss://host:8080/ws wss://host:8080/ws yes
  • ruff format --check clean; ruff check reports the same set of pre-existing findings before and after this change.

I don't have a TLS deployment to hand, so the wss / 443 case is reasoned from the same yarl behaviour rather than tested end to end.

🤖 Generated with Claude Code

Websocket.signed_path built the string to sign by formatting the endpoint
yarl.URL. yarl omits the port when it matches the default for the scheme, so
ws:// on port 80 and wss:// on port 443 signed a url with no port while the
server signed one that had it, and the handshake failed with 401.

Build the signing string directly from the hostname and port so the port is
always present, and derive endpoint from that same string so the two cannot
drift apart.

Reproduced against Flix Server 8.1.1 deployed on http_port 80: the websocket
handshake returned 401 while signed REST requests to the same server on the
same port succeeded, since sign_request signs only the path and is therefore
unaffected. With this change the handshake succeeds. Deployments on the
default port 8080 keep their existing behaviour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jamescamping jamescamping changed the title (Bug) include the port in the websocket signing url [main][](Bug) include the port in the websocket signing url Aug 6, 2026
@jamescamping jamescamping changed the title [main][](Bug) include the port in the websocket signing url (Bug) include the port in the websocket signing url Aug 6, 2026
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.

1 participant