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.

verodocs