WebSocket virtual service architecture

A developer or tester uses a web browser to access the console. The console manages the virtual service. The system under test (application under test) connects directly to the virtual service.

WebSocket connections share the virtual service HTTP port (trafficparrot.virtualservice.http.port, 8081 by default) with the HTTP virtual service. A client opens an ordinary HTTP request carrying the Connection: Upgrade and Upgrade: websocket headers, and Traffic Parrot upgrades that connection to a WebSocket. Every other request on the port is handled by the HTTP virtual service exactly as before, so there is no separate WebSocket port to configure or open in a firewall.

The path the client connects to (/ws/chat in ws://localhost:8081/ws/chat) is the channel. Each mapping names the channel it applies to, so one virtual service can simulate many WebSocket endpoints at once.

The console (http://localhost:8080 by default) provides the WebSocket menu with the Add/Edit, Record and Logs pages, and proxies the management API so that scripts and tests can create mappings and inspect the frame journal.

Configuration

Enabling WebSocket support

WebSocket support is switched off by default. To switch it on, set the following property in trafficparrot.properties and restart Traffic Parrot:

Property Default Description
trafficparrot.websocket.enabled false When true, the virtual service HTTP port accepts WebSocket upgrade requests and the WebSocket menu appears in the console. When false, an upgrade request is answered by the HTTP virtual service like any other request, and the menu is hidden.

See the WebSocket properties reference.

Where mappings are stored

Mappings are JSON files, one file per mapping, in the websocket-mappings directory. The directory sits next to the mappings directory that holds HTTP mappings:

  • trafficparrot-x.y.z/websocket-mappings/ serves when no scenario is selected.
  • trafficparrot-x.y.z/scenarios/<scenario name>/websocket-mappings/ serves while that scenario is selected.

Traffic Parrot creates the directory at the root and in every scenario. Each file is named after the mapping id, for example 0738393e-45e7-435b-8fca-49a7d4474e26.json. Files saved from the console or the management API land in the directory of the currently selected scenario. You can also write or copy files into these directories by hand; they are picked up without a restart because the directory is read again for every frame, unless mapping caching is switched on:

Property Description
trafficparrot.virtualservice.mapping.cache.milliseconds Mapping files can be cached in memory during replay to improve performance. The default value of 0 means do not cache at all. With a cache, a file written by hand is served once the cache expires; files saved from the console or the management API are served immediately.

WebSocket mappings

A mapping file

A mapping answers one thing on one channel: either a message the client sends, or the client connecting. This mapping replies pong whenever a client connected to /ws/chat sends ping:

{
  "id" : "0738393e-45e7-435b-8fca-49a7d4474e26",
  "name" : "chat ping",
  "trigger" : {
    "type" : "message",
    "channel" : {
      "type" : "websocket",
      "initiatingRequestPattern" : {
        "urlPath" : "/ws/chat",
        "method" : "ANY"
      }
    },
    "message" : {
      "body" : {
        "equalTo" : "ping"
      }
    }
  },
  "actions" : [ {
    "type" : "send",
    "message" : {
      "body" : "pong"
    },
    "channelTarget" : {
      "type" : "originating"
    }
  } ]
}

This mapping sends Welcome to the chat to every client as soon as it connects to /ws/chat. It has no message pattern, and the tp.onConnect metadata flag is what makes it fire on connect:

{
  "id" : "b592801c-297d-4923-bbd2-b81bad436151",
  "name" : "chat welcome",
  "trigger" : {
    "type" : "message",
    "channel" : {
      "type" : "websocket",
      "initiatingRequestPattern" : {
        "urlPath" : "/ws/chat",
        "method" : "ANY"
      }
    }
  },
  "actions" : [ {
    "type" : "send",
    "message" : {
      "body" : "Welcome to the chat"
    },
    "channelTarget" : {
      "type" : "originating"
    }
  } ],
  "metadata" : {
    "tp.onConnect" : true
  }
}

Fields

Field Description
id A UUID that also names the file. Generated when a mapping is created without one.
name Optional. A label for people; it is not used for matching.
priority Optional, 5 when absent. When more than one mapping matches a frame, the lowest number wins, as for HTTP mappings.
trigger.type Always message.
trigger.channel Which connections the mapping applies to. type is websocket and initiatingRequestPattern is matched against the HTTP request that opened the connection, using the same request matchers as an HTTP mapping: urlPath for an exact path, urlPathPattern for a regular expression, url or urlPattern to include the query string, and headers or queryParameters to match the upgrade request's headers or query parameters. Omit initiatingRequestPattern to match every channel. method is added automatically as ANY.
trigger.message.body Optional. A body matcher applied to the text of the incoming frame, for example equalTo, contains, matches, equalToJson or matchesJsonPath. Omit message to match every frame on the channel.
actions The frames to send, in order, each with type send and the frame text in message.body. channelTarget is added automatically as originating, meaning the frame goes back to the client whose frame or connection fired the mapping.
metadata Optional. "tp.onConnect": true makes the mapping an on-connect mapping. Other keys are stored and returned unchanged.

When you create a mapping you only need to provide the fields you care about. Traffic Parrot fills in the id, method and channelTarget when it saves the file:

{
  "name": "chat ping",
  "trigger": {
    "type": "message",
    "channel": {"type": "websocket", "initiatingRequestPattern": {"urlPath": "/ws/chat"}},
    "message": {"body": {"equalTo": "ping"}}
  },
  "actions": [{"type": "send", "message": {"body": "pong"}}]
}

Message triggers and on-connect triggers

A mapping fires on exactly one of two things:

  • A message. The client sends a text frame on the channel. If the mapping has a trigger.message.body matcher, the frame has to match it; a mapping without one answers any frame on the channel.
  • Connect (server push). The client connects to the channel. The mapping carries "tp.onConnect": true in its metadata and has no trigger.message. Its frames are sent as soon as the connection is open, before the client has sent anything. An on-connect mapping is never considered for incoming frames, and a message mapping never fires on connect.

A channel that greets the client on connect and then answers two different messages is three mappings that share the same channel, not one mapping with three rules.

How a frame is matched

When a text frame arrives, Traffic Parrot considers every message mapping whose channel matches the connection and whose body matcher, if any, matches the frame. Among the candidates the mapping with the lowest priority number wins; when two candidates share a priority, the one that was added first wins. The winner's frames are sent back on the same connection and both the incoming frame and the reply are written to the message journal.

A frame that matches no mapping gets no reply. The connection stays open, and the frame is written to the journal as unmatched, so a silent client is the first thing to check there. For example, with a mapping that answers a JSON subscription on /ws/orders at the default priority and a catch-all mapping on the same channel at priority 10:

Client sends Traffic Parrot replies
{"action":"subscribe","orderId":1234} {"orderId":1234,"status":"SHIPPED"} from the mapping whose body matcher is contains "action":"subscribe"
{"action":"cancel"} {"error":"unknown action"} from the catch-all mapping, because it is the only candidate

Only text frames are matched. A binary frame sent by a client is ignored.

Scenarios

WebSocket mappings follow the selected scenario. While a scenario is selected, Traffic Parrot serves the mappings in that scenario's websocket-mappings directory and the Add/Edit page lists and saves mappings there. Selecting a different scenario changes what is served straight away, including on connections that are already open. When no scenario is selected, the websocket-mappings directory at the traffic files root serves.

A composite scenario serves no WebSocket mappings, and a virtual service running on its own dedicated ports serves the WebSocket mappings of the scenario selected in the console rather than its own. Scenario cleanup copies HTTP mappings only, so WebSocket mappings stay in the scenario they were saved in.

Adding and editing mappings in the console

Adding a mapping

Go to WebSocket > Add/Edit. The form at the top of the page creates one mapping:

  • Channel is the path a client connects to, for example /ws/chat. Leave it blank to answer on any channel.
  • Fires on is either A message or Connect (server push).
  • Match text, shown for a message trigger, is the frame text to match exactly. Leave it blank to match any message.
  • Send text is the frame Traffic Parrot sends back.

Click Save. The mapping is written to the websocket-mappings directory of the selected scenario and appears in the Mappings table below the form.

Add WebSocket mapping form with channel /ws/chat, match text ping and send text pong

The table shows each mapping's channel, what it fires on (On connect, Any message, or the body matcher such as equalTo 'ping') and what it sends. It lists the mappings of the selected scenario and refreshes itself, so a mapping created over the management API or copied into the directory appears here too.

WebSocket mappings table listing channel, trigger, sends and actions columns

Editing and deleting a mapping

Clicking the edit button Edit button on a row opens the mapping in a dialog with the same fields as the form. Clicking the delete button removes the mapping and its file.

Edit WebSocket mapping dialog with trigger and sends panels

The form edits the simple shape of a mapping: an exact channel path, an exact match text and a text frame to send. A mapping written by hand or over the management API can use more than that, for example a contains body matcher or a urlPathPattern channel. The dialog shows such values as they are, and saving keeps them unless you change the field, so opening and saving a richer mapping does not simplify it. The mapping's id, name, priority and any other metadata are kept as well.

Logs

WebSocket > Logs links to the application logs and lists the log files, which is where a frame that matched nothing, a refused recording or a backend that could not be reached is reported. The frames themselves are kept in the message journal, which is read over the management API.

Recording WebSocket traffic

Introduction

Instead of writing mappings by hand, you can record them from a real WebSocket backend. While recording is on, Traffic Parrot proxies every WebSocket connection to the backend you name: each frame the client sends is forwarded to the backend, each frame the backend sends is forwarded to the client, and each pair is saved as a mapping. After you stop recording, and with the backend switched off, the same frames are answered from the recorded mappings.

Recording works frame by frame. Traffic Parrot pairs each frame from the backend with the most recent frame the client sent on that connection and saves the pair as a message mapping whose match text is the client frame and whose send text is the backend frame. A frame the backend sends before the client has sent anything is saved as an on-connect mapping.

Steps to record

  1. Start Traffic Parrot with WebSocket support enabled
  2. Go to the WebSocket record page
  3. Enter the WebSocket backend to record from, for example ws://backend.example.com:9000. Both ws:// and wss:// are accepted. Only the host and port are used: the path each client connects with is appended to it, so one backend serves every channel.
  4. Click Start recording
  5. Reconfigure your system under test to connect to the virtual service, ws://localhost:8081 by default, using the same paths it used against the real backend
  6. Execute a test case. The mappings appear in the Mappings table on the page as they are recorded.
  7. Click Stop recording
WebSocket record page while recording, showing the backend being proxied to and the recording limitation notice

Recorded mappings are saved into the websocket-mappings directory of the scenario selected when the frame arrived, so select the scenario you want to record into before you start. Here a backend that greets each client and answers ping and hello has produced three mappings on /ws/echo:

Mappings table after a recording, with two message mappings and one on-connect mapping on /ws/echo

Recording a client frame that a mapping on that channel already matches exactly replaces that mapping, so re-running a test does not pile up duplicates. A mapping you wrote with a contains or regular expression matcher on that channel is left alone.

While recording is on, every WebSocket connection is proxied and no mapping is served. If the backend cannot be reached, the client's connection is closed and the reason is written to the application log, rather than serving mappings while the page says it is recording. Clicking Stop recording stops capturing at once, including on connections that are still open; those connections keep being proxied until they close, and new connections are served from mappings.

What recording does not capture

Each frame is recorded on its own. A recorded mapping answers one client frame with one response frame. WebSocket has no request/response correlation, so Traffic Parrot pairs each response frame with the most recent client frame and records nothing about the order frames arrived in. On replay every frame is matched independently: a conversation whose meaning depends on sequence will not be reproduced. Frames the backend sends before the client has said anything are recorded as on-connect responses. Binary frames are passed through to the client but not recorded.

In particular:

  • When the backend answers one client frame with several frames, only the first is recorded; the others are forwarded to the client and noted in the application log.
  • A backend that pushes frames on its own schedule, after the client has spoken, has those frames paired with whatever the client sent last.
  • Replies are not written to the message journal while recording, because they come from the backend rather than from a mapping. The journal shows the client's frames only.

If your streaming API needs a stateful conversation rather than independent frames, consider modelling it as gRPC bidirectional streaming, where Traffic Parrot records and replays whole streams.

Management API

Base URL

The WebSocket management API is served under /websocket/management on the console port, for example http://localhost:8080/websocket/management/mappings. It is also available on the virtual service management port, trafficparrot.virtualservice.http.management.port (8083 by default), which is the same API without going through the console. Responses are JSON, and a mapping or a message pattern is sent as a JSON request body.

Mappings

Method and path Description
GET /websocket/management/mappings Lists the mappings of the selected scenario as a JSON array.
POST /websocket/management/mapping Creates a mapping from the JSON body and returns it as saved, with its id.
GET /websocket/management/mapping/{id} Returns one mapping.
PUT /websocket/management/mapping/{id} Replaces one mapping with the JSON body. The id in the URL wins over any in the body.
DELETE /websocket/management/mapping/{id} Deletes one mapping and its file. Returns {"result":"deleted"}.

To create the ping mapping from the example above:

curl -X POST -H 'Content-Type: application/json' http://localhost:8080/websocket/management/mapping -d '{
  "name": "chat ping",
  "trigger": {
    "type": "message",
    "channel": {"type": "websocket", "initiatingRequestPattern": {"urlPath": "/ws/chat"}},
    "message": {"body": {"equalTo": "ping"}}
  },
  "actions": [{"type": "send", "message": {"body": "pong"}}]
}'

To create an on-connect mapping, add the metadata flag and leave out the message pattern:

curl -X POST -H 'Content-Type: application/json' http://localhost:8080/websocket/management/mapping -d '{
  "name": "chat welcome",
  "trigger": {
    "type": "message",
    "channel": {"type": "websocket", "initiatingRequestPattern": {"urlPath": "/ws/chat"}}
  },
  "actions": [{"type": "send", "message": {"body": "Welcome to the chat"}}],
  "metadata": {"tp.onConnect": true}
}'

