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.

Note: the examples below show the Java, JavaScript, Python, Ruby, Go, .NET and Rust clients using their typed before/after-action APIs. The PHP client has no typed before/after-action builder, so its examples submit the same expectation JSON via Expectation::fromArray(...) — the REST API tab (last on each example) always shows the canonical request body.

 

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.

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'] })),
    ]
  )
)
import mockserver "github.com/mock-server/mockserver-monorepo/mockserver-client-go/v7"

client := mockserver.New("localhost", 1080)
client.Upsert(mockserver.Expectation{
    HttpRequest:  &mockserver.HttpRequest{Method: "POST", Path: "/order"},
    HttpResponse: &mockserver.HttpResponse{StatusCode: 201, Body: `{ "status": "created" }`},
    AfterActions: []mockserver.AfterAction{
        {HttpRequest: &mockserver.HttpRequest{Method: "POST", Path: "/analytics",
            Headers: map[string][]string{"Host": {"analytics.svc:8080"}}}},
        {HttpRequest: &mockserver.HttpRequest{Method: "POST", Path: "/audit",
            Headers: map[string][]string{"Host": {"audit.svc:8080"}}}},
    },
})
using MockServer.Client;
using MockServer.Client.Models;

using var client = new MockServerClient("localhost", 1080);
client.Upsert(new Expectation
{
    HttpRequest = HttpRequest.Request().WithMethod("POST").WithPath("/order"),
    HttpResponse = HttpResponse.Response().WithStatusCode(201).WithBody("{ \"status\": \"created\" }"),
    AfterActions = new List<AfterAction>
    {
        new AfterAction { HttpRequest = HttpRequest.Request().WithMethod("POST").WithPath("/analytics").WithHeader("Host", "analytics.svc:8080") },
        new AfterAction { HttpRequest = HttpRequest.Request().WithMethod("POST").WithPath("/audit").WithHeader("Host", "audit.svc:8080") }
    }
});
use mockserver_client::{ClientBuilder, Expectation, ExpectationAction, HttpRequest, HttpResponse};

let client = ClientBuilder::new("localhost", 1080).build().unwrap();
client.upsert(&[
    Expectation::new(HttpRequest::new().method("POST").path("/order"))
        .respond(HttpResponse::new().status_code(201).body(r#"{ "status": "created" }"#))
        .after_action(ExpectationAction::request(
            HttpRequest::new().method("POST").path("/analytics").header("Host", "analytics.svc:8080"),
        ))
        .after_action(ExpectationAction::request(
            HttpRequest::new().method("POST").path("/audit").header("Host", "audit.svc:8080"),
        )),
]).unwrap();
use MockServer\MockServerClient;
use MockServer\Expectation;

$client = new MockServerClient('localhost', 1080);
$client->upsertExpectation(Expectation::fromArray([
    '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']]]],
    ],
]));
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" ] } } }
  ]
}
 

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.

Note: the Python and Ruby clients reuse the AfterAction class for before-actions too (placed inside before_actions) — there is no separate BeforeAction class in those clients. This is intentional, not a typo.

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'
      )
    ]
  )
)
import mockserver "github.com/mock-server/mockserver-monorepo/mockserver-client-go/v7"

blocking := true
client := mockserver.New("localhost", 1080)
client.Upsert(mockserver.Expectation{
    HttpRequest:  &mockserver.HttpRequest{Method: "GET", Path: "/account"},
    HttpResponse: &mockserver.HttpResponse{StatusCode: 200, Body: `{ "account": "ok" }`},
    BeforeActions: []mockserver.AfterAction{
        {
            HttpRequest:   &mockserver.HttpRequest{Method: "GET", Path: "/auth/check",
                Headers: map[string][]string{"Host": {"auth.svc:8080"}}},
            Blocking:      &blocking,
            Timeout:       &mockserver.Delay{TimeUnit: "SECONDS", Value: 2},
            FailurePolicy: mockserver.FailurePolicyFailFast,
        },
    },
})
using MockServer.Client;
using MockServer.Client.Models;

