An extension is a Java class of your own that MockServer loads and starts when it starts, and stops when it stops. Extensions can only be configured at startup: the control plane (PUT /mockserver/configuration) cannot add, remove or change them, so nobody who can reach MockServer over the network can load code into it.

Extensions are trusted code. They run inside MockServer with the same access as MockServer itself, so only load extensions you wrote or trust.

When no extension is configured, MockServer loads none of the extension machinery and scans nothing, so start-up time and request handling are unchanged.

Writing an extension

Implement org.mockserver.extension.Extension, which is in mockserver-core. Add MockServer to your build with provided scope, as MockServer supplies it at runtime. A class loaded by name needs a public no-argument constructor.

package com.example;

import org.mockserver.extension.Extension;
import org.mockserver.extension.ExtensionContext;

import java.util.List;

public class AuditExtension implements Extension {

    @Override
    public String name() {
        return "audit"; // unique; lower-case letters, digits and '-'
    }

    @Override
    public void start(ExtensionContext context) {
        // runs before MockServer builds anything else; throw to refuse start-up
    }

    @Override
    public void onStarted(List<Integer> ports) {
        // runs once every port is bound; /mockserver/ready stays not-ready until it returns
    }

    @Override
    public void stop() {
        // runs when MockServer stops
    }
}

Every method except name() is optional. Extensions start in order of priority() (lowest first, default 0), then by name, and stop in the reverse order.

Seeding expectations

An extension that also implements org.mockserver.server.initialize.ExpectationInitializer has its expectations loaded when MockServer starts, after the configured initialisers:

public class SeedExtension implements Extension, ExpectationInitializer {

    @Override
    public String name() {
        return "seed";
    }

    @Override
    public Expectation[] initializeExpectations() {
        return new Expectation[]{
            new Expectation(request().withPath("/health")).thenRespond(response().withBody("ok"))
        };
    }
}
 

Serving your own control-plane routes

An extension that also implements org.mockserver.extension.AdminApiExtension answers every request under /mockserver/extensions/<name>/, where <name> is its name(). Use it to add your own operations next to MockServer's, for example to reload test data or report the state your extension keeps:

public class TestDataExtension implements Extension, AdminApiExtension {

    @Override
    public String name() {
        return "test-data";
    }

    @Override
    public HttpResponse handle(HttpRequest request, String subPath) {
        // PUT /mockserver/extensions/test-data/reload has subPath "/reload"
        if (subPath.equals("/reload")) {
            reload();
            return response().withStatusCode(200).withBody("reloaded");
        }
        return null; // answered 404
    }

    @Override
    public boolean isReadOnly(String method, String subPath) {
        return subPath.equals("/status"); // only /status can be called with the read role
    }
}

These routes are part of the control plane, so they are protected exactly like MockServer's own:

  • the same control-plane authentication (JWT, mTLS or OIDC) applies, so a caller without valid credentials gets 401 and your code is never called;
  • with controlPlaneAuthorizationEnabled, every route needs the role that may change state, GET included, unless isReadOnly returns true for it; a caller without the role gets 403. isReadOnly is only asked once the caller has been authenticated;
  • the same browser-origin check and CORS headers apply, and calls are recorded in the control-plane audit log when it is on.

Your code never runs on MockServer's network threads, so a slow route cannot hold up mocked responses:

  • at most extensionAdminMaxInFlight calls (default 32) run at once; a call beyond that gets 503 with Retry-After: 1, including a call your route makes back into MockServer while the limit is reached;
  • a call that has not answered within extensionAdminTimeoutMillis (default 30 seconds) gets 500, and your code is interrupted; what it returns afterwards is discarded. The call keeps its place in the limit until your code returns, so code that ignores the interrupt makes later calls get 503;
  • a call made while MockServer is stopping gets 503, and your code is not called;
  • an exception gets 500 with a correlation id that you can find in the MockServer log; the exception's message is not sent to the caller;
  • in a WAR your code runs on the servlet container's request thread, and in-process on the thread that called handle, with the same limit but no timeout.

Only the names of loaded extensions are taken. Any other path under /mockserver/extensions/ behaves as it did before, so an expectation for it still matches. An expectation for a path under a loaded extension's name no longer matches, and MockServer logs a warning at start-up for each such expectation it loads then.