To list, change and delete mappings:

curl http://localhost:8080/websocket/management/mappings
curl -X PUT -H 'Content-Type: application/json' http://localhost:8080/websocket/management/mapping/0738393e-45e7-435b-8fca-49a7d4474e26 -d '{ ... }'
curl -X DELETE http://localhost:8080/websocket/management/mapping/0738393e-45e7-435b-8fca-49a7d4474e26

Message journal

Traffic Parrot keeps a journal of every text frame it receives and every frame it sends from a mapping, so a test can verify what the system under test sent. The journal is in memory and is not limited in size, so reset it between tests.

Method and path Description
GET /websocket/management/messages All journal events, newest first.
GET /websocket/management/message/{id} One journal event.
POST /websocket/management/messages/find The events matching the message pattern in the body, oldest first. {} matches everything.
POST /websocket/management/messages/count How many events match the message pattern in the body, as {"count": n}.
DELETE /websocket/management/messages Empties the journal. Returns {"result":"reset"}.

A message pattern has the same shape as a mapping's trigger.message: a body matcher, optionally with a channel request pattern to restrict it to one channel. To count how many times ping was received and to fetch the events for a subscription:

curl -X POST -H 'Content-Type: application/json' http://localhost:8080/websocket/management/messages/count -d '{"body": {"equalTo": "ping"}}'
curl -X POST -H 'Content-Type: application/json' http://localhost:8080/websocket/management/messages/find -d '{"body": {"contains": "subscribe"}}'

