MockServer supports mocking gRPC services by transparently converting gRPC requests (protobuf over HTTP/2) into JSON-over-HTTP requests internally. This allows the standard expectation matching engine to handle gRPC requests using the same JSON format used for HTTP mocking.

 

How gRPC Mocking Works

When MockServer receives a gRPC request:

  1. The protobuf binary body is decoded to JSON using the loaded proto descriptors
  2. MockServer adds metadata headers to the converted request:
  3. The converted JSON request is matched against active expectations
  4. The JSON response is encoded back to protobuf binary and returned as a gRPC response

This means you can set up gRPC expectations using the same JSON format and client APIs as HTTP mocking — no special gRPC client tooling is needed.

 

Loading Proto Descriptors

MockServer needs proto descriptors to convert between protobuf binary and JSON. There are three ways to load them:

1. Pre-compiled Descriptor Files

Compile your .proto files to descriptor sets and point MockServer at the directory:

protoc --descriptor_set_out=service.dsc --include_imports service.proto

Then configure MockServer:

-Dmockserver.grpcDescriptorDirectory="/path/to/descriptors"

Or via environment variable:

MOCKSERVER_GRPC_DESCRIPTOR_DIRECTORY=/path/to/descriptors

2. Proto Source Files (Auto-compiled)

Point MockServer at a directory of .proto source files and they will be compiled at startup using protoc:

-Dmockserver.grpcProtoDirectory="/path/to/protos"

This requires protoc to be available on the system PATH. If protoc is installed elsewhere, configure its path:

-Dmockserver.grpcProtocPath="/usr/local/bin/protoc"

3. REST API Upload

Upload compiled descriptors at runtime via the REST API:

curl -v -X PUT "http://localhost:1080/mockserver/grpc/descriptors" \
  --data-binary @service.dsc
 

Docker

When running MockServer in Docker, mount your proto files or descriptors into the container:

docker run -d --rm \
  -p 1080:1080 \
  -v /local/path/to/protos:/protos \
  -e MOCKSERVER_GRPC_PROTO_DIRECTORY=/protos \
  mockserver/mockserver:latest

Replace latest with a specific version tag (e.g. mockserver/mockserver:7.6.0) to pin a known working version.

 

Creating gRPC Expectations

Given a proto file such as:

syntax = "proto3";
package com.example.grpc;

service GreetingService {
  rpc Greeting (HelloRequest) returns (HelloResponse);
  rpc ListGreetings (HelloRequest) returns (stream HelloResponse);
}

message HelloRequest {
  string name = 1;
}

message HelloResponse {
  string greeting = 1;
}

You can create expectations that match on the JSON-converted request body and the gRPC metadata headers:

 

Unary RPC

new MockServerClient("localhost", 1080)
    .when(
        request()
            .withMethod("POST")
            .withPath("/com.example.grpc.GreetingService/Greeting")
            .withHeader("x-grpc-service", "com.example.grpc.GreetingService")
            .withHeader("x-grpc-method", "Greeting")
            .withBody(json("{\"name\": \"World\"}"))
    )
    .respond(
        response()
            .withStatusCode(200)
            .withHeader("grpc-status", "0")
            .withBody("{\"greeting\": \"Hello World\"}")
    );
var mockServerClient = require('mockserver-client').mockServerClient;
mockServerClient("localhost", 1080).mockAnyResponse({
    "httpRequest": {
        "method": "POST",
        "path": "/com.example.grpc.GreetingService/Greeting",
        "headers": {
            "x-grpc-service": ["com.example.grpc.GreetingService"],
            "x-grpc-method": ["Greeting"]
        },
        "body": {
            "type": "JSON",
            "json": "{\"name\": \"World\"}"
        }
    },
    "httpResponse": {
        "statusCode": 200,
        "headers": {
            "grpc-status": ["0"]
        },
        "body": "{\"greeting\": \"Hello World\"}"
    }
}).then(
    function () { console.log("expectation created"); },
    function (error) { console.log(error); }
);

See REST API for full JSON specification

from mockserver import MockServerClient, HttpRequest, HttpResponse, KeyToMultiValue

client = MockServerClient("localhost", 1080)
client.when(
    HttpRequest(
        method="POST",
        path="/com.example.grpc.GreetingService/Greeting",
        headers=[
            KeyToMultiValue(name="x-grpc-service", values=["com.example.grpc.GreetingService"]),
            KeyToMultiValue(name="x-grpc-method", values=["Greeting"])
        ],
        body={"type": "JSON", "json": '{"name": "World"}'}
    )
).respond(
    HttpResponse(
        status_code=200,
        headers=[KeyToMultiValue(name="grpc-status", values=["0"])],
        body='{"greeting": "Hello World"}'
    )
)
require 'mockserver-client'
include MockServer

client = MockServer::Client.new('localhost', 1080)
client.when(
  HttpRequest.new(
    method: 'POST',
    path: '/com.example.grpc.GreetingService/Greeting',
    headers: [
      { name: 'x-grpc-service', values: ['com.example.grpc.GreetingService'] },
      { name: 'x-grpc-method', values: ['Greeting'] }
    ],
    body: { type: 'JSON', json: '{"name": "World"}' }
  )
).respond(
  HttpResponse.new(
    status_code: 200,
    headers: [{ name: 'grpc-status', values: ['0'] }],
    body: '{"greeting": "Hello World"}'
  )
)
import mockserver "github.com/mock-server/mockserver-monorepo/mockserver-client-go/v7"

