Problem
Measured against a real local WebSocket while building the transport tests for
#139. How a provider disconnection is diagnosed depends on how politely the
provider disconnects, and the polite case is the only one that gets it right:
| What the provider did |
reason |
diagnostic_code |
| close frame, code 1000 |
connection |
connection |
| close frame, code 1011 |
unavailable |
internal_failure |
| close frame, code 1008 |
unavailable |
internal_failure |
| socket aborted, no close frame |
unavailable |
internal_failure |
| handshake answered with a redirect |
unavailable |
internal_failure |
Only a graceful close reaches run_provider's own
raise RelayPolicyError("connection"). Everything else raises a
websockets exception — ConnectionClosedError, InvalidStatus,
InvalidHandshake — none of which is a RelayPolicyError, so it falls through
to the catch-all in _Relay.run:
except Exception:
# No provider exception text, payload, URL, or key is logged or
# returned to the participant. A persistence failure is terminal.
self.diagnostic_code = "internal_failure"
self.diagnostic_stage = stage
self.stop("unavailable")
The redaction that comment describes is doing its job. The classification is
not: internal_failure is what an operator reads when something in their
deployment is broken, and it is being used for the most clearly provider-side
failure there is — the upstream dropping the socket.
Why it matters
diagnostic_code and diagnostic_stage exist so an operator can tell what went
wrong without seeing participant content. An interview that ends because Google
closed the socket with 1011 currently tells them to go looking for a bug in
their own service. The cases where they would be right — a persistence failure,
a programming error — are indistinguishable from the cases where they are not.
The more clearly a provider misbehaves, the more confidently this blames the
operator.
What to decide
- Should a transport failure be
connection / connection, matching the
graceful close? That is the answer the table above suggests, and it costs one
except clause in run_provider around the async with and the async for.
reason is the harder half. It is not only a diagnostic: it reaches the
participant in relayEnd and is persisted with the session. Moving abnormal
closes from unavailable to connection changes a stored value and whatever
reads it. Worth checking what distinguishes the two for a reader before
changing either.
- Is a redirect refusal the same class? It is a security control doing its job
rather than a fault — arguably its own code, so an operator can tell a
blocked redirect from a dropped connection.
Not urgent, and not invisible
Nothing about the participant's experience is wrong today: every case above ends
the session promptly, sends relayEnd, and closes the browser socket. Those
properties are asserted in services/api/tests/test_voice_relay_transport.py.
The same tests pin the current diagnostics, with a comment naming them as the
subject of this issue, so whoever fixes this will find the assertions to update
rather than a surprise.
Acceptance criteria
Problem
Measured against a real local WebSocket while building the transport tests for
#139. How a provider disconnection is diagnosed depends on how politely the
provider disconnects, and the polite case is the only one that gets it right:
reasondiagnostic_codeconnectionconnectionunavailableinternal_failureunavailableinternal_failureunavailableinternal_failureunavailableinternal_failureOnly a graceful close reaches
run_provider's ownraise RelayPolicyError("connection"). Everything else raises awebsocketsexception —ConnectionClosedError,InvalidStatus,InvalidHandshake— none of which is aRelayPolicyError, so it falls throughto the catch-all in
_Relay.run:The redaction that comment describes is doing its job. The classification is
not:
internal_failureis what an operator reads when something in theirdeployment is broken, and it is being used for the most clearly provider-side
failure there is — the upstream dropping the socket.
Why it matters
diagnostic_codeanddiagnostic_stageexist so an operator can tell what wentwrong without seeing participant content. An interview that ends because Google
closed the socket with 1011 currently tells them to go looking for a bug in
their own service. The cases where they would be right — a persistence failure,
a programming error — are indistinguishable from the cases where they are not.
The more clearly a provider misbehaves, the more confidently this blames the
operator.
What to decide
connection/connection, matching thegraceful close? That is the answer the table above suggests, and it costs one
exceptclause inrun_provideraround theasync withand theasync for.reasonis the harder half. It is not only a diagnostic: it reaches theparticipant in
relayEndand is persisted with the session. Moving abnormalcloses from
unavailabletoconnectionchanges a stored value and whateverreads it. Worth checking what distinguishes the two for a reader before
changing either.
rather than a fault — arguably its own code, so an operator can tell a
blocked redirect from a dropped connection.
Not urgent, and not invisible
Nothing about the participant's experience is wrong today: every case above ends
the session promptly, sends
relayEnd, and closes the browser socket. Thoseproperties are asserted in
services/api/tests/test_voice_relay_transport.py.The same tests pin the current diagnostics, with a comment naming them as the
subject of this issue, so whoever fixes this will find the assertions to update
rather than a surprise.
Acceptance criteria
deployment, by the diagnostic alone
reasoneither stays as it is with a stated rationale, or changes with anote on what reads the stored value
old classification