Steps
The ws step
A ws step opens one WebSocket, sends its messages, collects what comes back until a count or a timeout says stop, and closes. The plan is data: no message waits for a reply, and none can be templated from one. A conversation that has to react to the server is a script step.
| Key | Type | Meaning |
|---|---|---|
url |
string | ws:// or wss://, templated |
headers |
map of strings | sent with the upgrade request, templated |
send |
list of messages | sent in order as text frames after the upgrade |
expect |
object | required: when to stop collecting |
| Key | Type | Meaning |
|---|---|---|
text |
string | a message as written, templated |
json |
any | a message encoded like a json body: strings templated, numbers as written |
| Key | Type | Meaning |
|---|---|---|
count |
int, 1 to 1000 | stop after this many messages |
timeout |
duration | or stop this long after the upgrade (a grpc stream: after the call starts); must fit in the step's deadline |
What a step does
A ws step dials its own connection over HTTP/1.1, with keep-alives off and the run's TLS
settings for wss://, so --insecure-skip-verify, --ca-cert and a client certificate apply to
it as to an http step. The upgrade request honours HTTP_PROXY, HTTPS_PROXY and NO_PROXY from
the environment. No socket is shared between steps, and none outlives its step.
After the upgrade vero sends each send entry as a text frame, in order, and then collects.
Collection stops when count messages have arrived, when expect.timeout has passed since the
upgrade, or when the server closes. After count messages vero sends a normal close (code 1000).
A received message may be at most 8 MiB, the same cap as an HTTP body.
The step's deadline, 30 s unless timeout is set on the step, covers all of it; the loader refuses
an expect.timeout that does not fit inside it. A run timeout or a signal ends the step
timed_out, with deadline exceeded dialing, deadline exceeded sending or deadline exceeded waiting for messages as its reason, and closes the socket. The collection timeout running out is
not an error: the step's assertions judge what came. repeat does not apply to a ws step.
A plan
examples/ws.yaml runs against the fixture's /fixtures/ws, which echoes
every message it receives, or pushes {"tick": n} messages when asked. The first task sends a
JSON message and a text message and reads both echoes:
- name: echo
steps:
- ws:
url: "{{ env.baseUrl }}/fixtures/ws"
send:
- json: { kind: "ping", run: "{{ run.id }}" }
- text: "plain text, as written"
expect: { count: 2, timeout: 2s }
assert:
- status == 101
- messages[0].body.kind == "ping"
- messages[0].body.run == run.id
- messages[1].text == "plain text, as written"
- closed == nil
The second sends nothing and collects three pushed messages, asserting on when they arrived:
- name: server-push
steps:
- ws:
url: "{{ env.baseUrl }}/fixtures/ws?push=3&every=50"
expect: { count: 3, timeout: 2s }
assert:
- messages count 3
- messages all (it.body.tick > 0)
- messages[2].at > messages[0].at
- duration.first < 1s
Both pass, and each counts as one request in the summary line: the upgrade.
Names in scope
| Name | Value |
|---|---|
status |
the upgrade response's status, 101 on success; recorded even when the dial failed, so a 403 is assertable |
headers |
the upgrade response's headers, looked up without regard to case |
messages |
what arrived, in order; each has body, text and at |
messages[i].body |
the message decoded as JSON when its text parses, with exact numbers; the text otherwise |
messages[i].text |
the message as it came |
messages[i].at |
the time since the upgrade |
closed |
nil, or {code, reason} when the server closed the socket |
duration |
connect, tls, upgrade, first (the first message's arrival) and total |
env, tasks, run |
as on every step |
closed.code is 1006 with the reason the connection ended without a close frame when the
connection ended with no close frame at all, as a proxy's idle timeout ends it. The code is never
on the wire; a client reports it, and browsers report the same number.
Fewer messages is a failure
A ws step with expect also asserts messages count <count>, after the plan's own assertions and
reported at the expect: line. Fewer messages by the timeout is a failure that shows what did
arrive, never a pass and never an error. Here the server pushed once and the plan expected three,
and a second task read a socket the server dropped:
FAIL too-few › step 1 › assert #2 fail-ws.yaml:11
WS ws://127.0.0.1:18080/fixtures/ws?push=1 101 Switching Protocols 302.4ms
< {"tick":1} at 0.0ms
timing connect 0.1ms tls n/a upgrade 1.6ms first 0.0ms total 302.4ms
assert messages count 3 failed
messages = [{"body":{"tick":1},"text":"{\"tick\":1}","at":0.025}]
1 of 3 messages arrived within 300ms
fail-ws.yaml:11
FAIL dropped › step 1 › assert #2 fail-ws.yaml:18
WS ws://127.0.0.1:18080/fixtures/ws?push=2&drop=1 101 Switching Protocols 1.7ms
< {"tick":1} at 0.0ms
< {"tick":2} at 0.0ms
closed by the server: 1006 the connection ended without a close frame
timing connect 0.1ms tls n/a upgrade 1.6ms first 0.0ms total 1.7ms
assert closed.code == 1000 failed
closed.code = 1006
fail-ws.yaml:18
assert messages count 5 failed
messages = [{"body":{"tick":1},"text":"{\"tick\":1}","at":0.020},{"body":{"tick":2},"text":"{\"tick\":2}","at":0.023}]
2 of 5 messages arrived within 300ms
fail-ws.yaml:17
0 passed 2 failed 0 errored 0 timed out 0 skipped 0 blocked 0 not run (2 tasks, 2 requests, 0.3s)
The failure block prints each sent message after >, each received one after < with its
arrival time, the server's close if there was one, and the five phases. total is 302 ms in the
first task because the step waited out its 300 ms collection window.
Load errors
| Plan | Load error |
|---|---|
a url that is not ws:// or wss:// and does not start with {{ |
a ws url starts with ws:// or wss://, got "http://..." |
a send entry with both text and json, or neither |
a message is text or json, one of them |
no expect |
a ws step needs expect: { count, timeout }, saying when to stop collecting messages |
count outside 1 to 1000 |
expect.count is 1 to 1000, got 0 |
no expect.timeout |
expect.timeout is required: how long to collect after the upgrade |
expect.timeout at or past the step's deadline |
expect.timeout 45s does not fit in the step's 30s deadline; raise the step's timeout |
Like every load error, these name the step by path and line, and nothing is sent:
load-errors.yaml:17: tasks.long-wait.steps[0].ws.expect.timeout: expect.timeout 45s does not fit in the step's 30s deadline; raise the step's timeout
load-errors.yaml: 1 load error(s); nothing ran
A ws step's messages is a collection the step itself sizes through expect.count, so an all
or none over it draws no lint warning. The event log carries one request event per ws step
with the messages in it; see vero.events/v1.
This page is docs/content/steps/ws.md in the repository.