Part of Switching from another tool, the hub for every migration page.
« Back to documentation homeTraffic Parrot is a standalone mock and service virtualization server that is compatible with WireMock stub mappings. If you are coming from Hoverfly and rely on its Modify or Synthesize middleware to mutate or generate responses, this page maps that workflow onto Traffic Parrot.
Hoverfly's middleware modes run a language-agnostic executable: for each request/response pair, Hoverfly serialises the pair to JSON, feeds it to the middleware on stdin, and reads the mutated JSON back from stdout. Your middleware can be written in any runtime that can read stdin and write stdout: Python, Go, Node, a shell script, and so on.
Traffic Parrot matches that out-of-process middleware idea with the built-in
external-process response
transformer. It serialises a versioned JSON envelope (the matched request plus the
response about to be sent) to your executable's stdin and applies the mutated response read back
from its stdout. The
External-process middleware reference in
Dynamic Responses is the canonical, full description of the transformer; this page is the
Hoverfly-migration view of it: the concept mapping, a copy-pasteable worked example, and the
differences to expect.
The two designs share the same core: an external program is handed the HTTP exchange as JSON and returns a mutated exchange. The configuration model differs: Hoverfly enables middleware with a single global flag, while Traffic Parrot uses a two-layer model (a global opt-in, then a per-mapping opt-in). The table below translates each Hoverfly concept:
| Hoverfly | Traffic Parrot |
|---|---|
Global -middleware <cmd> flag enables one middleware for the whole
instance |
Two-layer model. Global opt-in in
trafficparrot.properties:
trafficparrot.http.externalprocess.enabled=true and a vetted
trafficparrot.http.externalprocess.allowedDir.
Per-mapping opt-in: list external-process in the response's
transformers array and configure it under
transformerParameters["external-process"]
(executable, args, timeoutMs, onError). |
| Middleware reads the request/response pair as JSON on stdin and writes the mutated pair on stdout | The versioned JSON envelope on stdin and
stdout. meta.contractVersion is "1"; the envelope carries the
matched request as read-only context and the response for the
middleware to mutate. |
| A Modify middleware changes the response that is about to be returned | The external-process response transformer runs on the matched stub's response,
so the middleware mutates the response section (status, headers, body) before
it is sent, the direct equivalent of Hoverfly's response-side Modify. |
| A Synthesize middleware constructs a response programmatically | Stub a minimal response in the mapping (for example an empty body and a status), then let
the middleware build the real response body, headers and status in the
envelope it returns; the transformer applies whatever the middleware writes. |
Because the middleware executable runs with the privileges of the Traffic Parrot process, the feature is off by default and is controlled by two properties in trafficparrot.properties. The External-process middleware reference covers them in full; in brief:
| Property | Default | Description |
|---|---|---|
trafficparrot.http.externalprocess.enabled |
false |
Set to true to activate the feature. While it is false, a mapping
that lists external-process is inert: the response is returned unchanged
and a warning is logged. |
trafficparrot.http.externalprocess.allowedDir |
(empty) | A single directory. Only an executable whose canonical (symlink-resolved) path is under this directory may be invoked. An empty value means nothing is allowed, even when the feature is enabled, so you must point it at a dedicated directory holding your vetted middleware. |
allowedDir once at
startup and logs a single WARN if it is empty, missing, not a directory,
non-canonical, or an existing directory that contains no files
(allowedDir contains no files: <path>; deploy the middleware executable(s) there or no
executable can ever be allowed), the case you hit if you point the property at a fresh
directory and deploy the script afterwards. This check is
warn-only and never aborts startup: a
misconfigured allow-list surfaces as a startup warning but the server still starts. Enforcement
happens per invocation: a disallowed executable is simply not run (see
Failure handling).
This is the same uppercase-body middleware shown in the reference, presented as a complete copy-pasteable migration: the script, the properties, the mapping, and the round-trip.
Save this Python script as uppercase-body.py in your allowedDir. It reads
the envelope from stdin, uppercases the response body when it is identity-encoded (leaving any
base64 binary body untouched), stamps a marker header, and writes the whole envelope back to
stdout:
#!/usr/bin/env python3
import json, sys
env = json.load(sys.stdin)
resp = env["response"]
if resp.get("bodyEncoding", "identity") == "identity":
resp["body"] = resp.get("body", "").upper()
resp.setdefault("headers", {})["X-Transformed-By"] = ["uppercase-body.py"]
json.dump(env, sys.stdout)
Make the script executable, and make sure its canonical path resolves under
allowedDir:
chmod +x /opt/tp-middleware/uppercase-body.py
Turn on the global opt-in and point allowedDir at the directory that holds the script,
in trafficparrot.properties:
trafficparrot.http.externalprocess.enabled=true
trafficparrot.http.externalprocess.allowedDir=/opt/tp-middleware
Opt the mapping in by listing external-process in the response's
transformers array and configuring it under
transformerParameters["external-process"]. Here the executable points at
the script, with no extra args, a 5-second per-invocation timeout, and
fail-open so a middleware failure falls back to the original response:
{
"request": {
"url": "/greeting",
"method": "GET"
},
"response": {
"status": 200,
"body": "hello parrot",
"headers": { "Content-Type": "text/plain" },
"transformers": ["external-process"],
"transformerParameters": {
"external-process": {
"executable": "/opt/tp-middleware/uppercase-body.py",
"args": [],
"timeoutMs": 5000,
"onError": "fail-open"
}
}
}
}
A request to the virtual service now returns the body uppercased by the middleware, with the X-Transformed-By header it stamped:
$ curl -i http://localhost:8081/greeting
HTTP/1.1 200 OK
Content-Type: text/plain
X-Transformed-By: uppercase-body.py
HELLO PARROT
The stub's hello parrot body was passed to the middleware on stdin and came back as
HELLO PARROT on stdout, and the middleware added the X-Transformed-By
response header. If the body had been base64-encoded binary, the script's
bodyEncoding check would have passed it through untouched.
The full envelope schema (version 1) is documented in the External-process middleware reference. If you are porting a Hoverfly middleware, three migration-relevant points matter:
meta.contractVersion == "1" and refuse an envelope whose major version it does not
understand, the equivalent of guarding against a Hoverfly schema change.response section only (status, headers, body). The
envelope also carries the matched request and a meta block, but any
changes a middleware makes to meta or request are ignored
in version 1: Traffic Parrot reads back only the returned response.bodyEncoding is either "identity" (UTF-8 text) or
"base64" (binary). A middleware that does not decode bodies should pass a base64
body through untouched, echoing back its bodyEncoding, exactly as
the example script does.
The external-process transformer matches the heart of Hoverfly's middleware design.
A few behaviours differ in this release; knowing them up front lets you plan a migration:
response only. The envelope still carries the matched request as
read-only context the middleware can read, but request mutation before matching
is not supported yet.meta.protocol discriminator (always "http" today) is
reserved so the same middleware contract can extend to messaging protocols later.-middleware
flag is global: one middleware for the whole instance. Traffic Parrot opts in
per mapping, so only the mappings that list external-process pay
the spawn cost, and the rest of your stubs are unaffected.
The Failure handling section of the reference is
the full description. In brief, four conditions count as a middleware failure: a
non-zero exit code, a timeout (the process is forcibly killed at
timeoutMs), malformed JSON on stdout, and a missing or
disallowed executable (including a path that does not resolve under
allowedDir). Each is logged, and the per-mapping onError setting decides
what the client receives:
fail-open (the default) returns the original, untransformed response, so the
failure is invisible to the client.fail-closed returns HTTP 500, so a broken middleware surfaces as a
failed request rather than silently passing the original response through.allowedDir allow-list is enforced per invocation: an executable
whose canonical path does not resolve under allowedDir is not run, and
onError is applied. This is distinct from the warn-only startup
validation of allowedDir: a misconfigured allow-list logs a single
startup WARN but does not abort startup, and the real protection is the
per-invocation check.
The Security and threat model section of the reference has the full guidance. The essentials a migrating user must know:
allowedDir may be invoked, so point it at a dedicated directory of vetted
scripts and keep it write-protected from untrusted users.