Listening to what MockServer does

An extension can also implement one or more listener interfaces, like WireMock's ServeEventListener, StubLifecycleListener and GlobalSettingsListener:

InterfaceTold about
ServeEventListenerEach request MockServer answered: a mocked response, a forwarded response or the not-matched response, with the request, the response, the id of the expectation that answered it and when. Bodies are cut to maxLoggedBodyBytes when that is set; headers are not redacted. Load-test traffic MockServer generates itself is not reported.
ExpectationLifecycleListenerEach expectation added, updated or removed, and why: API, FILE_INITIALISER, CLASS_INITIALISER, FILE_WATCHER, EXHAUSTED (no matches left), EXPIRED (its time to live passed), EVICTED (to stay within maxExpectations) or CLUSTER (another node changed it).
SettingsListenerEach PUT /mockserver/configuration, after it has been applied. Calling Configuration setters in your own code does not notify it.
public class TrafficRecorder implements ServeEventListener, ExpectationLifecycleListener {

    @Override
    public String name() {
        return "traffic-recorder";
    }

    @Override
    public void onServed(ServeEvent event) {
        // event.disposition(), event.request(), event.response(), event.expectationId()
    }

    @Override
    public void onChange(ExpectationChange change) {
        if (change.type() == ExpectationChange.Type.REMOVED && change.cause() == ChangeCause.EXHAUSTED) {
            // an expectation has used up its matches
        }
    }
}

Each listener extension gets a thread of its own, so a listener may block or do slow work without slowing MockServer down. Expectation changes wait in a queue in the order MockServer made them; when a listener falls too far behind, further events for it are dropped (see extensionListenerQueueSize and extensionListenerMaxBytes) and counted in the mock_server_extension_listener_dropped_total metric. A dropped event never affects the event log or verification. An exception thrown by a listener is logged once and counted in mock_server_extension_errors_total, and later events are still delivered. When MockServer stops, it delivers what is still queued for up to stopDrainMillis before stopping the extensions.

An expectation that runs out of matches is reported as soon as the response that used its last match completes. An expectation whose time to live passes with no traffic is reported when a later request or a retrieve next finds it. The expectation in a change is MockServer's own, not a copy, so read it but do not change it.

 

Template helpers

An extension that also implements org.mockserver.extension.TemplateHelperProvider adds its own helpers to response and forward templates, under ext.<extension name>. Velocity and JavaScript templates call the methods of a public interface you choose; Mustache templates use a section helper, which receives the section after it has been rendered:

public class AcmeHelpers implements TemplateHelperProvider {

    public interface Strings {          // must be a public interface
        String upper(String value);
    }

    @Override
    public String name() {
        return "acme";
    }

    @Override
    public Map<Class<?>, Object> helpers() {
        // Velocity: $ext.acme.Strings.upper($request.path)
        // JavaScript: ext.acme.Strings.upper(request.path)
        return Map.of(Strings.class, (Strings) value -> value.toUpperCase());
    }

    @Override
    public Map<String, MustacheHelper> mustacheHelpers() {
        // Mustache: 
        return Map.of("upper", section -> section.toUpperCase());
    }
}

How helpers run:

  • Only a response template (httpResponseTemplate) or a forward template (httpForwardTemplate) can call a helper. Any other template that calls one (a webhook, a secondary action, a delay, a templated file body, a stream, a load scenario or the template dry run) fails with an error saying so.
  • Each call runs on a helper thread of its own, never on a thread that handles network traffic, so a slow helper cannot hold up other requests.
  • All the helper calls one template makes share one time budget, extensionHelperTimeoutMillis (default 1 second). A call still running when it is used up is interrupted and the template fails, like any other failing template.
  • At most extensionHelperMaxInFlight calls (default 32) run at once; a call over that limit fails its template at once. A helper cannot call another helper.
  • Helpers work the same over HTTP/1.1, HTTP/2 and HTTP/3.

Give helper methods string, number or boolean parameters. A JavaScript object or array passed to a helper cannot be read on the helper's thread, so pass it as JSON.stringify(...) and parse it in the helper. When a helper throws, the template's error names only the exception's type, so a client never sees what the exception says; the MockServer log has the full exception.