client := mockserver.New("localhost", 1080)
client.When(
    mockserver.Request().
        Method("POST").
        Path("/com.example.grpc.GreetingService/Greeting").
        Header("x-grpc-service", "com.example.grpc.GreetingService").
        Header("x-grpc-method", "Greeting").
        Body(`{"name": "World"}`),
).Respond(
    mockserver.Response().
        StatusCode(200).
        Header("grpc-status", "0").
        Body(`{"greeting": "Hello World"}`),
)
using MockServer.Client;
using MockServer.Client.Models;

using var client = new MockServerClient("localhost", 1080);
client.When(
    HttpRequest.Request()
        .WithMethod("POST")
        .WithPath("/com.example.grpc.GreetingService/Greeting")
        .WithHeader("x-grpc-service", "com.example.grpc.GreetingService")
        .WithHeader("x-grpc-method", "Greeting")
        .WithBody("{\"name\": \"World\"}")
).Respond(
    HttpResponse.Response()
        .WithStatusCode(200)
        .WithHeader("grpc-status", "0")
        .WithBody("{\"greeting\": \"Hello World\"}")
);
use mockserver_client::{ClientBuilder, HttpRequest, HttpResponse};

let client = ClientBuilder::new("localhost", 1080).build().unwrap();
client.when(
    HttpRequest::new()
        .method("POST")
        .path("/com.example.grpc.GreetingService/Greeting")
        .header("x-grpc-service", "com.example.grpc.GreetingService")
        .header("x-grpc-method", "Greeting")
        .body(r#"{"name": "World"}"#)
).respond(
    HttpResponse::new()
        .status_code(200)
        .header("grpc-status", "0")
        .body(r#"{"greeting": "Hello World"}"#)
).unwrap();
use MockServer\MockServerClient;
use MockServer\HttpRequest;
use MockServer\HttpResponse;

$client = new MockServerClient('localhost', 1080);
$client->when(
    HttpRequest::request()
        ->method('POST')
        ->path('/com.example.grpc.GreetingService/Greeting')
        ->header('x-grpc-service', 'com.example.grpc.GreetingService')
        ->header('x-grpc-method', 'Greeting')
        ->jsonBody(['name' => 'World'])
)->respond(
    HttpResponse::response()
        ->statusCode(200)
        ->header('grpc-status', '0')
        ->body('{"greeting": "Hello World"}')
);
curl -v -X PUT "http://localhost:1080/mockserver/expectation" -d '{
  "httpRequest": {
    "method": "POST",
    "path": "/com.example.grpc.GreetingService/Greeting",
    "headers": {
      "x-grpc-service": ["com.example.grpc.GreetingService"],
      "x-grpc-method": ["Greeting"]
    },
    "body": {
      "type": "JSON",
      "json": "{\"name\": \"World\"}"
    }
  },
  "httpResponse": {
    "statusCode": 200,
    "headers": {
      "grpc-status": ["0"]
    },
    "body": "{\"greeting\": \"Hello World\"}"
  }
}'
 

Server Streaming RPC

For server streaming RPCs, use a gRPC stream response to return multiple messages. Each message can have an optional delay:

new MockServerClient("localhost", 1080)
    .when(
        request()
            .withMethod("POST")
            .withPath("/com.example.grpc.GreetingService/ListGreetings")
            .withHeader("x-grpc-service", "com.example.grpc.GreetingService")
            .withHeader("x-grpc-method", "ListGreetings")
    )
    .respondWithGrpcStream(
        grpcStreamResponse()
            .withStatusName("OK")
            .withMessage("{\"greeting\": \"Hello Alice\"}")
            .withMessage("{\"greeting\": \"Hello Bob\"}", delay(MILLISECONDS, 100))
            .withMessage("{\"greeting\": \"Hello Charlie\"}", delay(MILLISECONDS, 200))
    );
var mockServerClient = require('mockserver-client').mockServerClient;
mockServerClient("localhost", 1080).mockAnyResponse({
    "httpRequest": {
        "method": "POST",
        "path": "/com.example.grpc.GreetingService/ListGreetings",
        "headers": {
            "x-grpc-service": ["com.example.grpc.GreetingService"],
            "x-grpc-method": ["ListGreetings"]
        }
    },
    "grpcStreamResponse": {
        "statusName": "OK",
        "messages": [
            {"json": "{\"greeting\": \"Hello Alice\"}"},
            {"json": "{\"greeting\": \"Hello Bob\"}", "delay": {"timeUnit": "MILLISECONDS", "value": 100}},
            {"json": "{\"greeting\": \"Hello Charlie\"}", "delay": {"timeUnit": "MILLISECONDS", "value": 200}}
        ]
    }
}).then(
    function () { console.log("expectation created"); },
    function (error) { console.log(error); }
);

See REST API for full JSON specification

from mockserver import MockServerClient, HttpRequest, KeyToMultiValue, GrpcStreamResponse, GrpcStreamMessage, Delay

client = MockServerClient("localhost", 1080)
client.when(
    HttpRequest(
        method="POST",
        path="/com.example.grpc.GreetingService/ListGreetings",
        headers=[
            KeyToMultiValue(name="x-grpc-service", values=["com.example.grpc.GreetingService"]),
            KeyToMultiValue(name="x-grpc-method", values=["ListGreetings"])
        ]
    )
).respond_with_grpc_stream(
    GrpcStreamResponse(
        status_name="OK",
        messages=[
            GrpcStreamMessage(json='{"greeting": "Hello Alice"}'),
            GrpcStreamMessage(json='{"greeting": "Hello Bob"}', delay=Delay(time_unit="MILLISECONDS", value=100)),
            GrpcStreamMessage(json='{"greeting": "Hello Charlie"}', delay=Delay(time_unit="MILLISECONDS", value=200))
        ]
    )
)
require 'mockserver-client'
include MockServer

client = MockServer::Client.new('localhost', 1080)
client.when(
  HttpRequest.new(
    method: 'POST',
    path: '/com.example.grpc.GreetingService/ListGreetings',
    headers: [
      KeyToMultiValue.new(name: 'x-grpc-service', values: ['com.example.grpc.GreetingService']),
      KeyToMultiValue.new(name: 'x-grpc-method', values: ['ListGreetings'])
    ]
  )
).respond_with_grpc_stream(
  GrpcStreamResponse.new(
    status_name: 'OK',
    messages: [
      GrpcStreamMessage.new(json: '{"greeting": "Hello Alice"}'),
      GrpcStreamMessage.new(json: '{"greeting": "Hello Bob"}', delay: Delay.new(time_unit: 'MILLISECONDS', value: 100)),
      GrpcStreamMessage.new(json: '{"greeting": "Hello Charlie"}', delay: Delay.new(time_unit: 'MILLISECONDS', value: 200))
    ]
  )
)
package main

import (
    "bytes"
    "net/http"
)

func main() {
    body := []byte(`{
  "httpRequest": {
    "method": "POST",
    "path": "/com.example.grpc.GreetingService/ListGreetings",
    "headers": {
      "x-grpc-service": ["com.example.grpc.GreetingService"],
      "x-grpc-method": ["ListGreetings"]
    }
  },
  "grpcStreamResponse": {
    "statusName": "OK",
    "messages": [
      {"json": "{\"greeting\": \"Hello Alice\"}"},
      {"json": "{\"greeting\": \"Hello Bob\"}", "delay": {"timeUnit": "MILLISECONDS", "value": 100}},
      {"json": "{\"greeting\": \"Hello Charlie\"}", "delay": {"timeUnit": "MILLISECONDS", "value": 200}}
    ]
  }
}`)
    req, _ := http.NewRequest("PUT",
        "http://localhost:1080/mockserver/expectation",
        bytes.NewReader(body))
    req.Header.Set("Content-Type", "application/json")
    http.DefaultClient.Do(req)
}
using System.Net.Http;
using System.Text;

using var httpClient = new HttpClient();
var json = @"{
  ""httpRequest"": {
    ""method"": ""POST"",
    ""path"": ""/com.example.grpc.GreetingService/ListGreetings"",
    ""headers"": {
      ""x-grpc-service"": [""com.example.grpc.GreetingService""],
      ""x-grpc-method"": [""ListGreetings""]
    }
  },
  ""grpcStreamResponse"": {
    ""statusName"": ""OK"",
    ""messages"": [
      {""json"": ""{\""greeting\"": \""Hello Alice\""}""},
      {""json"": ""{\""greeting\"": \""Hello Bob\""}"", ""delay"": {""timeUnit"": ""MILLISECONDS"", ""value"": 100}},
      {""json"": ""{\""greeting\"": \""Hello Charlie\""}"", ""delay"": {""timeUnit"": ""MILLISECONDS"", ""value"": 200}}
    ]
  }
}";
await httpClient.PutAsync(
    "http://localhost:1080/mockserver/expectation",
    new StringContent(json, Encoding.UTF8, "application/json")
);
use reqwest::blocking::Client;

