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 writtenafterActions (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:

  1. The audit webhook runs first (blocking, fail-fast — if it fails, the client gets 502).
  2. The forward-replace responder proxies the request upstream with an added header, and its response is returned to the client.
  3. 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.