Each event records the direction, the frame, the connection it travelled on and, for a received frame that was matched, the mapping that answered it. A received frame that matched nothing has "wasMatched": false and no stubMapping:

{
  "id" : "6ed47582-3d47-49dd-8347-2c555d44b665",
  "eventType" : "received",
  "channel" : {
    "type" : "websocket",
    "id" : "e81d801c-b657-448d-a534-d0756db94e53",
    "open" : true,
    "initiatingRequest" : {
      "url" : "/ws/chat",
      "absoluteUrl" : "http://localhost:8081/ws/chat",
      "method" : "GET",
      "headers" : {
        "Host" : "localhost:8081",
        "Connection" : "Upgrade",
        "Upgrade" : "websocket",
        "Sec-WebSocket-Key" : "qY15nAvIPNCftBdM0vYmXw==",
        "Sec-WebSocket-Version" : "13",
        "Sec-WebSocket-Extensions" : "permessage-deflate; client_max_window_bits"
      },
      "loggedDate" : 1790024962369,
      "scheme" : "http",
      "host" : "localhost",
      "port" : 8081,
      "loggedDateString" : "2026-09-21T21:09:22.369Z"
    }
  },
  "message" : "ping",
  "stubMapping" : {
    "id" : "0738393e-45e7-435b-8fca-49a7d4474e26",
    "name" : "chat ping",
    ...
  },
  "wasMatched" : true,
  "timestamp" : "2026-09-21T21:09:22.979091727Z"
}
Field Description
eventType received for a frame from the client, sent for a frame Traffic Parrot sent from a mapping.
channel The connection: its own id, whether it is still open, and the HTTP request that opened it, including the path and the headers.
message The text of the frame.
wasMatched true when a mapping answered the frame, or for every sent event. false for a received frame that matched nothing.
stubMapping The mapping that answered a received frame. Absent on sent events and on unmatched frames.
timestamp When the frame was received or sent.

