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.

In Java there is no "append one action" call — every withBeforeAction.../withAfterAction... method replaces the whole list. withBeforeAction(AfterAction) / withAfterAction(AfterAction) (singular) are plain convenience wrappers that forward to the plural varargs form with one argument; calling the singular method twice does not accumulate two actions — the second call's single-element list replaces the first. To register more than one before- or after-action, pass them all in a single call to the plural form, withBeforeActions(AfterAction...) / withAfterActions(AfterAction...). The fluent client chain (mockServerClient.when(...)..., i.e. ForwardChainExpectation) only has this varargs form; a List<AfterAction> overload exists separately on the Expectation model object itself (useful when the list is built conditionally elsewhere, but not reachable through the fluent chain):

expectation.withAfterActions(mirrorAction, auditAction); // the only way to register both in Java
 

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. Webhooks are the only targets that a before-action can block on.
httpRequestTemplate A webhook whose request is rendered by a Velocity, Mustache or JavaScript template from the triggering request. See Rendering a webhook with a template.
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 or httpRequestTemplate 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")
    )
    .withAfterActions(
        afterAction().withHttpRequest(request().withMethod("POST").withPath("/analytics").withHeader("Host", "analytics.svc:8080")),
        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/v9"

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/v9"

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/v9"

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.

 

Rendering a webhook with a template

When runtime expressions are not enough, for example to build a JSON body, branch on a value or use template helpers such as dates, UUIDs or JWTs, use httpRequestTemplate instead of httpRequest. It takes the same templateType (VELOCITY, MUSTACHE or JAVASCRIPT) and template or templateFile as a response template, and the template sees the triggering request as request with the same request variables and helpers. The template must render a whole HTTP request in MockServer's JSON format, including a Host header naming where to send it.

  • The rendered request is sent exactly like an httpRequest webhook: the same delay, blocking, timeout and failurePolicy apply, and, when forwardProxyBlockPrivateNetworks is enabled (it is off by default), the rendered destination is refused if it is a private, loopback or link-local address.
  • A template that fails, or does not render a valid request, is logged and nothing is sent. For a blocking before-action that counts as a failure, so FAIL_FAST returns 502 Bad Gateway.
  • Templates run with the same time limits and class restrictions as response templates. In the standalone server they run on background worker threads, so rendering never delays the response of an after-action.
  • {$request…} runtime expressions are not applied to a templated webhook; use the template's own variables instead.
  • Only the triggering request is available to the template, not the response sent to the client.
  • The template's own delay, responseOverride and responseModifier are ignored; use the action's delay.

Warning: escape request values, and keep them out of the destination. The template's output is parsed as the webhook request, so an unescaped request value can add or change fields, for example a socketAddress or Host that sends the webhook, with any credentials in its headers, to a server the caller chooses. In Velocity escape values with $esc.java(…) inside JSON strings (its escapes are valid JSON) and $esc.url(…) in the path (it form-encodes, so a space becomes +; validate IDs used in paths). Mustache HTML-escapes {{ … }} values, which stops a quote ending a JSON string but not a trailing backslash, so prefer Velocity or JavaScript for values taken from the request. A JavaScript template returns an object, so request values cannot change its structure. In every engine, never build the Host, port, socketAddress or URL from request data unless you trust every caller. If webhook destinations can come from request data, enable forwardProxyBlockPrivateNetworks (it is off by default) so private, loopback and cloud-metadata addresses are refused; it never blocks public destinations.

new MockServerClient("localhost", 1080)
    .when(
        request()
            .withMethod("POST")
            .withPath("/order")
    )
    .withAfterActions(
        afterAction().withHttpRequestTemplate(
            template(
                HttpTemplate.TemplateType.VELOCITY,
                "{" +
                "  \"method\": \"POST\"," +
                "  \"path\": \"/orders/$!esc.url($request.headers['x-order-id'][0])/created\"," +
                "  \"headers\": { \"Host\": [ \"orders.svc:8080\" ], \"Content-Type\": [ \"application/json\" ] }," +
                "  \"body\": { \"received\": \"$!esc.java($request.body)\", \"at\": \"$!now_iso_8601\" }" +
                "}"
            )
        )
    )
    .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": [
        {
            "httpRequestTemplate": {
                "templateType": "VELOCITY",
                "template": "{ \"method\": \"POST\", \"path\": \"/orders/$!esc.url($request.headers['x-order-id'][0])/created\", \"headers\": { \"Host\": [ \"orders.svc:8080\" ], \"Content-Type\": [ \"application/json\" ] }, \"body\": { \"received\": \"$!esc.java($request.body)\", \"at\": \"$!now_iso_8601\" } }"
            }
        }
    ]
}).then(
    function () { console.log("expectation created"); },
    function (error) { console.log(error); }
);
from mockserver import (
    MockServerClient, AfterAction, Expectation, HttpRequest, HttpResponse, HttpTemplate
)

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_template=HttpTemplate(
                template_type="VELOCITY",
                template=r'''{ "method": "POST", "path": "/orders/$!esc.url($request.headers['x-order-id'][0])/created", "headers": { "Host": [ "orders.svc:8080" ], "Content-Type": [ "application/json" ] }, "body": { "received": "$!esc.java($request.body)", "at": "$!now_iso_8601" } }''',
            )),
        ],
    )
)
require 'mockserver-client'
include MockServer