let client = Client::new();
let body = r#"{
  "httpRequest": {
    "method": "POST",
    "path": "/com.example.grpc.GreetingService/ListGreetings",
    "headers": {
      "x-grpc-service": ["com.example.grpc.GreetingService"],
      "x-grpc-method": ["ListGreetings"]
    }
  },
  "grpcStreamResponse": {
    "statusName": "OK",
    "messages": [
      {"json": "{\"greeting\": \"Hello Alice\"}"},
      {"json": "{\"greeting\": \"Hello Bob\"}", "delay": {"timeUnit": "MILLISECONDS", "value": 100}},
      {"json": "{\"greeting\": \"Hello Charlie\"}", "delay": {"timeUnit": "MILLISECONDS", "value": 200}}
    ]
  }
}"#;
client.put("http://localhost:1080/mockserver/expectation")
    .header("Content-Type", "application/json")
    .body(body.to_string())
    .send()
    .unwrap();
{% raw %}
// gRPC stream responses are not supported in the PHP client;
// use the REST API directly
$ch = curl_init('http://localhost:1080/mockserver/expectation');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'PUT',
    CURLOPT_POSTFIELDS => json_encode([[
        'httpRequest' => [
            'method' => 'POST',
            'path' => '/com.example.grpc.GreetingService/ListGreetings',
            'headers' => [
                'x-grpc-service' => ['com.example.grpc.GreetingService'],
                'x-grpc-method' => ['ListGreetings'],
            ],
        ],
        'grpcStreamResponse' => [
            'statusName' => 'OK',
            'messages' => [
                ['json' => '{"greeting": "Hello Alice"}'],
                ['json' => '{"greeting": "Hello Bob"}', 'delay' => ['timeUnit' => 'MILLISECONDS', 'value' => 100]],
                ['json' => '{"greeting": "Hello Charlie"}', 'delay' => ['timeUnit' => 'MILLISECONDS', 'value' => 200]],
            ],
        ],
    ]]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['Content-Type: application/json; charset=utf-8'],
]);
curl_exec($ch);
curl_close($ch);
{% endraw %}
curl -v -X PUT "http://localhost:1080/mockserver/expectation" -d '{
  "httpRequest": {
    "method": "POST",
    "path": "/com.example.grpc.GreetingService/ListGreetings",
    "headers": {
      "x-grpc-service": ["com.example.grpc.GreetingService"],
      "x-grpc-method": ["ListGreetings"]
    }
  },
  "grpcStreamResponse": {
    "statusName": "OK",
    "messages": [
      {"json": "{\"greeting\": \"Hello Alice\"}"},
      {"json": "{\"greeting\": \"Hello Bob\"}", "delay": {"timeUnit": "MILLISECONDS", "value": 100}},
      {"json": "{\"greeting\": \"Hello Charlie\"}", "delay": {"timeUnit": "MILLISECONDS", "value": 200}}
    ]
  }
}'
 