Recording

Method and path Description
POST /websocket/management/startRecording Starts recording. Takes a form parameter proxy-url naming the backend, which must start with ws:// or wss://.
POST /websocket/management/stopRecording Stops recording.
GET /websocket/management/currentRecordingStatus Whether recording is on and, if so, where to. Clients connect to the virtual service HTTP port to be recorded, so currentRecordingPort reports that port.
curl -X POST http://localhost:8080/websocket/management/startRecording --data 'proxy-url=ws://backend.example.com:9000'
curl http://localhost:8080/websocket/management/currentRecordingStatus
curl -X POST http://localhost:8080/websocket/management/stopRecording

While recording, the status reads:

{
  "recordingOn" : true,
  "currentProxyUrl" : "ws://backend.example.com:9000",
  "currentProxyBaseUrl" : "ws://backend.example.com:9000",
  "currentRecordingPort" : "8081",
  "currentRecordingPath" : "",
  "recordRequestHeadersForMatching" : ""
}

Errors

Every error is a JSON object with a result message:

Status When Example
400 Bad Request The id in the URL is not a UUID, the mapping or pattern in the body cannot be parsed, or the recording backend URL is not a WebSocket URL. {"result":"not a valid mapping id: 'not-a-uuid'"}
404 Not Found No mapping or event with that id, or a path the API does not serve. {"result":"not found: 11111111-2222-3333-4444-555555555555"}
405 Method Not Allowed The path exists but not for that method. The Allow header names the methods it does serve. {"result":"'/mapping' does not support this method (allowed: POST)"}
409 Conflict A PUT arrived for a mapping that the selected scenario's directory no longer holds, for example because it was deleted or the scenario was switched between reading and saving.

Limitations

  • Text frames only. A binary frame from a client is ignored, and a mapping can only send text. During recording, binary frames from the backend are forwarded but not recorded.
  • Every frame is independent. A mapping answers a frame with fixed frames. There is no state between frames, no sequence and no delay, and recording captures nothing about frame order. For a stateful stream, see gRPC bidirectional streaming.
  • Inline text only. The frames a mapping sends are written in the mapping file. Response bodies cannot be read from a file in __files, which is why the WebSocket menu has no Static Files entry, and dynamic responses are not applied to WebSocket frames.
  • Replies go to the client that triggered them. A mapping cannot send a frame to a different connection.
  • The journal grows until it is reset. Reset it with DELETE /websocket/management/messages between tests.