Repository navigation
fix(realtime): record a session the upstream never let start as a failure - #1412
Conversation
…lure An upstream that completes the upgrade and then reports an error before any other frame (OpenAI's answer to a wrong key) is now a failure row with the upstream's text, and the client receives only the upstream's frames. A refused handshake's body is read in full from the stream up to the HTTP routes' bound and time limit, and its row records the real response and headers. A dial past connect_timeout records timeout and 504 and takes the timeout cooldown gate.
|
No actionable comments were generated in the recent review. Walkthrough
Merge Risk:
|
…sample The body read ends when the client leaves, keeps what arrived, and decodes a chunked body's framing out of the row. A refused handshake's row says the upstream was reached. A refused session's row and line carry the upgrade's provider request id, frames that arrive while closing still decide whether the session was refused, and a session an upstream failure ended before any frame counts as a deployment failure.
A realtime session the upstream never let start now answers and records as a failure, whichever way it was refused. Four changes, all on
/v1/realtime; a session that became usable is not touched.An upstream that accepts the upgrade and then reports an error. This is how OpenAI answers a wrong key: it completes the
101, sends anerrorevent, then closes with3000(4000for an unknown model or the beta header). That used to read as a success: the row and metrics said 200, and the client got one extra gatewayserver_errorframe after the upstream's. Now, when the upstream's half ends having sent anerrorevent and no other frame:1011, also without a gateway frame.error_classisupstream_in_band, anderror_messageand the access log'serrorreadupstream reported an in-band error: <upstream text>. The first error event is the one recorded. Atcontent_mode: full,responseholds that frame as sent.provider_request_idis read from the101's request-id headers, on the row and the access-log line. OpenAI's101carried nox-request-idwhen measured.status_code, the access log, and thestatusandoutcomerequest-metric labels come from OpenAI's event vocabulary, since no HTTP status arrives:invalid_api_keyis 401;rate_limit_exceededandinsufficient_quotaare 429; any otherinvalid_request_erroris 400 (from fix: answer video failures the way chat does, translate Azure 4xx without a Content-Type, record realtime model errors as 404 #1413 on,invalid_modelandmodel_not_foundare 404); everything else, including servers with other vocabularies, is 502. A numericcodein 400-499 is used as itself; one in 500-599 is recorded as 502, like every other 5xx. The 401 and 400 lines were measured against OpenAI. The 429 lines were not.upstream_auth_failureandinvalid_modelor a caller's beta header doesn't. With no status it takestrigger_on_transport, asupstream_in_band_error.aisix_deployment_*: the success sample used to be taken at the101. It now waits for the first non-error frame or the end of the session, so a refused session counts one failure and no success. A session an upstream failure ended before any frame (a dropped connection, the idle deadline) now counts as a failure too, where it used to count as a success.The session counts as usable from its first frame that isn't an
errorevent: on OpenAI that'ssession.created, and a compatible server need not open with it. Unchanged, and pinned by tests: anerrorevent after that frame, which OpenAI sends for a bad client event while the session stays open; a usable session that later ends on an error close, which keeps its 200 and the gateway frame; and a non-1000 close with no frames at all, which keeps its 200 and the gateway frame because the client has no other explanation.A refused handshake's body is read in full. The dial used to see only the bytes that came with the response head, so a body arriving separately fell back to
upstream returned 401 Unauthorizedand lost the upstream's text,codeandparam. Whether that happened depended on the body's size and on timing. The handshake is still tungstenite's, and its stream now comes back on refusal, so the rest is read by the head's framing (Content-Length, chunked, or to the end of the connection), up toMAX_UPSTREAM_ERROR_BODY_BYTES, within the model's stream budget, the limit a streaming HTTP route's error body has. TLS for the dial moved out of tokio-tungstenite intoupstream_dial::connect_tls, with the same rustls config and server name.codeandparamon these refusals.upstream returned HTTP <status>: failed to read response body: <reason>, where they used to sayupstream returned HTTP <status> during the realtime handshake. A stalled body holds the client's frame for up to one stream budget past the dial, and with no budget (timeout: 0and nostream_timeout) for as long as the upstream stalls and the client stays. A client that leaves ends the read at once, and the reason then readsthe client closed the session.Content-Lengthand no chunking used to fall back; it is now quoted once the connection ends.The failure row records what the upstream answered. On a refused handshake,
responseatcontent_mode: fullwas synthetic text. It is now the upstream's body as sent, or what arrived of it with chunked framing decoded, andmetadata_onlystill leaves it out.upstream_response_headersnow carries the exporter's listed headers, through the same filter as HTTP routes, andprovider_request_idis read from the response's request-id headers, on the row and on the access-log line. These come from the same functions HTTP routes use. An empty body keeps the synthetic text; from #1413 on, a 4xx or 5xx reads there as chat does (upstream returned HTTP <status> with no error detail in the response), and a 2xx or 3xx keepsduring the realtime handshake. The row also now says the upstream was reached:upstream_latency_msis the dial's duration where it was 0, and OTLP spans carry the server address where they had none.A dial past
upstream.connect_timeout_msis a timeout. It recordedtransportand 502, sotrigger_on_timeouthad no effect on a realtime dial hang. It now recordserror_classtimeoutand 504 on the row, the access log and the metrics, readsupstream request timed out after <n>ms: ..., and takes the timeout gate, sotrigger_on_timeout: falsekeeps the model available andtrigger_on_transport: falseno longer does. A kernelETIMEDOUTon the dial reads the same way, as it does on HTTP routes. What the client gets doesn't change: the fixedfailed to connect to the upstream realtime endpointframe and close1011. HTTP routes answer a timeout withupstream request timed out after <n>ms, so this one frame stays different.No configuration needs editing. Alerts or dashboards that count realtime dial hangs under 502, or OpenAI wrong-key sessions under 200, will see them move to 504 and 401.
realtime-refused-handshake-e2edrives all of this against scripted upstreams: head and body written separately, the measured OpenAI sequences, and chat on the same upstream as the oracle for the frame and the row. Two existing specs pinned the old behaviour and were changed: the silent dial's status inrealtime-ws-e2eand the never-arrived body's row text infailure-row-metadata-only-e2e.🤖 Generated with Claude Code
Summary by CodeRabbit