Client Streaming RPC

Client streaming requests are converted with the combined stream messages in the request body. MockServer adds an x-grpc-client-streaming header to indicate this is a client streaming request. Client streaming support is limited — see Limitations below.

 

Bidirectional Streaming RPC (Experimental)

MockServer supports true bidirectional (bidi) gRPC streaming, where both the client and server can send messages independently and interleaved on a single stream. This requires the grpcBidiStreamingEnabled configuration flag to be set to true (default is false). See the gRPC Configuration section for details on enabling it.

A grpcBidiResponse action supports two response mechanisms:

Each reactive-rule response message can optionally be a response template by setting its templateType to VELOCITY or MUSTACHE. A templated response is rendered against the matched inbound message (exposed as the request body), so the reply can echo or derive fields from the request — for example {"greeting": "Hi $jsonPath.find(\"$.name\")"} — and can transition scenario state via $scenario.set('name','state') on an inbound match. Templating is opt-in: response messages with no templateType are sent exactly as written. JAVASCRIPT is not supported for bidi stream responses.

new MockServerClient("localhost", 1080)
    .when(
        request()
            .withMethod("POST")
            .withPath("/com.example.grpc.ChatService/BidiChat")
            .withHeader("x-grpc-service", "com.example.grpc.ChatService")
            .withHeader("x-grpc-method", "BidiChat")
    )
    .respondWithGrpcBidi(
        grpcBidiResponse()
            .withStatusName("OK")
            .withMessage("{\"message\": \"Welcome to the chat!\"}")
            .withRule(grpcBidiRule("{\"message\": \"hello\"}")
                .withResponse("{\"message\": \"Hello! How can I help?\"}"))
            .withRule(grpcBidiRule(".*goodbye.*")
                .withResponse("{\"message\": \"Goodbye! Have a nice day.\"}"))
    );
var mockServerClient = require('mockserver-client').mockServerClient;
mockServerClient("localhost", 1080).mockAnyResponse({
    "httpRequest": {
        "method": "POST",
        "path": "/com.example.grpc.ChatService/BidiChat",
        "headers": {
            "x-grpc-service": ["com.example.grpc.ChatService"],
            "x-grpc-method": ["BidiChat"]
        }
    },
    "grpcBidiResponse": {
        "statusName": "OK",
        "messages": [
            {"json": "{\"message\": \"Welcome to the chat!\"}"}
        ],
        "rules": [
            {
                "matchJson": "{\"message\": \"hello\"}",
                "responses": [
                    {"json": "{\"message\": \"Hello! How can I help?\"}"}
                ]
            },
            {
                "matchJson": ".*goodbye.*",
                "responses": [
                    {"json": "{\"message\": \"Goodbye! Have a nice day.\"}"}
                ]
            }
        ]
    }
}).then(
    function () { console.log("expectation created"); },
    function (error) { console.log(error); }
);

See REST API for full JSON specification

from mockserver import (
    MockServerClient, HttpRequest, KeyToMultiValue,
    GrpcBidiResponse, GrpcBidiRule, GrpcStreamMessage,
)

client = MockServerClient("localhost", 1080)
client.when(
    HttpRequest(
        method="POST",
        path="/com.example.grpc.ChatService/BidiChat",
        headers=[
            KeyToMultiValue(name="x-grpc-service", values=["com.example.grpc.ChatService"]),
            KeyToMultiValue(name="x-grpc-method", values=["BidiChat"])
        ]
    )
).respond_with_grpc_bidi(
    GrpcBidiResponse(
        status_name="OK",
        messages=[GrpcStreamMessage(json='{"message": "Welcome to the chat!"}')],
        rules=[
            GrpcBidiRule(
                match_json='{"message": "hello"}',
                responses=[GrpcStreamMessage(json='{"message": "Hello! How can I help?"}')]
            ),
            GrpcBidiRule(
                match_json='.*goodbye.*',
                responses=[GrpcStreamMessage(json='{"message": "Goodbye! Have a nice day."}')]
            ),
        ]
    )
)
require 'mockserver-client'
include MockServer

client = MockServer::Client.new('localhost', 1080)
client.when(
  HttpRequest.new(
    method: 'POST',
    path: '/com.example.grpc.ChatService/BidiChat',
    headers: [
      KeyToMultiValue.new(name: 'x-grpc-service', values: ['com.example.grpc.ChatService']),
      KeyToMultiValue.new(name: 'x-grpc-method', values: ['BidiChat'])
    ]
  )
).respond_with_grpc_bidi(
  GrpcBidiResponse.new(
    status_name: 'OK',
    messages: [GrpcStreamMessage.new(json: '{"message": "Welcome to the chat!"}')],
    rules: [
      GrpcBidiRule.new(
        match_json: '{"message": "hello"}',
        responses: [GrpcStreamMessage.new(json: '{"message": "Hello! How can I help?"}')]
      ),
      GrpcBidiRule.new(
        match_json: '.*goodbye.*',
        responses: [GrpcStreamMessage.new(json: '{"message": "Goodbye! Have a nice day."}')]
      )
    ]
  )
)
package main

import (
    "bytes"
    "net/http"
)