using var client = new MockServerClient("localhost", 1080);
client.Upsert(new Expectation
{
    HttpRequest = HttpRequest.Request().WithMethod("GET").WithPath("/account"),
    HttpResponse = HttpResponse.Response().WithStatusCode(200).WithBody("{ \"account\": \"ok\" }"),
    BeforeActions = new List<AfterAction>
    {
        new AfterAction
        {
            HttpRequest = HttpRequest.Request().WithMethod("GET").WithPath("/auth/check").WithHeader("Host", "auth.svc:8080"),
            Blocking = true,
            Timeout = new Delay { TimeUnit = TimeUnit.SECONDS, Value = 2 },
            FailurePolicy = ActionFailurePolicy.FAIL_FAST
        }
    }
});
use mockserver_client::{ClientBuilder, Delay, Expectation, ExpectationAction, HttpRequest, HttpResponse};

let client = ClientBuilder::new("localhost", 1080).build().unwrap();
client.upsert(&[
    Expectation::new(HttpRequest::new().method("GET").path("/account"))
        .respond(HttpResponse::new().status_code(200).body(r#"{ "account": "ok" }"#))
        .before_action(ExpectationAction {
            blocking: Some(true),
            timeout: Some(Delay::seconds(2)),
            failure_policy: Some("FAIL_FAST".to_string()),
            ..ExpectationAction::request(
                HttpRequest::new().method("GET").path("/auth/check").header("Host", "auth.svc:8080"),
            )
        }),
]).unwrap();
use MockServer\MockServerClient;
use MockServer\Expectation;

$client = new MockServerClient('localhost', 1080);
$client->upsertExpectation(Expectation::fromArray([
    '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',
        ],
    ],
]));
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"
    }
  ]
}
 

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'] }))
    ]
  )
)
import mockserver "github.com/mock-server/mockserver-monorepo/mockserver-client-go/v7"

blocking := true
client := mockserver.New("localhost", 1080)
client.Upsert(mockserver.Expectation{
    HttpRequest:  &mockserver.HttpRequest{Method: "GET", Path: "/account"},
    HttpResponse: &mockserver.HttpResponse{StatusCode: 200, Body: `{ "account": "ok" }`},
    BeforeActions: []mockserver.AfterAction{
        {
            HttpRequest:   &mockserver.HttpRequest{Method: "GET", Path: "/auth/check",
                Headers: map[string][]string{"Host": {"auth.svc:8080"}}},
            Blocking:      &blocking,
            Timeout:       &mockserver.Delay{TimeUnit: "SECONDS", Value: 2},
            FailurePolicy: mockserver.FailurePolicyFailFast,
        },
    },
    AfterActions: []mockserver.AfterAction{
        {HttpRequest: &mockserver.HttpRequest{Method: "POST", Path: "/audit",
            Headers: map[string][]string{"Host": {"audit.svc:8080"}}}},
    },
})
using MockServer.Client;
using MockServer.Client.Models;

using var client = new MockServerClient("localhost", 1080);
client.Upsert(new Expectation
{
    HttpRequest = HttpRequest.Request().WithMethod("GET").WithPath("/account"),
    HttpResponse = HttpResponse.Response().WithStatusCode(200).WithBody("{ \"account\": \"ok\" }"),
    BeforeActions = new List<AfterAction>
    {
        new AfterAction
        {
            HttpRequest = HttpRequest.Request().WithMethod("GET").WithPath("/auth/check").WithHeader("Host", "auth.svc:8080"),
            Blocking = true,
            Timeout = new Delay { TimeUnit = TimeUnit.SECONDS, Value = 2 },
            FailurePolicy = ActionFailurePolicy.FAIL_FAST
        }
    },
    AfterActions = new List<AfterAction>
    {
        new AfterAction { HttpRequest = HttpRequest.Request().WithMethod("POST").WithPath("/audit").WithHeader("Host", "audit.svc:8080") }
    }
});
use mockserver_client::{ClientBuilder, Delay, Expectation, ExpectationAction, HttpRequest, HttpResponse};

