# websocket

Source: https://www.pingtower.com/docs/addons/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](/docs/service/checks/).

## Parameters

| Parameter | Default | Meaning |
| --- | --- | --- |
| `url`<span class="req">REQUIRED</span> | | 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](#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

```yaml
- 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`](/docs/service/checks/#secrets) naming the header.

To keep a credential sealed, put it in the check's `env` and reference it from the header value as `{{NAME}}`:

```json
{"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 in `env`, and every name in `env` must 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 in `url`, `send`, `expect` and `subprotocol`. A check without `env` is 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`:

```text
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](/docs/service/checks/#a-new-agent-token-withholds-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

```json
{"name":"socket-down","match":{"tags":["addon:websocket"],"min_level":400},
 "fire_after":{"count":2,"window_minutes":5,"distinct_sources":true}}
```
