Before & After Actions
An expectation normally has a single primary action — the response (or forward) sent back to the client. Before-actions and after-actions let an expectation also fire extra HTTP webhooks or callbacks as side-effects, either before or after that primary response.
This is useful when a request should do more than produce one response — for example mirroring traffic to a second service, fanning a request out to several systems, or calling a downstream dependency and only responding if that call succeeds.
When to use them
| Pattern | Use | Which list |
|---|---|---|
| Tee / mirror | Copy each matched request to another service while still returning the mocked response to the client. | afterActions |
| Shadow | Send traffic to a new implementation in the background to compare behaviour, without affecting the client. | afterActions |
| Fan-out | Notify several downstream systems for a single incoming request. | afterActions (or non-blocking beforeActions) |
| Gate / precondition | Call a downstream dependency first and only return the primary response if it succeeds. | blocking beforeActions |
Before vs after
- After-actions run after the primary response has been written to the client. They are always fire-and-forget: their own responses are discarded and any failures are only logged, so they never change or delay the response the client receives.
- Before-actions run before the primary response. By default they block the response until they complete, so they can be used as a precondition — and they can abort the response entirely if they fail.
For a matched request the order is: beforeActions (may block or gate the response) → the primary action (response or forward) → the client response is written → afterActions (fire-and-forget, after the response). After-actions run only once the response has been written, so they cannot change or delay what the client receives.
Action shape
Both beforeActions and afterActions accept either a single action object or an array of them. Each action specifies exactly one of the following targets, plus an optional delay:
| Field | Description |
|---|---|
| httpRequest | An HTTP request (webhook) that MockServer sends. This is the only target that a before-action can block on. |
| httpClassCallback | A class callback invoked for the request (fire-and-forget). |
| httpObjectCallback | A WebSocket / object callback to a connected client (fire-and-forget). |
| delay | Optional delay before the action runs. |
Before-action controls
Before-actions support three extra optional fields. These are meaningful only for before-actions and are ignored on after-actions.
| Field | Type | Default | Description |
|---|---|---|---|
| blocking | boolean | true | When true the response waits for the action to complete; when false the action is started before the response but not waited for. |
| timeout | Delay object — { "timeUnit": …, "value": … } | server maxSocketTimeout | Maximum time to wait for a blocking action, expressed as a structured Delay (a timeUnit plus a value), not a bare number. If it elapses the action is treated as failed. |
| failurePolicy | string enum | BEST_EFFORT | BEST_EFFORT — log the failure and continue to the primary response. FAIL_FAST — abort and return 502 Bad Gateway (the primary action does not run). |
Only webhook before-actions can block. A httpClassCallback or httpObjectCallback before-action is always dispatched fire-and-forget, even if blocking is true (MockServer logs a warning). Use a httpRequest before-action when you need the response to wait or to gate on success.
Example — fan-out with after-actions
Return a mocked response to the client and also mirror the request to two other services in the background.
PUT /mockserver/expectation HTTP/1.1
Content-Type: application/json
{
"httpRequest" : {
"method" : "POST",
"path" : "/order"
},
"httpResponse" : {
"statusCode" : 201,
"body" : "{ \"status\": \"created\" }"
},
"afterActions" : [
{ "httpRequest" : { "method" : "POST", "path" : "/analytics", "headers" : { "Host" : [ "analytics.svc:8080" ] } } },
{ "httpRequest" : { "method" : "POST", "path" : "/audit", "headers" : { "Host" : [ "audit.svc:8080" ] } } }
]
}
new MockServerClient("localhost", 1080)
.when(
request()
.withMethod("POST")
.withPath("/order")
)
.withAfterAction(
afterAction().withHttpRequest(request().withMethod("POST").withPath("/analytics").withHeader("Host", "analytics.svc:8080"))
)
.withAfterAction(
afterAction().withHttpRequest(request().withMethod("POST").withPath("/audit").withHeader("Host", "audit.svc:8080"))
)
.respond(
response()
.withStatusCode(201)
.withBody("{ \"status\": \"created\" }")
);
var mockServerClient = require('mockserver-client').mockServerClient;
mockServerClient("localhost", 1080).mockAnyResponse({
"httpRequest": {
"method": "POST",
"path": "/order"
},
"httpResponse": {
"statusCode": 201,
"body": "{ \"status\": \"created\" }"
},
"afterActions": [
{ "httpRequest": { "method": "POST", "path": "/analytics", "headers": { "Host": [ "analytics.svc:8080" ] } } },
{ "httpRequest": { "method": "POST", "path": "/audit", "headers": { "Host": [ "audit.svc:8080" ] } } }
]
}).then(
function () { console.log("expectation created"); },
function (error) { console.log(error); }
);
from mockserver import (
MockServerClient, AfterAction, Expectation, HttpRequest, HttpResponse
)
client = MockServerClient("localhost", 1080)
client.upsert(
Expectation(
http_request=HttpRequest(method="POST", path="/order"),
http_response=HttpResponse(status_code=201, body='{ "status": "created" }'),
after_actions=[
AfterAction(http_request=HttpRequest(method="POST", path="/analytics",
headers={"Host": ["analytics.svc:8080"]})),
AfterAction(http_request=HttpRequest(method="POST", path="/audit",
headers={"Host": ["audit.svc:8080"]})),
],
)
)
require 'mockserver-client'
include MockServer
client = MockServer::Client.new('localhost', 1080)
client.upsert(
Expectation.new(
http_request: HttpRequest.new(method: 'POST', path: '/order'),
http_response: HttpResponse.new(status_code: 201, body: '{ "status": "created" }'),
after_actions: [
AfterAction.new(http_request: HttpRequest.new(method: 'POST', path: '/analytics',
headers: { 'Host' => ['analytics.svc:8080'] })),
AfterAction.new(http_request: HttpRequest.new(method: 'POST', path: '/audit',
headers: { 'Host' => ['audit.svc:8080'] })),
]
)
)
Example — gate with a blocking before-action
Call a downstream dependency first; if it fails or takes longer than two seconds, return 502 instead of the mocked response.
PUT /mockserver/expectation HTTP/1.1
Content-Type: application/json
{
"httpRequest" : {
"method" : "GET",
"path" : "/account"
},
"httpResponse" : {
"statusCode" : 200,
"body" : "{ \"account\": \"ok\" }"
},
"beforeActions" : [
{
"httpRequest" : { "method" : "GET", "path" : "/auth/check", "headers" : { "Host" : [ "auth.svc:8080" ] } },
"blocking" : true,
"timeout" : { "timeUnit" : "SECONDS", "value" : 2 },
"failurePolicy" : "FAIL_FAST"
}
]
}
new MockServerClient("localhost", 1080)
.when(
request()
.withMethod("GET")
.withPath("/account")
)
.withBeforeAction(
beforeAction()
.withHttpRequest(request().withMethod("GET").withPath("/auth/check").withHeader("Host", "auth.svc:8080"))
.withBlocking(true)
.withTimeout(seconds(2))
.withFailurePolicy(FailurePolicy.FAIL_FAST)
)
.respond(
response()
.withStatusCode(200)
.withBody("{ \"account\": \"ok\" }")
);
var mockServerClient = require('mockserver-client').mockServerClient;
mockServerClient("localhost", 1080).mockAnyResponse({
"httpRequest": {
"method": "GET",
"path": "/account"
},
"httpResponse": {
"statusCode": 200,
"body": "{ \"account\": \"ok\" }"
},
"beforeActions": [
{
"httpRequest": { "method": "GET", "path": "/auth/check", "headers": { "Host": [ "auth.svc:8080" ] } },
"blocking": true,
"timeout": { "timeUnit": "SECONDS", "value": 2 },
"failurePolicy": "FAIL_FAST"
}
]
}).then(
function () { console.log("expectation created"); },
function (error) { console.log(error); }
);
from mockserver import (
MockServerClient, AfterAction, Delay, Expectation, HttpRequest, HttpResponse
)
client = MockServerClient("localhost", 1080)
client.upsert(
Expectation(
http_request=HttpRequest(method="GET", path="/account"),
http_response=HttpResponse(status_code=200, body='{ "account": "ok" }'),
before_actions=[
AfterAction(
http_request=HttpRequest(method="GET", path="/auth/check",
headers={"Host": ["auth.svc:8080"]}),
blocking=True,
timeout=Delay(time_unit="SECONDS", value=2),
failure_policy="FAIL_FAST",
)
],
)
)
require 'mockserver-client'
include MockServer
client = MockServer::Client.new('localhost', 1080)
client.upsert(
Expectation.new(
http_request: HttpRequest.new(method: 'GET', path: '/account'),
http_response: HttpResponse.new(status_code: 200, body: '{ "account": "ok" }'),
before_actions: [
AfterAction.new(
http_request: HttpRequest.new(method: 'GET', path: '/auth/check',
headers: { 'Host' => ['auth.svc:8080'] }),
blocking: true,
timeout: Delay.new(time_unit: 'SECONDS', value: 2),
failure_policy: 'FAIL_FAST'
)
]
)
)
Example — combined gate and mirror
Gate on an auth check before responding, and mirror the request to an audit service after.
new MockServerClient("localhost", 1080)
.when(
request()
.withMethod("GET")
.withPath("/account")
)
.withBeforeAction(
beforeAction()
.withHttpRequest(request().withMethod("GET").withPath("/auth/check").withHeader("Host", "auth.svc:8080"))
.withBlocking(true)
.withTimeout(seconds(2))
.withFailurePolicy(FailurePolicy.FAIL_FAST)
)
.withAfterAction(
afterAction().withHttpRequest(request().withMethod("POST").withPath("/audit").withHeader("Host", "audit.svc:8080"))
)
.respond(
response()
.withStatusCode(200)
.withBody("{ \"account\": \"ok\" }")
);
var mockServerClient = require('mockserver-client').mockServerClient;
mockServerClient("localhost", 1080).mockAnyResponse({
"httpRequest": {
"method": "GET",
"path": "/account"
},
"httpResponse": {
"statusCode": 200,
"body": "{ \"account\": \"ok\" }"
},
"beforeActions": [
{
"httpRequest": { "method": "GET", "path": "/auth/check", "headers": { "Host": [ "auth.svc:8080" ] } },
"blocking": true,
"timeout": { "timeUnit": "SECONDS", "value": 2 },
"failurePolicy": "FAIL_FAST"
}
],
"afterActions": [
{ "httpRequest": { "method": "POST", "path": "/audit", "headers": { "Host": [ "audit.svc:8080" ] } } }
]
}).then(
function () { console.log("expectation created"); },
function (error) { console.log(error); }
);
from mockserver import (
MockServerClient, AfterAction, Delay, Expectation, HttpRequest, HttpResponse
)
client = MockServerClient("localhost", 1080)
client.upsert(
Expectation(
http_request=HttpRequest(method="GET", path="/account"),
http_response=HttpResponse(status_code=200, body='{ "account": "ok" }'),
before_actions=[
AfterAction(
http_request=HttpRequest(method="GET", path="/auth/check",
headers={"Host": ["auth.svc:8080"]}),
blocking=True,
timeout=Delay(time_unit="SECONDS", value=2),
failure_policy="FAIL_FAST",
)
],
after_actions=[
AfterAction(http_request=HttpRequest(method="POST", path="/audit",
headers={"Host": ["audit.svc:8080"]})),
],
)
)
require 'mockserver-client'
include MockServer
client = MockServer::Client.new('localhost', 1080)
client.upsert(
Expectation.new(
http_request: HttpRequest.new(method: 'GET', path: '/account'),
http_response: HttpResponse.new(status_code: 200, body: '{ "account": "ok" }'),
before_actions: [
AfterAction.new(
http_request: HttpRequest.new(method: 'GET', path: '/auth/check',
headers: { 'Host' => ['auth.svc:8080'] }),
blocking: true,
timeout: Delay.new(time_unit: 'SECONDS', value: 2),
failure_policy: 'FAIL_FAST'
)
],
after_actions: [
AfterAction.new(http_request: HttpRequest.new(method: 'POST', path: '/audit',
headers: { 'Host' => ['audit.svc:8080'] }))
]
)
)
Using values from the triggering request
A webhook's fields can reference values from the request that triggered the expectation using {$request.…} runtime expressions, which are resolved before the webhook is sent. Supported expressions include {$url}, {$request.method}, {$request.header.NAME}, {$request.query.NAME}, {$request.path.NAME}, and {$request.body#/json/pointer}. Unknown or missing values resolve to an empty string. These are the same expressions used by OpenAPI callbacks.
Unified ordered steps
For more complex pipelines, the steps field provides a unified, ordered alternative to separate beforeActions, a primary action, and afterActions. A steps list declares an ordered sequence of actions where exactly one step is marked as the responder (the action that produces the HTTP response), and the remaining steps are side-effects.
Steps that appear before the responder behave like before-actions (they can block, timeout, and gate the response). Steps that appear after the responder behave like after-actions (fire-and-forget).
When to use steps instead of beforeActions/afterActions
- When you want a single ordered list that makes the pipeline sequence explicit.
- When the responder is a forward (proxy) and you also want side-effect forwards, all in one ordered list.
- When you want side-effects both before and after the response in a single declaration.
Backward compatibility: existing expectations using beforeActions and/or afterActions with a top-level primary action work unchanged. The steps field is an alternative — you do not need to migrate.
Step shape
Each step specifies exactly one action target:
| Field | Can be responder? | Description |
|---|---|---|
| httpResponse | Yes | Static or templated response. |
| httpForward | Yes | Proxy forward. As a side-effect, the upstream response is discarded. |
| httpOverrideForwardedRequest | Yes | Forward with request/response overrides. |
| httpClassCallback | Yes | Server-side class callback. |
| httpObjectCallback | Yes | WebSocket object callback. |
| httpRequest | No (side-effect only) | Webhook — fires an HTTP request. This is the only target that can block in a pre-responder step. |
| httpError | Yes (solo only) | Connection-level error. Must be the only step — cannot combine with other steps. |
Each step also supports:
- responder (boolean) — exactly one step must have responder: true.
- delay — delay before the step runs.
- blocking, timeout, failurePolicy — for pre-responder side-effect steps (same semantics as before-action controls).
Validation rules
- Exactly one step must be the responder (responder: true).
- Each step must have exactly one action target.
- httpError cannot be combined with other steps — it must be the only step.
- httpRequest (webhook) cannot be a responder — it is side-effect-only.
- steps cannot be combined with beforeActions — use steps for the full ordered pipeline.
- steps cannot be combined with a top-level response action (e.g. httpResponse, httpForward) — the responder step defines the action.
- steps can coexist with afterActions — after-actions fire after the entire steps pipeline completes.
Invalid combinations are rejected at expectation creation time with a clear error message.
Example — webhook then forward-replace responder
PUT /mockserver/expectation HTTP/1.1
Content-Type: application/json
{
"httpRequest" : {
"method" : "POST",
"path" : "/order"
},
"steps" : [
{
"httpRequest" : { "method" : "POST", "path" : "/audit", "headers" : { "Host" : [ "audit.svc:8080" ] } },
"blocking" : true,
"timeout" : { "timeUnit" : "SECONDS", "value" : 3 },
"failurePolicy" : "FAIL_FAST"
},
{
"httpOverrideForwardedRequest" : {
"requestOverride" : { "headers" : { "X-Audited" : [ "true" ] } }
},
"responder" : true
},
{
"httpRequest" : { "method" : "POST", "path" : "/analytics", "headers" : { "Host" : [ "analytics.svc:8080" ] } }
}
]
}
In this pipeline:
- The audit webhook runs first (blocking, fail-fast — if it fails, the client gets 502).
- The forward-replace responder proxies the request upstream with an added header, and its response is returned to the client.
- The analytics webhook runs after the response as a fire-and-forget side-effect.
import static org.mockserver.model.ExpectationStep.step;
new MockServerClient("localhost", 1080)
.when(
request()
.withMethod("POST")
.withPath("/order")
)
.withSteps(
step()
.withHttpRequest(request().withMethod("POST").withPath("/audit").withHeader("Host", "audit.svc:8080"))
.withBlocking(true)
.withTimeout(seconds(3))
.withFailurePolicy(FailurePolicy.FAIL_FAST),
step()
.withHttpOverrideForwardedRequest(
forwardOverriddenRequest()
.withRequestOverride(request().withHeader("X-Audited", "true"))
)
.withResponder(true),
step()
.withHttpRequest(request().withMethod("POST").withPath("/analytics").withHeader("Host", "analytics.svc:8080"))
)
.upsert();
// When steps are present, the responder step defines the primary action.
// Use .upsert() to submit the expectation without setting a redundant
// top-level action (the server rejects steps + a top-level action).
var mockServerClient = require('mockserver-client').mockServerClient;
mockServerClient("localhost", 1080).mockAnyResponse({
"httpRequest": {
"method": "POST",
"path": "/order"
},
"steps": [
{
"httpRequest": { "method": "POST", "path": "/audit", "headers": { "Host": [ "audit.svc:8080" ] } },
"blocking": true,
"timeout": { "timeUnit": "SECONDS", "value": 3 },
"failurePolicy": "FAIL_FAST"
},
{
"httpOverrideForwardedRequest": {
"requestOverride": { "headers": { "X-Audited": [ "true" ] } }
},
"responder": true
},
{
"httpRequest": { "method": "POST", "path": "/analytics", "headers": { "Host": [ "analytics.svc:8080" ] } }
}
]
}).then(
function () { console.log("expectation created"); },
function (error) { console.log(error); }
);
from mockserver import (
MockServerClient, Delay, Expectation, ExpectationStep,
HttpOverrideForwardedRequest, HttpRequest
)
client = MockServerClient("localhost", 1080)
client.upsert(
Expectation(
http_request=HttpRequest(method="POST", path="/order"),
steps=[
ExpectationStep(
http_request=HttpRequest(method="POST", path="/audit",
headers={"Host": ["audit.svc:8080"]}),
blocking=True,
timeout=Delay(time_unit="SECONDS", value=3),
failure_policy="FAIL_FAST",
),
ExpectationStep(
http_override_forwarded_request=HttpOverrideForwardedRequest(
request_override=HttpRequest(headers={"X-Audited": ["true"]})
),
responder=True,
),
ExpectationStep(
http_request=HttpRequest(method="POST", path="/analytics",
headers={"Host": ["analytics.svc:8080"]}),
),
],
)
)
# When steps is set the responder step defines the primary action.
# Do not also set http_response / http_forward on the Expectation — the server
# rejects an expectation that combines steps with a top-level response action.
require 'mockserver-client'
include MockServer
client = MockServer::Client.new('localhost', 1080)
client.when(
HttpRequest.new(method: 'POST', path: '/order')
).with_steps([
ExpectationStep.new(
http_request: HttpRequest.new(method: 'POST', path: '/audit',
headers: { 'Host' => ['audit.svc:8080'] }),
blocking: true,
timeout: Delay.new(time_unit: 'SECONDS', value: 3),
failure_policy: 'FAIL_FAST'
),
ExpectationStep.new(
http_override_forwarded_request: HttpOverrideForwardedRequest.new(
request_override: HttpRequest.new(headers: { 'X-Audited' => ['true'] })
),
responder: true
),
ExpectationStep.new(
http_request: HttpRequest.new(method: 'POST', path: '/analytics',
headers: { 'Host' => ['analytics.svc:8080'] })
)
])
# When steps is set the responder step defines the primary action.
# Do not also call .respond() or .forward() — the server rejects steps
# combined with a top-level response action.
Note: When steps is present, the primary action is determined by the responder step. Use .upsert() to submit the expectation without setting a redundant top-level action. Using .respond() or .forward() would set a top-level action that conflicts with the steps pipeline, and the server would reject the expectation. afterActions are still allowed alongside steps and fire after the entire steps pipeline completes.
Notes
- An expectation without beforeActions, afterActions, or steps behaves exactly as before — these fields are fully optional and backward compatible.
- After-actions never affect the client response; even on a FAIL_FAST before-action abort, the 502 counts as the response and any after-actions still fire.
- Webhook responses are discarded — these actions are about side-effects, not about composing the client response.
- The steps field is an alternative to beforeActions + a top-level primary action. When steps is present it determines the dispatch pipeline; the responder step's action becomes the primary action. steps cannot be combined with beforeActions or a top-level response action (the server rejects such expectations), but afterActions still fire after the steps pipeline completes.