func main() {
    body := []byte(`{
  "httpRequest": {
    "method": "POST",
    "path": "/com.example.grpc.ChatService/BidiChat",
    "headers": {
      "x-grpc-service": ["com.example.grpc.ChatService"],
      "x-grpc-method": ["BidiChat"]
    }
  },
  "grpcBidiResponse": {
    "statusName": "OK",
    "messages": [
      {"json": "{\"message\": \"Welcome to the chat!\"}"}
    ],
    "rules": [
      {
        "matchJson": "{\"message\": \"hello\"}",
        "responses": [
          {"json": "{\"message\": \"Hello! How can I help?\"}"}
        ]
      },
      {
        "matchJson": ".*goodbye.*",
        "responses": [
          {"json": "{\"message\": \"Goodbye! Have a nice day.\"}"}
        ]
      }
    ]
  }
}`)
    req, _ := http.NewRequest("PUT",
        "http://localhost:1080/mockserver/expectation",
        bytes.NewReader(body))
    req.Header.Set("Content-Type", "application/json")
    http.DefaultClient.Do(req)
}
using System.Net.Http;
using System.Text;

using var httpClient = new HttpClient();
var json = @"{
  ""httpRequest"": {
    ""method"": ""POST"",
    ""path"": ""/com.example.grpc.ChatService/BidiChat"",
    ""headers"": {
      ""x-grpc-service"": [""com.example.grpc.ChatService""],
      ""x-grpc-method"": [""BidiChat""]
    }
  },
  ""grpcBidiResponse"": {
    ""statusName"": ""OK"",
    ""messages"": [
      {""json"": ""{\""message\"": \""Welcome to the chat!\""}""}
    ],
    ""rules"": [
      {
        ""matchJson"": ""{\""message\"": \""hello\""}"",
        ""responses"": [
          {""json"": ""{\""message\"": \""Hello! How can I help?\""}""}
        ]
      },
      {
        ""matchJson"": "".*goodbye.*"",
        ""responses"": [
          {""json"": ""{\""message\"": \""Goodbye! Have a nice day.\""}""}
        ]
      }
    ]
  }
}";
await httpClient.PutAsync(
    "http://localhost:1080/mockserver/expectation",
    new StringContent(json, Encoding.UTF8, "application/json")
);
use reqwest::blocking::Client;

let client = Client::new();
let body = r#"{
  "httpRequest": {
    "method": "POST",
    "path": "/com.example.grpc.ChatService/BidiChat",
    "headers": {
      "x-grpc-service": ["com.example.grpc.ChatService"],
      "x-grpc-method": ["BidiChat"]
    }
  },
  "grpcBidiResponse": {
    "statusName": "OK",
    "messages": [
      {"json": "{\"message\": \"Welcome to the chat!\"}"}
    ],
    "rules": [
      {
        "matchJson": "{\"message\": \"hello\"}",
        "responses": [
          {"json": "{\"message\": \"Hello! How can I help?\"}"}
        ]
      },
      {
        "matchJson": ".*goodbye.*",
        "responses": [
          {"json": "{\"message\": \"Goodbye! Have a nice day.\"}"}
        ]
      }
    ]
  }
}"#;
client.put("http://localhost:1080/mockserver/expectation")
    .header("Content-Type", "application/json")
    .body(body.to_string())
    .send()
    .unwrap();
{% raw %}
// gRPC bidi responses are not supported in the PHP client;
// use the REST API directly
$ch = curl_init('http://localhost:1080/mockserver/expectation');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'PUT',
    CURLOPT_POSTFIELDS => json_encode([[
        'httpRequest' => [
            'method' => 'POST',
            'path' => '/com.example.grpc.ChatService/BidiChat',
            'headers' => [
                'x-grpc-service' => ['com.example.grpc.ChatService'],
                'x-grpc-method' => ['BidiChat'],
            ],
        ],
        'grpcBidiResponse' => [
            'statusName' => 'OK',
            'messages' => [
                ['json' => '{"message": "Welcome to the chat!"}'],
            ],
            'rules' => [
                [
                    'matchJson' => '{"message": "hello"}',
                    'responses' => [['json' => '{"message": "Hello! How can I help?"}']],
                ],
                [
                    'matchJson' => '.*goodbye.*',
                    'responses' => [['json' => '{"message": "Goodbye! Have a nice day."}']],
                ],
            ],
        ],
    ]]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['Content-Type: application/json; charset=utf-8'],
]);
curl_exec($ch);
curl_close($ch);
{% endraw %}
curl -v -X PUT "http://localhost:1080/mockserver/expectation" -d '{
  "httpRequest": {
    "method": "POST",
    "path": "/com.example.grpc.ChatService/BidiChat",
    "headers": {
      "x-grpc-service": ["com.example.grpc.ChatService"],
      "x-grpc-method": ["BidiChat"]
    }
  },
  "grpcBidiResponse": {
    "statusName": "OK",
    "messages": [
      {"json": "{\"message\": \"Welcome to the chat!\"}"}
    ],
    "rules": [
      {
        "matchJson": "{\"message\": \"hello\"}",
        "responses": [
          {"json": "{\"message\": \"Hello! How can I help?\"}"}
        ]
      },
      {
        "matchJson": ".*goodbye.*",
        "responses": [
          {"json": "{\"message\": \"Goodbye! Have a nice day.\"}"}
        ]
      }
    ]
  }
}'

Supported Features (Bidi Streaming)

Requirements (Bidi Streaming)

 

Matching gRPC Requests

Since gRPC requests are converted to JSON, all standard MockServer matchers work:

 

Binary Metadata (-bin keys)

gRPC treats any metadata key whose name ends in -bin as binary: the value travels over the wire base64-encoded. MockServer never encodes or decodes these values — it passes them through exactly as written. So write the value already base64-encoded, both when matching a request and when setting a response header.

For example, to match metadata that your client sets as the four raw bytes 0x01 0x02 0x03 0x04, base64-encode them first and match on the result:

