From 00270814a1d9b8c9b31edb3e28cdaf5faaeba0ca Mon Sep 17 00:00:00 2001 From: scovl Date: Thu, 9 Apr 2026 20:42:42 -0300 Subject: [PATCH] docs(postserveaction): document remote mode for long-lived post-serve actions Add sections explaining the difference between local (subprocess per request) and remote (long-lived HTTP server) execution modes, including: - When to use each mode and why local does not scale under high load - Step-by-step guide: writing the server, payload format, configuring Hoverfly via flag and hoverctl, verifying registration via admin API - Minimal Python server example with inline comments - Docker Compose example with service hostname Closes #1188 --- docs/pages/keyconcepts/postserveaction.rst | 148 +++++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/docs/pages/keyconcepts/postserveaction.rst b/docs/pages/keyconcepts/postserveaction.rst index 7b0c8acff..6dc2cb866 100644 --- a/docs/pages/keyconcepts/postserveaction.rst +++ b/docs/pages/keyconcepts/postserveaction.rst @@ -39,4 +39,152 @@ Ways to register a Post Serve Action } +Choosing Between Local and Remote Execution +-------------------------------------------- + +Hoverfly supports two execution modes for post-serve actions. Understanding the +difference is important when running under significant load. + +**Local execution** + +Hoverfly forks a new subprocess for every request that triggers the action. The +binary or script is re-executed from scratch each time: + +.. code:: + + hoverfly -post-serve-action "my-action python3 /path/to/script.py 0" + +This is the easiest option to get started, but it does not scale. At high request +rates (e.g. 40+ rps with a Python script), you will quickly accumulate dozens of +concurrent processes — each with its own interpreter startup cost, memory footprint, +and open connections. This commonly leads to OOMKilled in containerised environments. + +**Remote execution (recommended for high throughput)** + +Instead of forking a subprocess, Hoverfly makes an HTTP ``POST`` request to a +server that you run separately. That server stays alive indefinitely — only one +process, shared across all requests: + +.. code:: + + hoverfly -post-serve-action "my-action http://localhost:8080/trigger 0" + +This means you can use async frameworks such as ``aiohttp`` or ``FastAPI`` in their +natural form: one running event loop handling many concurrent webhooks efficiently, +without paying the startup cost on every request. + +Running a Remote Post Serve Action: Step by Step +-------------------------------------------------- + +**Step 1 — Write your server** + +Your server must accept HTTP ``POST`` requests and return ``HTTP 200``. Hoverfly +considers any other status code a failure and logs an error. Here is a minimal +Python example: + +.. code:: python + + # server.py + from http.server import BaseHTTPRequestHandler, HTTPServer + import json + + class Handler(BaseHTTPRequestHandler): + def do_POST(self): + length = int(self.headers.get("Content-Length", 0)) + payload = json.loads(self.rfile.read(length)) + + # payload contains two keys: "request" and "response" + # use them to implement your webhook/callback logic + print(f"[action] path={payload['request']['path']}") + + self.send_response(200) + self.end_headers() + + def log_message(self, format, *args): + pass # suppress default access log noise + + if __name__ == "__main__": + print("Listening on :8080") + HTTPServer(("", 8080), Handler).serve_forever() + +Start it: + +.. code:: + + python3 server.py + +**Step 2 — Understand the payload Hoverfly sends** + +On every matching request, Hoverfly POSTs a JSON body to your server containing +the full request-response pair: + +.. code:: json + + { + "request": { + "path": [{"matcher": "exact", "value": "/api/orders"}], + "method": [{"matcher": "exact", "value": "POST"}], + "destination": [{"matcher": "exact", "value": "example.com"}], + "body": [{"matcher": "exact", "value": ""}], + "headers": {} + }, + "response": { + "status": 200, + "body": "Hello World", + "encodedBody": false + } + } + +Your server can read any field from this payload to drive its logic — for example, +extracting an order ID from the request body to send a webhook. + +**Step 3 — Configure Hoverfly to call your server** + +Pass the remote URL as the second token in ``-post-serve-action``: + +.. code:: + + hoverfly -post-serve-action "my-action http://localhost:8080 0" -import simulation.json + +The format is: ``" "``. + +- ``my-action`` must match the ``postServeAction`` field in your simulation JSON. +- The URL must be reachable from Hoverfly at runtime. +- The delay (in milliseconds) is applied before Hoverfly calls the endpoint. + +Alternatively, register it at runtime via hoverctl: + +.. code:: + + hoverctl post-serve-action set --name my-action --remote http://localhost:8080 --delay 0 + +**Step 4 — Confirm the action is registered** + +.. code:: + + curl http://localhost:8888/api/v2/hoverfly/post-serve-action + +You should see your action listed with its remote URL. + +**Running in Docker Compose** + +When both Hoverfly and your action server run as containers, use the service name +as the hostname. Make sure Hoverfly starts after the action server is ready: + +.. code:: yaml + + services: + hoverfly: + image: spectolabs/hoverfly + command: > + -post-serve-action "my-action http://postaction:8080 0" + -import /app/simulation.json + depends_on: + - postaction + + postaction: + build: ./postaction-server + ports: + - "8080:8080" +