websocket
Open a WebSocket, optionally send one text message, and check the first reply. The upgrade and the reply are timed apart.
Realtime features fail apart from the HTTP API beside them: a proxy that drops the Upgrade header, or a socket server that accepts and then never answers. This check performs the upgrade itself and verifies the server’s answer. It then sends your message, if you set one, only after the server has proved it speaks WebSocket, and reads the reply.
It runs on your own agents and on Pingtower’s servers as a hosted check.
Parameters
| Parameter | Default | Meaning |
|---|---|---|
urlREQUIRED | A ws:// or wss:// URL. The port defaults to 80 or 443. Credentials in the URL are refused. | |
send | A text message to send once connected, at most 4 KiB. | |
expect | Text the first message received must contain. Set without send, the check waits for the server to speak first. | |
subprotocol | A subprotocol to request. The server must accept it. | |
headers | Map of extra headers for the upgrade request. Host, Upgrade, Connection and any Sec-WebSocket-* header are refused, as is a line break in a value. A value can take a sealed secret as {{NAME}}: see Secrets. | |
max_response | Reply time above this is degraded. With neither send nor expect, the upgrade time instead. |
wss:// uses TLS 1.2 or later and verifies the certificate.
Status
With neither send nor expect, a verified upgrade is up: upgraded in 85ms. Otherwise the first data message must arrive and contain expect: replied in 40ms. The check answers the server’s pings while it waits. Over max_response, either one is degraded.
It is down when:
- the host does not resolve, the connection is refused or times out, or the TLS handshake fails;
- the upgrade is refused, such as
upgrade refused with HTTP 403, or the server’s answer is not a valid WebSocket upgrade; - the server does not accept the requested subprotocol;
- no message arrives before the timeout, or the server closes the connection first;
- the reply does not contain
expect, or is larger than 64 KiB.
Measurements
| Key | Meaning |
|---|---|
dns_ms | Resolution time, when the URL names a host rather than an IP |
connect_ms | TCP connect time |
handshake_ms | TLS, when wss://, and the upgrade |
response_ms | From sending send, or from the upgrade, to the first reply, when the check waits for one |
total_ms | The whole run |
status_code | Label: the HTTP status, when the upgrade was refused |
The check needs addons agent 0.4.0 or later; an older agent reports it as an unknown addon.
Example
- id: live-feed
addon: websocket
interval: 60s
with:
url: wss://live.acme.example/socket
send: '{"type":"ping"}'
expect: '"type":"pong"'
headers:
Origin: https://acme.example
max_response: 500ms
Secrets
A header value written as it is, such as Authorization: Bearer abc123, is stored in the clear. Everyone who can read checks in the project sees it, in the web app, the apps and the API. A create or update that finds a credential in one still saves, and the answer carries secret_warnings naming the header.
To keep a credential sealed, put it in the check’s env and reference it from the header value as {{NAME}}:
{"addon":"websocket",
"with":{"url":"wss://live.acme.example/socket",
"headers":{"Authorization":"Bearer {{TOKEN}}"}},
"env":{"TOKEN":"…"}}
The agent fills in {{TOKEN}} when the check runs. The value is sealed at rest and write-only: reading the check back shows env_keys and the header as written, Bearer {{TOKEN}}, never the value. A value of env that turns up in a result is stored as [redacted].
- A name is upper-case letters, digits and
_, not starting with a digit, at most 64 characters. A check takes at most 20. - A value is 1 to 4096 bytes, has no line break or NUL, and does not start with
enc:. - Every
{{NAME}}in a header value must be inenv, and every name inenvmust be used by a header value. A header value may hold several references, with other text around them. - While the check has
env, every{{in a header value must begin a{{NAME}}reference, and{{is refused inurl,send,expectandsubprotocol. A check withoutenvis sent exactly as written, braces included. - A value is filled in once: a secret that itself contains
{{…}}is sent as it is.
A create, or an update that sends env, that breaks these is refused with a 400, such as with.headers Authorization references {{TOKEN}}, which env does not define or env TOKEN is not referenced by any with.headers value.
The secret is tied to the check’s url. Leaving env out of an update keeps it while url stays exactly the same, host, path and query. Otherwise the update must send env again, or {} to clear it, and is refused with a 422:
env: with.url now points at another destination; re-supply env (or send {} to clear it)
An update that leaves env out is refused the same way when its headers no longer fit the stored names: env: with.headers references NAME, which is not sealed; send env with the check or env: NAME is no longer referenced by any with.headers value; re-supply env (or send {} to clear it). Adding a region or source to sources without sending env again is refused too. Minting a new agent token for a source stops the check running from that source until you send env again: see Secrets.
Sealed headers work only on checks Pingtower assigns to the agent, not on checks in a local agent.yaml, and need addons agent 0.6.0 or later. An agent older than 0.6.0 refuses a WebSocket check that carries env and runs the rest of its checks; the refused check goes silent there, and the agent’s heartbeat counts it in assignments_rejected.
A rule for it
{"name":"socket-down","match":{"tags":["addon:websocket"],"min_level":400},
"fire_after":{"count":2,"window_minutes":5,"distinct_sources":true}}