metadata key:    x-trace-bin
raw bytes:       0x01 0x02 0x03 0x04
value to use:    AQIDBA==   (or AQIDBA — see below)

Padding does not matter. Most base64 encoders (including Java's Base64.getEncoder()) add = padding, producing AQIDBA==. The gRPC wire format omits that padding, so what actually arrives from a real gRPC client is AQIDBA. MockServer compares -bin values with the padding ignored, so both spellings match the same request and you do not have to know which one your client library produces.

This padding-insensitive comparison applies only to header names ending in -bin — the padding of any other header value is significant, as before.

Apart from the padding, a -bin value is compared exactly like any other header value. In particular header matching is case-insensitive, so two base64 values differing only in letter case are treated as equal even though they decode to different bytes. This is long-standing behaviour for every header, not something specific to -bin, but it is worth knowing when your metadata is binary.

 

gRPC Status Codes

For unary RPCs, set the gRPC status via the grpc-status response header (as shown in the unary example above). For server streaming RPCs, use the statusName field in the grpcStreamResponse action instead. You can also set a grpc-message header (unary) or statusMessage field (streaming) to provide error details. Standard gRPC status codes are supported:

Code Name
0OK
1CANCELLED
2UNKNOWN
3INVALID_ARGUMENT
4DEADLINE_EXCEEDED
5NOT_FOUND
6ALREADY_EXISTS
7PERMISSION_DENIED
8RESOURCE_EXHAUSTED
9FAILED_PRECONDITION
10ABORTED
11OUT_OF_RANGE
12UNIMPLEMENTED
13INTERNAL
14UNAVAILABLE
15DATA_LOSS
16UNAUTHENTICATED
 

Forwarding & Recording gRPC Calls

As well as returning mocked responses, MockServer can forward a gRPC call to a real upstream gRPC server and record the exchange so it can later be replayed as a mock — the same record-then-mock workflow available for HTTP. This is useful for capturing real service behaviour once and then testing offline against the recording.

Forwarding happens when a gRPC request either matches a forward expectation, or (in proxy mode) matches no expectation at all. MockServer re-encodes the request back into gRPC protobuf framing, relays it upstream over HTTP/2, then decodes the framed response and returns it to your client — all transparently, so your gRPC client is unaware a proxy is in the path.

Forward a specific method to an upstream gRPC server:

new MockServerClient("localhost", 1080)
    .when(
        request()
            .withPath("/com.example.grpc.GreetingService/Greeting")
    )
    .forward(
        forward()
            .withHost("upstream-grpc.example.com")
            .withPort(50051)
    );
var mockServerClient = require('mockserver-client').mockServerClient;
mockServerClient("localhost", 1080).mockAnyResponse({
    "httpRequest": {
        "path": "/com.example.grpc.GreetingService/Greeting"
    },
    "httpForward": {
        "host": "upstream-grpc.example.com",
        "port": 50051
    }
}).then(
    function () { console.log("expectation created"); },
    function (error) { console.log(error); }
);

See REST API for full JSON specification

from mockserver import MockServerClient, HttpRequest, HttpForward

client = MockServerClient("localhost", 1080)
client.when(
    HttpRequest(path="/com.example.grpc.GreetingService/Greeting")
).forward(
    HttpForward(host="upstream-grpc.example.com", port=50051)
)
require 'mockserver-client'
include MockServer

client = MockServer::Client.new('localhost', 1080)
client.when(
  HttpRequest.new(path: '/com.example.grpc.GreetingService/Greeting')
).forward(
  HttpForward.new(host: 'upstream-grpc.example.com', port: 50051)
)
import mockserver "github.com/mock-server/mockserver-monorepo/mockserver-client-go/v7"

client := mockserver.New("localhost", 1080)
client.When(
    mockserver.Request().Path("/com.example.grpc.GreetingService/Greeting"),
).Forward(
    mockserver.Forward().
        Host("upstream-grpc.example.com").
        Port(50051),
)
using MockServer.Client;
using MockServer.Client.Models;

using var client = new MockServerClient("localhost", 1080);
client.When(
    HttpRequest.Request().WithPath("/com.example.grpc.GreetingService/Greeting")
).Forward(
    HttpForward.Forward()
        .WithHost("upstream-grpc.example.com")
        .WithPort(50051)
);
use mockserver_client::{ClientBuilder, HttpRequest, HttpForward};

let client = ClientBuilder::new("localhost", 1080).build().unwrap();
client.when(
    HttpRequest::new().path("/com.example.grpc.GreetingService/Greeting")
).forward(
    HttpForward::new("upstream-grpc.example.com", 50051)
).unwrap();
use MockServer\MockServerClient;
use MockServer\HttpRequest;
use MockServer\HttpForward;

$client = new MockServerClient('localhost', 1080);
$client->when(
    HttpRequest::request()
        ->path('/com.example.grpc.GreetingService/Greeting')
)->forward(
    HttpForward::forward()
        ->host('upstream-grpc.example.com')
        ->port(50051)
);
curl -v -X PUT "http://localhost:1080/mockserver/expectation" -d '{
  "httpRequest": {
    "path": "/com.example.grpc.GreetingService/Greeting"
  },
  "httpForward": {
    "host": "upstream-grpc.example.com",
    "port": 50051
  }
}'

See REST API for full JSON specification

Provided a proto descriptor is loaded, the forwarded exchange is recorded with the decoded gRPC method path, status, and message JSON. Retrieve the recording as replayable expectations and re-apply it later to serve the same responses without the upstream:

// after driving traffic through the proxy, snapshot what was forwarded
Expectation[] recorded = new MockServerClient("localhost", 1080)
    .retrieveRecordedExpectations(
        request().withPath("/com.example.grpc.GreetingService/Greeting")
    );