template = <<~'VTL'.chomp
  { "method": "POST", "path": "/orders/$!esc.url($request.headers['x-order-id'][0])/created", "headers": { "Host": [ "orders.svc:8080" ], "Content-Type": [ "application/json" ] }, "body": { "received": "$!esc.java($request.body)", "at": "$!now_iso_8601" } }
VTL

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_template: HttpTemplate.new(template_type: 'VELOCITY', template: template))
    ]
  )
)
import mockserver "github.com/mock-server/mockserver-monorepo/mockserver-client-go/v9"

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{
        {HttpRequestTemplate: &mockserver.HttpTemplate{
            TemplateType: "VELOCITY",
            Template:     `{ "method": "POST", "path": "/orders/$!esc.url($request.headers['x-order-id'][0])/created", "headers": { "Host": [ "orders.svc:8080" ], "Content-Type": [ "application/json" ] }, "body": { "received": "$!esc.java($request.body)", "at": "$!now_iso_8601" } }`,
        }},
    },
})
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
        {
            HttpRequestTemplate = new HttpTemplate
            {
                TemplateType = TemplateType.VELOCITY,
                Template = @"{ ""method"": ""POST"", ""path"": ""/orders/$!esc.url($request.headers['x-order-id'][0])/created"", ""headers"": { ""Host"": [ ""orders.svc:8080"" ], ""Content-Type"": [ ""application/json"" ] }, ""body"": { ""received"": ""$!esc.java($request.body)"", ""at"": ""$!now_iso_8601"" } }"
            }
        }
    }
});
use mockserver_client::{ClientBuilder, Expectation, ExpectationAction, HttpRequest, HttpResponse, HttpTemplate};

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_template(HttpTemplate::new(
            "VELOCITY",
            r#"{ "method": "POST", "path": "/orders/$!esc.url($request.headers['x-order-id'][0])/created", "headers": { "Host": [ "orders.svc:8080" ], "Content-Type": [ "application/json" ] }, "body": { "received": "$!esc.java($request.body)", "at": "$!now_iso_8601" } }"#,
        ))),
]).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' => [
        ['httpRequestTemplate' => [
            'templateType' => 'VELOCITY',
            'template' => '{ "method": "POST", "path": "/orders/$!esc.url($request.headers[\'x-order-id\'][0])/created", "headers": { "Host": [ "orders.svc:8080" ], "Content-Type": [ "application/json" ] }, "body": { "received": "$!esc.java($request.body)", "at": "$!now_iso_8601" } }',
        ]],
    ],
]));
PUT /mockserver/expectation HTTP/1.1
Content-Type: application/json

{
  "httpRequest" : {
    "method" : "POST",
    "path" : "/order"
  },
  "httpResponse" : {
    "statusCode" : 201,
    "body" : "{ \"status\": \"created\" }"
  },
  "afterActions" : [ {
    "httpRequestTemplate" : {
      "templateType" : "VELOCITY",
      "template" : "{ \"method\": \"POST\", \"path\": \"/orders/$!esc.url($request.headers['x-order-id'][0])/created\", \"headers\": { \"Host\": [ \"orders.svc:8080\" ], \"Content-Type\": [ \"application/json\" ] }, \"body\": { \"received\": \"$!esc.java($request.body)\", \"at\": \"$!now_iso_8601\" } }"
    }
  } ]
}
 

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/v9"

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.