Keep helpers short and non-blocking. Anyone who can create an expectation can call them, so a helper must not return an object that exposes reflection or class loading, which would get round velocityDisallowClassLoading and the JavaScript class allow-list: return strings, numbers, booleans or plain data.

 

Filtering every request and transforming every response

Two more interfaces apply one rule to all the traffic MockServer serves, like WireMock's RequestFilterV2 and ResponseTransformerV2. Both are experimental: they may change in the next minor release.

InterfaceSeesCanTypical uses
RequestFiltereach request before MockServer matches it against your expectations, except the few listed belowlet it through, change it, or answer it at oncereject requests without a test API key or token; normalise requests, such as removing a path prefix or renaming a header, so expectations stay simple
ResponseTransformereach response just before MockServer sends it: mocked, forwarded and proxied responses, and the responses MockServer makes itself, such as its "no expectation matched" 404return a changed responseadd a header to every response; give MockServer's own errors the error format your API uses; rewrite something in every forwarded response

Use them when one rule has to apply to nearly all the traffic MockServer serves. For behaviour that belongs to one endpoint, an expectation (with a template or a callback where needed) is simpler. With no filter or transformer loaded, request handling is exactly as it is without them.

 

Writing a request filter

This filter answers 401 to any request without the right X-Api-Key header, lets browsers' CORS preflights through, and removes the key from requests it lets through, so your expectations and recorded requests never contain it. It shows how a filter works; it is not an access control, because a few kinds of request are answered without reaching any filter (listed below). To require credentials on every mocked endpoint, use dataPlaneAuthenticationRequired instead.

package com.example;

import org.mockserver.extension.FilterContext;
import org.mockserver.extension.FilterResult;
import org.mockserver.extension.RequestFilter;
import org.mockserver.model.HttpRequest;

import static org.mockserver.model.HttpResponse.response;

public class ApiKeyRequestFilter implements RequestFilter {

    @Override
    public String name() {
        return "api-key";
    }

    @Override
    public FilterResult filter(HttpRequest request, FilterContext context) {
        if (context.isCorsPreflight()) {
            // a browser sends its CORS preflight without the key, so let it through
            return FilterResult.proceed();
        }
        if (!"let-me-in".equals(request.getFirstHeader("X-Api-Key"))) {
            // answer now: no expectation is matched and no later filter runs
            return FilterResult.respond(response().withStatusCode(401).withBody("missing or wrong X-Api-Key"));
        }
        // continue without the key, so expectations and recorded requests never show it
        return FilterResult.proceedWith(request.removeHeader("X-Api-Key"));
    }
}

A filter returns one of three results:

  • FilterResult.proceed() — carry on with the request as it was before this filter ran. The filter is given its own copy of the request, so changes it made to that copy are thrown away.
  • FilterResult.proceedWith(request) — carry on with this request instead, usually the copy the filter was given, changed. Facts about the connection, such as the client's address, its TLS certificate and the HTTP version, are always kept from the original request.
  • FilterResult.respond(response) — answer the request now. No expectation is matched and later filters do not run. Response transformers still see this response.