// (later, offline) replay the captured responses as mocks
new MockServerClient("localhost", 1080).upsert(recorded);
var mockServerClient = require('mockserver-client').mockServerClient;
var client = mockServerClient("localhost", 1080);

// after driving traffic through the proxy, snapshot what was forwarded
client.retrieveRecordedExpectations({
    "path": "/com.example.grpc.GreetingService/Greeting"
}).then(function (recorded) {
    // (later, offline) replay the captured responses as mocks
    recorded.forEach(function (expectation) {
        client.mockAnyResponse(expectation);
    });
});
from mockserver import MockServerClient, HttpRequest

client = MockServerClient("localhost", 1080)

# after driving traffic through the proxy, snapshot what was forwarded
recorded = client.retrieve_recorded_expectations(
    HttpRequest(path="/com.example.grpc.GreetingService/Greeting")
)

# (later, offline) replay the captured responses as mocks
client.upsert(*recorded)
require 'mockserver-client'
include MockServer

client = MockServer::Client.new('localhost', 1080)

# after driving traffic through the proxy, snapshot what was forwarded
recorded = client.retrieve_recorded_expectations(
  request: HttpRequest.new(path: '/com.example.grpc.GreetingService/Greeting')
)

# (later, offline) replay the captured responses as mocks
client.upsert(*recorded)
import mockserver "github.com/mock-server/mockserver-monorepo/mockserver-client-go/v7"

client := mockserver.New("localhost", 1080)

// after driving traffic through the proxy, snapshot what was forwarded
recorded, _ := client.RetrieveRecordedExpectations(
    mockserver.Request().Path("/com.example.grpc.GreetingService/Greeting"),
)

// (later, offline) replay the captured responses as mocks
client.Upsert(recorded...)
using MockServer.Client;
using MockServer.Client.Models;

using var client = new MockServerClient("localhost", 1080);

// after driving traffic through the proxy, snapshot what was forwarded
var recorded = client.RetrieveRecordedExpectations(
    HttpRequest.Request().WithPath("/com.example.grpc.GreetingService/Greeting")
);

// (later, offline) replay the captured responses as mocks
client.Upsert(recorded.ToArray());
use mockserver_client::{ClientBuilder, HttpRequest};

let client = ClientBuilder::new("localhost", 1080).build().unwrap();

// after driving traffic through the proxy, snapshot what was forwarded
let recorded = client.retrieve_recorded_expectations(
    Some(&HttpRequest::new().path("/com.example.grpc.GreetingService/Greeting"))
).unwrap();

// (later, offline) replay the captured responses as mocks
client.upsert(&recorded).unwrap();
use MockServer\MockServerClient;
use MockServer\HttpRequest;
use MockServer\Expectation;

$client = new MockServerClient('localhost', 1080);

// after driving traffic through the proxy, snapshot what was forwarded
$recorded = $client->retrieveRecordedExpectations(
    HttpRequest::request()
        ->path('/com.example.grpc.GreetingService/Greeting')
);

// (later, offline) replay the captured responses as mocks
foreach ($recorded as $expectation) {
    $client->upsertExpectation(Expectation::fromArray($expectation));
}
# after driving traffic through the proxy, snapshot what was forwarded
curl -v -X PUT "http://localhost:1080/mockserver/retrieve?type=RECORDED_EXPECTATIONS&format=JSON" -d '{
  "path": "/com.example.grpc.GreetingService/Greeting"
}' > recorded-expectations.json

# (later, offline) replay the captured responses as mocks
curl -v -X PUT "http://localhost:1080/mockserver/expectation" -d @recorded-expectations.json

See REST API for full JSON specification

What is supported:

 

Limitations

 

gRPC Fault Injection (Chaos)

MockServer can inject gRPC-level faults — UNAVAILABLE, DEADLINE_EXCEEDED, RESOURCE_EXHAUSTED, and other status codes — into matched RPC calls, with optional latency and request-quota controls. Faults are registered per gRPC service name via a dedicated control-plane endpoint and apply before normal request conversion in GrpcToHttpRequestHandler.

This is separate from the health-check serving-status feature described below. See the gRPC Fault Injection section on the Chaos Testing page for the full profile reference and REST API examples.

 

gRPC Health Checking Protocol

Kubernetes readiness and liveness probes commonly use the gRPC Health Checking Protocol (grpc.health.v1.Health/Check) to verify that a service is ready to receive traffic. MockServer auto-responds to this well-known method without requiring a proto descriptor — protobuf encoding and decoding is handled manually so health checks work out of the box even when no descriptors have been loaded.

Health checking is always available — it is built in and cannot be disabled via a configuration property; MockServer answers any gRPC request whose content-type is gRPC and whose path matches the well-known health-check method. You can set the status for individual services, and for the overall server, via a REST endpoint.

A Check for a service that has never had a status registered fails with gRPC status NOT_FOUND, as the health-checking protocol requires — it does not report SERVING. This means a mistyped service name surfaces as an error rather than quietly looking healthy. Register each service whose health you want to check:

curl -v -X PUT "http://localhost:1080/mockserver/grpc/health" \
  -H "Content-Type: application/json" \
  -d '{"service": "my.payments.PaymentService", "status": "SERVING"}'

The overall-server health check — the one that uses the empty service name ("") — always answers and never returns NOT_FOUND. Its status defaults to SERVING and can be overridden. Note that setting the overall status applies only to that empty-name check; it is not a default inherited by named services.

Behaviour change. A Check for an unregistered service name previously returned the overall default status — usually SERVING — so a mistyped service name reported healthy, and a test asserting that a dependency was unhealthy passed without proving anything. If you relied on setting the overall status to cover every service, register each service explicitly with the PUT above.

