# Hoverfly middleware in Traffic Parrot

## Coming from Hoverfly?

Traffic Parrot is a standalone mock and service virtualization server that is [compatible with WireMock](https://trafficparrot.com/features.html) 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](https://trafficparrot.com/documentation/5.65.x/dynamic.html#external-process-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](https://trafficparrot.com/documentation/5.65.x/dynamic.html#external-process-transformer) 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.

## Mapping Hoverfly concepts onto Traffic Parrot

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`](https://trafficparrot.com/documentation/5.65.x/properties.html#extension-system-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](https://trafficparrot.com/documentation/5.65.x/dynamic.html#external-process-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. |

## Enabling the feature

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`](https://trafficparrot.com/documentation/5.65.x/properties.html#extension-system-properties). The [External-process middleware reference](https://trafficparrot.com/documentation/5.65.x/dynamic.html#external-process-transformer) 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. |

> When the feature is enabled, Traffic Parrot validates `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](https://trafficparrot.com/documentation/5.65.x/hoverfly-middleware.html#failure-handling)).

## A working example

This is the same uppercase-body middleware shown in the [reference](https://trafficparrot.com/documentation/5.65.x/dynamic.html#external-process-transformer), presented as a complete copy-pasteable migration: the script, the properties, the mapping, and the round-trip.

##### 1. The middleware script

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:

```python
#!/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`:

```bash
chmod +x /opt/tp-middleware/uppercase-body.py
```

##### 2. Enable the feature

Turn on the global opt-in and point `allowedDir` at the directory that holds the script, in [`trafficparrot.properties`](https://trafficparrot.com/documentation/5.65.x/properties.html#extension-system-properties):

```properties
trafficparrot.http.externalprocess.enabled=true
trafficparrot.http.externalprocess.allowedDir=/opt/tp-middleware
```

##### 3. Configure the mapping

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:

```json
{
  "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"
      }
    }
  }
}
```

##### 4. The round-trip

A request to the virtual service now returns the body uppercased by the middleware, with the `X-Transformed-By` header it stamped:

```bash
$ 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 JSON envelope

The full envelope schema (version 1) is documented in the [External-process middleware reference](https://trafficparrot.com/documentation/5.65.x/dynamic.html#external-process-envelope). If you are porting a Hoverfly middleware, three migration-relevant points matter:

- The envelope is **versioned**. Your middleware should check that `meta.contractVersion == "1"` and refuse an envelope whose major version it does not understand, the equivalent of guarding against a Hoverfly schema change.
- Mutate the **`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.

## Differences from Hoverfly in this release

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.** Hoverfly middleware can mutate the request before matching; Traffic Parrot's transformer runs on the matched stub's response, so it mutates the `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.
- **One process per matched request.** Like Hoverfly's per-pair invocation, Traffic Parrot spawns a fresh process for every matched request on a mapping that uses the transformer. The dominant cost is interpreter/runtime startup (roughly 30 to 50 ms for a Python script), so a lightweight script keeps the per-request overhead small; there is no long-lived middleware pool in this release.
- **HTTP only.** The transformer applies to HTTP responses in this release. The envelope's `meta.protocol` discriminator (always `"http"` today) is reserved so the same middleware contract can extend to messaging protocols later.
- **Per-mapping opt-in, framed as a benefit.** Hoverfly's `-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.

## Failure handling

The [Failure handling](https://trafficparrot.com/documentation/5.65.x/dynamic.html#external-process-failure) 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.

> The `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.

## Security and threat model

The [Security and threat model](https://trafficparrot.com/documentation/5.65.x/dynamic.html#external-process-security) section of the reference has the full guidance. The essentials a migrating user must know:

- The feature is **off by default** and must be explicitly enabled.
- Only an executable whose **canonical path** resolves under the operator-configured `allowedDir` may be invoked, so point it at a dedicated directory of vetted scripts and keep it write-protected from untrusted users.
- The middleware runs as a native OS process with the privileges of the Traffic Parrot user and **no sandbox**. Run Traffic Parrot as an **unprivileged OS user** and apply operating-system resource limits (for example `ulimit` or cgroups) to bound a runaway middleware; Traffic Parrot does not impose these itself.

## Next steps

- [External-process middleware reference](https://trafficparrot.com/documentation/5.65.x/dynamic.html#external-process-transformer): the full description of the transformer, covering enabling, configuring a mapping, the [envelope schema](https://trafficparrot.com/documentation/5.65.x/dynamic.html#external-process-envelope), [failure handling](https://trafficparrot.com/documentation/5.65.x/dynamic.html#external-process-failure) and the [security model](https://trafficparrot.com/documentation/5.65.x/dynamic.html#external-process-security).
- [Configuration Reference](https://trafficparrot.com/documentation/5.65.x/properties.html): every configurable property, including the [extension system properties](https://trafficparrot.com/documentation/5.65.x/properties.html#extension-system-properties) that enable this feature.
- [Traffic Parrot for WireMock users](https://trafficparrot.com/features.html): how Traffic Parrot relates to WireMock, including stub compatibility.