The FilterContext tells the filter about the request: correlationId() (the id MockServer's log entries for the request carry), protocol(), isSecure(), remoteAddress(), isProxyRequest(), isCorsPreflight() and isGrpc().

With several filters, they run in order of priority() (lowest first), then by name, and the first one that answers stops the rest. MockServer logs the order when it starts. The event log, verification and the dashboard see the request as the filters left it; when a filter changed a request, an extra log entry, "request filtered by", records the request as it was received. A request a filter answered is logged like a mocked response, with no expectation id.

A filter fails when it throws an exception, returns null, takes longer than extensionDataPlaneTimeoutMillis, or breaks one of these rules:

  • it may not move a request onto MockServer's control plane, that is under /mockserver/ or /_mockserver_;
  • for a gRPC request (isGrpc()), it may not change the path or the x-grpc-service and x-grpc-method headers;
  • it may not answer with an interim (1xx) response.

A request whose filter fails is answered 500. The response names the extension and the kind of failure, and a correlation id to find the details in the MockServer log; the exception's message is never sent to the client. To let the request carry on instead, override failOpen() to return true. After a throw, a null or a broken rule, the request then carries on as it was before that filter ran, and the later filters still run. After a timeout, the later filters do not run, and the request carries on as it was before that filter ran only if every later filter also fails open; otherwise it is answered 500.

 

Requests that filters do not see. These requests are answered without any filter running, so neither a filter nor a transformer can change or refuse them:

  • requests to MockServer's control plane (/mockserver/... operations such as creating expectations), and CORS preflights for them;
  • requests MockServer refuses before matching, such as a 401 from data-plane authentication, or a request that is too large or malformed;
  • over HTTP/1.1, requests matched by an expectation that uses respondBeforeBody, which is answered as soon as the request's headers arrive;
  • gRPC bidirectional streaming calls answered by a grpcBidiResponse expectation (a client-streaming call, or a bidirectional call no such expectation answers, is filtered as one request once all its messages have arrived);
  • gRPC calls answered by MockServer's built-in gRPC health check or server reflection, or by a fault from a gRPC chaos profile;
  • traffic that is not an HTTP request matched against expectations: binary (TCP) proxying, DNS, and MockServer's own WebSocket endpoints, such as the dashboard's.
 

Writing a response transformer

This transformer adds an X-Served-By header to every response, and gives MockServer's own "no expectation matched" 404 a JSON body:

package com.example;

import org.mockserver.extension.ResponseTransformer;
import org.mockserver.extension.TransformContext;
import org.mockserver.model.HttpRequest;
import org.mockserver.model.HttpResponse;

import static org.mockserver.model.JsonBody.json;

public class ServedByResponseTransformer implements ResponseTransformer {

    @Override
    public String name() {
        return "served-by";
    }

    @Override
    public HttpResponse transform(HttpRequest request, HttpResponse response, TransformContext context) {
        response.replaceHeader("X-Served-By", "mockserver");
        if (context.isGenerated() && Integer.valueOf(404).equals(response.getStatusCode())) {
            // MockServer's own "no expectation matched" response gets a JSON body; leave Content-Length
            // alone and MockServer sets it to the length of the new body
            response.withBody(json("{\"error\": \"no expectation matched this request\"}"));
        }
        return response;
    }
}

A transformer is given copies of the request and the response, and returns the response to send, which may be the copy it was given, changed. The TransformContext has the same facts as a filter's, and three about the response:

  • isGenerated() is true when MockServer made the response itself rather than an expectation, an upstream or a callback: its 404 when no expectation matched, an overload 503, a rate-limit 429, a chaos error, a failed template or forward, a breakpoint abort, or a filter's respond();
  • expectationId() is the id of the expectation that answered, when there is one;
  • isStreamed() is true when the body is streamed (such as a chunked forwarded response). The transformer then sees only the status and headers, and may change only those.

With several transformers, they run in the reverse of the filter order: highest priority() first, then by name in reverse. So an extension that is both a filter and a transformer, and whose filter ran first, sees the response last. When a transformer changed a response, the event log adds a "response transformed by" entry recording the response that was sent.

Things to get right in a transformer:

  • Content-Length. When a transformer changes the body and leaves the Content-Length header it was given, MockServer removes that header and sends the length of the new body. A Content-Length the transformer set or changed itself is sent exactly as it is. If that value is wrong, the client reads the wrong number of bytes: over HTTP/1.1 the response is cut short, or the client waits for bytes that never come, and the keep-alive connection can no longer be used for the next request. Unless you have a reason to, do not set Content-Length; if you are unsure, remove it.
  • Compressed forwarded bodies. A forwarded or proxied response's body is as the upstream sent it, which may be compressed, as its Content-Encoding header says. A transformer that replaces such a body must also set or remove Content-Encoding to match the new body.
  • Headers added afterwards. CORS headers and defaultResponseHeaders are added after the transformers run, so a transformer cannot remove them.

A transformer fails when it throws an exception, returns null, takes longer than extensionDataPlaneTimeoutMillis, or replaces the body of a streamed response. Its response is then replaced by a 500 naming the extension, with a correlation id; with failOpen() returning true, a throw, a null or a replaced streamed body instead lets the response as it was before that transformer ran carry on to the next transformer, or be sent. After a timeout, the later transformers do not run, and the response as it was before that transformer ran is sent only if every later transformer also fails open; otherwise it is replaced by a 500.

Transformers do not see: responses from the control plane (/mockserver/... operations), MockServer's own answer to a CORS preflight, interim (1xx) responses, SSE, WebSocket and streaming gRPC responses, the answers to the requests that filters do not see (such as expectations that use respondBeforeBody, and gRPC health, reflection and chaos-profile faults), errors that close or reset the connection instead of sending a response, responses to requests MockServer refuses before matching (such as a data-plane authentication 401), and the 500 or 503 MockServer sends when a filter or transformer fails. A gRPC unary response is shown as JSON, before it is encoded.

 

How filters and transformers run, and what to watch for

  • Their own threads, with a time limit. Filters and transformers never run on the threads that serve connections, so a slow one cannot hold up other requests. All the filters for one request share one time budget, extensionDataPlaneTimeoutMillis (default 1 second), and so do all the transformers for one response; a filter or transformer still running at the end is interrupted, and whatever it returns afterwards is ignored. At most extensionDataPlaneMaxInFlight filter and transformer runs (default 32) happen at once; a request beyond that is answered 503 with Retry-After: 1, unless every extension involved fails open, when they are skipped. A gRPC call's grpc-timeout includes the time its filters and transformers take. In a WAR, and for InProcessMockServer.handle, the thread handling the request waits for them, for at most the time limit. If MockServer stops while one is still running, the request is answered 503; over HTTP/1.1 and HTTP/2, MockServer first waits up to stopDrainMillis for it to finish.
  • Not an access control. A filter is not a way to protect MockServer: some requests are answered without reaching it (listed above). To require credentials on every mocked endpoint, use dataPlaneAuthenticationRequired; it is checked before any filter runs. To protect the control plane, such as creating expectations or retrieving recorded requests, use control-plane authentication.
  • CORS preflights. A browser sends a CORS preflight (an OPTIONS request with Origin and Access-Control-Request-Method headers) before many cross-origin requests, and it never carries credentials. A preflight for one of your mocked endpoints reaches your filters with isCorsPreflight() true, so a filter that checks credentials should let it through, as the example above does; otherwise the browser never sends the real request. A preflight for a control-plane path, such as one a browser-based client sends before calling /mockserver/expectation, is answered by MockServer's control-plane CORS rules and never reaches a filter.
  • HTTP/3 status and readiness checks. Over HTTP/3, MockServer does not answer /mockserver/status and /mockserver/ready itself: they are matched against your expectations like any other request. So a filter sees them, and a filter that refuses requests without credentials refuses them too. Point health checks at MockServer's HTTP/1.1 or HTTP/2 port, or let those paths through in your filter. The same applies, on any protocol, to a /mockserver/... path that is not one of MockServer's own operations and so falls through to expectation matching.
  • After-actions and logs see the response before transformers. The event log, verification, retrieved and recorded responses, the dashboard, OpenAPI response validation, metrics, traces, and an expectation's after-actions and webhooks all see the response as it was before the transformers changed it. An after-action can therefore fire, or a webhook be sent, for a response that a failing transformer then replaces with a 500. What was actually sent is in the "response transformed by" log entry or, after a failure, in the warning.
  • Where proxied requests go. A filter can change where MockServer sends a request it proxies, for example by changing the request's socketAddress to another host and port (whether it is sent over TLS still follows how the request arrived). MockServer does not stop a filter from sending requests to internal or cloud-metadata addresses unless you turn on forwardProxyBlockPrivateNetworks, which is off by default; forwardProxyAllowedHosts and forwardProxyDeniedHosts, also off by default, apply to these requests too. Turn them on if anyone you do not trust can influence what your filter does.
  • Failures are visible. Every failure adds one warning to the MockServer log, with the correlation id, the extension, what failed and the response sent, if any. When metrics are on, failures are counted by mock_server_extension_dataplane_failures (labelled by extension, hook and type: threw, timeout, null or contract_violation), and requests refused because too many were running by mock_server_overload_rejections with reason="extension_dataplane".
  • Custom bodies. Filters and transformers are given copies, so a custom subclass of MockServer's Body must override copy(); a request or response with a body that cannot be copied fails the filter or transformer.

A runnable version of both examples, with a test that starts MockServer with them loaded, is in examples/java. Load them like any other extension, as described next.

Loading extensions

Name the extension classes in the extensions property, as a comma-separated list, and put their JARs on MockServer's classpath.

Command line — java -jar ignores -cp, so start MockServer with its main class:

java -cp mockserver-netty-jar-with-dependencies.jar:ext/* org.mockserver.cli.Main \
  --port 1080 \
  --extensions com.example.AuditExtension,com.example.SeedExtension

System property, environment variable or property file:

-Dmockserver.extensions=com.example.AuditExtension
MOCKSERVER_EXTENSIONS=com.example.AuditExtension
mockserver.extensions=com.example.AuditExtension

Docker — every MockServer image adds every JAR in /extensions to the classpath, so mount a folder of extension JARs there, read-only, and set the property. This works the same in every image variant, including -graaljs, -clustered and -aot, and keeps the images' fast start-up. Use /extensions rather than /libs: the -graaljs and -clustered images ship their own JARs in /libs, so mounting over it breaks them.

docker run -v "$(pwd)/ext:/extensions:ro" -e MOCKSERVER_EXTENSIONS=com.example.AuditExtension -p 1080:1080 mockserver/mockserver

Helm — put the JARs in any Kubernetes volume (a ConfigMap, a Secret, a PersistentVolumeClaim, ...) and the chart mounts it read-only at /extensions:

extensions:
  classes: "com.example.AuditExtension"
  volume:
    configMap:
      name: mockserver-extensions

Embedded Java — pass classes or suppliers. Each server you start gets its own instances, even when two servers share one Configuration:

ClientAndServer mockServer = ClientAndServer.startClientAndServer(
    Configuration.configuration().extensions(AuditExtension.class),
    1080
);

Spring — set the property on the test or on each named instance; each instance loads only the extensions it names:

@MockServerTest("mockserver.extensions=com.example.AuditExtension")

@MockServerInstance(name = "orders", properties = "mockserver.extensions=com.example.AuditExtension")

WAR — put the JARs in WEB-INF/lib and set the system property or mockserver.properties. onStarted is called once, when the servlet is created, with an empty list of ports.

In-process — pass them to new InProcessMockServer(configuration().extensions(AuditExtension.class)). They start when the instance is created and stop when it is closed. onStarted gets an empty list of ports, admin routes are reached by passing the request to handle and run on the calling thread with no timeout, and a SettingsListener is never called, because PUT /mockserver/configuration is not available in-process.

To also load extensions listed in META-INF/services/org.mockserver.extension.Extension files on the classpath, turn on extensionScanning. It is off by default, so only the extensions you name are loaded.

When an extension fails

MockServer refuses to start, and the error names the extension, if:

  • an extension class cannot be found, does not implement Extension, or has no public no-argument constructor;
  • two extensions have the same name, or a name is not lower-case letters, digits and -;
  • start, onStarted or, for a request filter or response transformer, failOpen throws, or does not return within extensionStartTimeoutMillis (default 30 seconds);
  • a template helper is not a public interface with an implementation of it, two helpers of one extension share an interface name, or a Mustache helper key is not letters, digits, _ or -;
  • an extension that seeds expectations throws from initializeExpectations. This is stricter than initializationClass, which only stops start-up when failOnInitializationError is set;
  • a file MockServer is set to write would land in the folder an extension was loaded from: persisted expectations (persistExpectations), persisted recorded expectations (persistRecordedExpectations), persisted recorded requests (persistRecordedRequestsToDisk), the audit log (auditLogFile) or the memory-usage CSV files (outputMemoryUsageCsv). Write them somewhere else, such as a separate volume. For an extension JAR this means the JAR's own folder, for example /extensions; for a folder of classes it means anywhere inside it.

The extensions that had already started are stopped, in reverse order, and nothing else is left running, except the thread of an extension call that did not return in time: it is interrupted, and a warning says it may still be running. An exception thrown from stop is logged and the other extensions still stop.

GET /mockserver/configuration shows the configured extension names; a PUT that changes them is ignored with a warning in the log.