ServingStatus values

Value Proto code Meaning
SERVING 1 The service is healthy and ready to accept requests (default)
NOT_SERVING 2 The service is temporarily unavailable (probe will fail)
UNKNOWN 0 Status is unknown
SERVICE_UNKNOWN 3 The named service is not known to this server. Used by the streaming Watch method; a Check for an unknown service fails with gRPC status NOT_FOUND instead of returning this value

REST API

Override the status for a named service:

curl -v -X PUT "http://localhost:1080/mockserver/grpc/health" \
  -H "Content-Type: application/json" \
  -d '{"service": "my.payments.PaymentService", "status": "NOT_SERVING"}'

Set service to the fully-qualified gRPC service name. Registering a service is also what makes it checkable at all — an unregistered name fails with NOT_FOUND. Use an empty string ("") to set the overall server status, which is the status reported to a health check that uses the empty service name; it is not inherited by named services. The response confirms the registration:

{ "status": "registered", "service": "my.payments.PaymentService", "servingStatus": "NOT_SERVING" }

Read all current status overrides:

curl -v "http://localhost:1080/mockserver/grpc/health"

Returns a JSON object mapping service names to their current status. The empty string key ("_default") shows the global default:

{
  "_default": "SERVING",
  "my.payments.PaymentService": "NOT_SERVING"
}

All status overrides are cleared on server reset. The GET endpoint returns only services that have had their status explicitly set, plus the global default.

How it works

When MockServer receives a request whose path is exactly /grpc.health.v1.Health/Check, the request is intercepted in GrpcToHttpRequestHandler before descriptor lookup — no proto descriptor for the health service is needed. GrpcHealthCheckHandler decodes the 5-byte gRPC frame header and then manually parses the protobuf HealthCheckRequest (field 1 = service name string). It then looks the service up in GrpcHealthRegistry. A named service that has no registered status fails the RPC with gRPC status NOT_FOUND and no message body; the empty service name always resolves, to the overall status. Otherwise the response is a manually-encoded gRPC-framed HealthCheckResponse (field 1 = status enum varint). The whole path bypasses the expectation matching engine so health checks always respond, even with no expectations registered.

 

Server Reflection

MockServer supports the gRPC Server Reflection Protocol out of the box. Tools such as grpcurl and grpcui can use reflection to discover services and describe message types without needing a local .proto file.

Both the v1 and v1alpha reflection service paths are supported:

The reflection service answers from the proto descriptors already loaded into MockServer (via grpcDescriptorDirectory, grpcProtoDirectory, or the REST API upload). No additional configuration is required.

Example: listing services with grpcurl

# List all services known to MockServer
grpcurl -plaintext localhost:1080 list

# Describe a specific service
grpcurl -plaintext localhost:1080 describe com.example.grpc.GreetingService

# Describe a message type
grpcurl -plaintext localhost:1080 describe com.example.grpc.HelloRequest

Limitation

MockServer's gRPC path is buffered-unary: each HTTP/2 request carries exactly one gRPC message. The reflection handler therefore processes a single ServerReflectionRequest per call. This is sufficient for grpcurl list, single symbol lookups, and single file lookups. Fully interactive bidirectional-streaming reflection (a long-lived stream with multiple back-and-forth messages) is not supported by the current pipeline.

 

gRPC-Web Support

MockServer supports gRPC-Web, the variant of gRPC designed for browser clients and environments that cannot use HTTP/2 trailers. gRPC-Web requests are automatically detected and translated to standard gRPC for matching against existing expectations, so no additional configuration or separate expectations are needed.

Supported Content Types

How It Works

When MockServer receives a request with a gRPC-Web content type:

  1. The request body is decoded (base64-decoded for the -text variant) and the content type is translated to application/grpc
  2. The request is processed through the normal gRPC pipeline — descriptor lookup, protobuf-to-JSON conversion, and expectation matching all work unchanged
  3. The response is re-framed as gRPC-Web: the message frame(s) are followed by a trailer frame (flag byte 0x80) containing grpc-status and grpc-message as ASCII lines in the body, instead of HTTP/2 trailers
  4. For the -text variant, the entire response body is base64-encoded

gRPC-Web works over both HTTP/1.1 and HTTP/2, making it suitable for browser-based gRPC clients such as grpc-web and Improbable grpc-web.

Built-in Services via gRPC-Web

The built-in gRPC health check (/grpc.health.v1.Health/Check), server reflection, and chaos fault injection all work transparently via gRPC-Web. No special configuration is needed.

Connect Protocol (connectrpc)

The Connect protocol (used by connectrpc) is not currently supported. Connect uses a different framing format (JSON or proto over standard HTTP POST with application/connect+proto content type and trailers in a JSON envelope) that is distinct from gRPC-Web. If you need Connect support, please open a feature request.

 

gRPC over HTTP/3

gRPC works over HTTP/3 (QUIC) as well as HTTP/2 — set the http3Port configuration property to start the HTTP/3 listener and clients can make gRPC calls over QUIC. Unary, server-streaming, and bidirectional-streaming gRPC all work over HTTP/3 with the correct trailing-HEADERS grpc-status framing. As on the TCP path, bidirectional streaming requires the grpcBidiStreamingEnabled=true flag. Expectations are matched identically regardless of whether the call arrives over HTTP/2 or HTTP/3 — no separate or HTTP/3-specific expectations are needed.

See the HTTP/3 (QUIC) Support page for details on enabling and tuning the HTTP/3 listener.

 

Configuration

For full details on gRPC configuration properties, see the gRPC Configuration section on the Configuration page.