let client = ClientBuilder::new("localhost", 1080).build().unwrap();
client.upsert(&[
    Expectation::new(HttpRequest::new().method("GET").path("/account"))
        .respond(HttpResponse::new().status_code(200).body(r#"{ "account": "ok" }"#))
        .before_action(ExpectationAction {
            blocking: Some(true),
            timeout: Some(Delay::seconds(2)),
            failure_policy: Some("FAIL_FAST".to_string()),
            ..ExpectationAction::request(
                HttpRequest::new().method("GET").path("/auth/check").header("Host", "auth.svc:8080"),
            )
        })
        .after_action(ExpectationAction::request(
            HttpRequest::new().method("POST").path("/audit").header("Host", "audit.svc:8080"),
        )),
]).unwrap();
use MockServer\MockServerClient;
use MockServer\Expectation;

$client = new MockServerClient('localhost', 1080);
$client->upsertExpectation(Expectation::fromArray([
    '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']]]],
    ],
]));
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"
    }
  ],
  "afterActions" : [
    { "httpRequest" : { "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

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.
import mockserver "github.com/mock-server/mockserver-monorepo/mockserver-client-go/v7"

blocking := true
responder := true
client := mockserver.New("localhost", 1080)
client.Upsert(mockserver.Expectation{
    HttpRequest: &mockserver.HttpRequest{Method: "POST", Path: "/order"},
    Steps: []mockserver.ExpectationStep{
        {
            HttpRequest:   &mockserver.HttpRequest{Method: "POST", Path: "/audit",
                Headers: map[string][]string{"Host": {"audit.svc:8080"}}},
            Blocking:      &blocking,
            Timeout:       &mockserver.Delay{TimeUnit: "SECONDS", Value: 3},
            FailurePolicy: mockserver.FailurePolicyFailFast,
        },
        {
            HttpOverrideForwardedRequest: &mockserver.HttpOverrideForwardedRequest{
                RequestOverride: &mockserver.HttpRequest{Headers: map[string][]string{"X-Audited": {"true"}}},
            },
            Responder: &responder,
        },
        {
            HttpRequest: &mockserver.HttpRequest{Method: "POST", Path: "/analytics",
                Headers: map[string][]string{"Host": {"analytics.svc:8080"}}},
        },
    },
})
// When steps are present the responder step defines the primary action, so
// submit with Upsert and do not set a top-level response action.
using MockServer.Client;
using MockServer.Client.Models;

using var client = new MockServerClient("localhost", 1080);
client.Upsert(new Expectation
{
    HttpRequest = HttpRequest.Request().WithMethod("POST").WithPath("/order"),
    Steps = new List<ExpectationStep>
    {
        new ExpectationStep
        {
            HttpRequest = HttpRequest.Request().WithMethod("POST").WithPath("/audit").WithHeader("Host", "audit.svc:8080"),
            Blocking = true,
            Timeout = new Delay { TimeUnit = TimeUnit.SECONDS, Value = 3 },
            FailurePolicy = ActionFailurePolicy.FAIL_FAST
        },
        new ExpectationStep
        {
            HttpOverrideForwardedRequest = new HttpOverrideForwardedRequest
            {
                HttpRequest = HttpRequest.Request().WithHeader("X-Audited", "true")
            },
            Responder = true
        },
        new ExpectationStep
        {
            HttpRequest = HttpRequest.Request().WithMethod("POST").WithPath("/analytics").WithHeader("Host", "analytics.svc:8080")
        }
    }
});
// When steps are present the responder step defines the primary action, so
// submit with Upsert and do not set a top-level response action.
use mockserver_client::{
    ClientBuilder, Delay, Expectation, ExpectationStep,
    HttpOverrideForwardedRequest, HttpRequest,
};

let client = ClientBuilder::new("localhost", 1080).build().unwrap();
client.upsert(&[
    Expectation::new(HttpRequest::new().method("POST").path("/order"))
        .step(ExpectationStep {
            http_request: Some(
                HttpRequest::new().method("POST").path("/audit").header("Host", "audit.svc:8080"),
            ),
            blocking: Some(true),
            timeout: Some(Delay::seconds(3)),
            failure_policy: Some("FAIL_FAST".to_string()),
            ..ExpectationStep::new()
        })
        .step(ExpectationStep {
            http_override_forwarded_request: Some(
                HttpOverrideForwardedRequest::new()
                    .request_override(HttpRequest::new().header("X-Audited", "true")),
            ),
            responder: Some(true),
            ..ExpectationStep::new()
        })
        .step(ExpectationStep {
            http_request: Some(
                HttpRequest::new().method("POST").path("/analytics").header("Host", "analytics.svc:8080"),
            ),
            ..ExpectationStep::new()
        }),
]).unwrap();
// When steps are present the responder step defines the primary action, so
// submit with upsert and do not set a top-level response action.
use MockServer\MockServerClient;
use MockServer\Expectation;

$client = new MockServerClient('localhost', 1080);
$client->upsertExpectation(Expectation::fromArray([
    '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']]],
        ],
    ],
]));
// When steps is set the responder step defines the primary action, so do not
// also set a top-level response action — the server rejects that combination.
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" ] } }
    }
  ]
}

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.