# Protobuf / gRPC

Source: https://apigenie.pl/docs/reference/server/grpc
Part of the reference of [apigenie-server](https://apigenie.pl/docs/reference/server.md).
Its other pages: [Responses](https://apigenie.pl/docs/reference/server/responses.md), [Scenarios and extensions](https://apigenie.pl/docs/reference/server/scenarios.md), [Scripts](https://apigenie.pl/docs/reference/server/scripts.md), [Document store](https://apigenie.pl/docs/reference/server/store.md), [Several services](https://apigenie.pl/docs/reference/server/services.md), [Validation and security](https://apigenie.pl/docs/reference/server/validation.md), [Contract testing (validating proxy)](https://apigenie.pl/docs/reference/server/contract-testing.md), [Session recording and replay](https://apigenie.pl/docs/reference/server/recording.md), [AsyncAPI](https://apigenie.pl/docs/reference/server/asyncapi.md), [GraphQL](https://apigenie.pl/docs/reference/server/graphql.md), [Configuration](https://apigenie.pl/docs/reference/server/configuration.md), [Dashboard and admin API](https://apigenie.pl/docs/reference/server/dashboard.md), [TLS](https://apigenie.pl/docs/reference/server/tls.md).

For a `.proto` file the mock serves its services over gRPC, on the same port as HTTP: clients
connect with cleartext HTTP/2 (`-plaintext` in grpcurl, `createInsecure()` in grpc-js,
`--http2-prior-knowledge` in buf curl), or over TLS when the mock has `--tls-cert` (grpcurl
without `-plaintext`, `createSsl()` in grpc-js; see [TLS](/docs/reference/server/tls)). Imports are looked up in `--proto-path` directories, then
next to the file, then among the embedded standard files: the well-known types,
`google/api/annotations.proto`, `google/rpc/status.proto`, `code.proto` and
`error_details.proto`.

```sh
apigenie-server greeter.proto --stubs stubs/
buf curl --protocol grpc --http2-prior-knowledge -d '{"name":"Ann"}' \
  http://127.0.0.1:4010/acme.greeter.v1.Greeter/SayHello
```

```text
Listening on http://127.0.0.1:4010 (spec: greeter.proto, 1 service, 3 methods, gRPC over HTTP/2)
```

Unary, server-streaming, client-streaming and bidirectional methods are served. A call is answered
by the first of:

1. Its `x-apigenie-mock-*` metadata (below).
2. A script's `onRequest`.
3. The rules of the active scenarios.
4. The stubs.
5. The upstream of `--proxy-to`.
6. Messages generated from the response type, seeded by `--seed`: one, or one to three for a
   server stream; with `grpc.generation: false`, UNIMPLEMENTED. Fields marked
   `(google.api.field_behavior) = REQUIRED` are always set (not to their default value; lists
   and string- or bool-keyed maps get an item), except where that would make a message contain
   itself; fields marked `INPUT_ONLY` are left out.

The response metadata `x-apigenie-mock-source` (`stub:hello-ann`, `generated`, `proxy`,
`scenario:outage`) and `x-apigenie-mock-stub` say which one it was.

## Stubs

Stub files are YAML or JSON: a list, `{stubs: [...]}`, or a single stub. Their JSON Schema is
[apigenie-mock-grpc-stubs.json](/schemas/apigenie-mock-grpc-stubs.json); add
`# yaml-language-server: $schema=https://apigenie.pl/schemas/apigenie-mock-grpc-stubs.json` to a
file for completion in editors.

```yaml
stubs:
  - name: hello-ann
    method: acme.greeter.v1.Greeter/SayHello
    priority: 10
    request:
      equals: {name: Ann}
    response:
      headers: {x-greeting: formal}
      message: {message: "Hello Ann", sentAt: "2026-10-04T10:00:00Z", mood: MOOD_HAPPY}
      trailers: {x-cost: "1"}

  - name: hello-tenant
    method: acme.greeter.v1.Greeter/SayHello
    request:
      contains: {name: Bob}
      metadata:
        equals: {x-tenant: acme}
    response:
      message: {message: "Hello Bob of {{metadata.x-tenant}}"}

  - name: hello-empty
    method: acme.greeter.v1.Greeter/SayHello
    request:
      equals: {}
    response:
      status:
        code: INVALID_ARGUMENT
        message: name is required
        details:
          - "@type": type.googleapis.com/google.rpc.BadRequest
            fieldViolations: [{field: name, description: must not be empty}]

  - name: hello-all
    method: acme.greeter.v1.Greeter/SayHelloToAll
    response:
      each: {message: {message: "Hi {{message.name}}"}}
```

- **`method`** is `package.Service/Method` (or `method: Method` with `service:
  package.Service`). An unknown method stops the start with exit status 2 and the methods there
  are.
- **`request`** matches the request message, written as proto3 JSON: `equals` compares canonical
  forms, so default values may be left out; `contains` matches a subset, recursively (arrays as
  subsets, `ignoreArrayOrder` for any order); `matches` holds regular expressions for string
  fields. `request.metadata` matches the metadata the same three ways.
- **`requests`** matches the messages of a client stream, after the client has finished sending:
  a list matched one by one, or `{every: matcher}`.
- **`priority`** (higher first, then the order of the files) and **`times`** (how many calls a
  stub answers before it's skipped) order the candidates.
- **`response`** has `headers`, `message` or `messages` (a list of `{message, delay}` for a
  stream), `each` (bidirectional streams: one answer per message received), `trailers`, `status`
  (`code` as a name or a number, `message`, `details` as `google.protobuf.Any` JSON), `delay`
  (milliseconds), `drop` (`reset`, `close`, `timeout`, `truncate`) and `generate: true` (fill
  the fields the message doesn't give).
- **Templates.** String values can use `{{request.name}}`, `{{requests[1].name}}`,
  `{{requests.length}}`, `{{message.name}}` (the message `each` answers), `{{metadata.x-tenant}}`
  and `{{fake:person.firstName}}`. A value that is a template alone keeps the type of what it
  refers to.

## Metadata directives

| Metadata | Effect |
| --- | --- |
| `x-apigenie-mock-code: NOT_FOUND` | End the call with that status (a name or a number). |
| `x-apigenie-mock-message: text` | The status message that goes with it. |
| `x-apigenie-mock-stub: hello-ann` | Answer with that stub. |
| `x-apigenie-mock-delay: 1500` | Answer after 1.5 seconds. |
| `x-apigenie-mock-drop: reset` | Reset the connection (`close`, `timeout` and `truncate` too). |
| `x-apigenie-mock-scenario: outage` | Apply that scenario to this call. |
| `x-apigenie-mock-dynamic: true` | Generate the answer. |

## What a gRPC server does

- `grpc-timeout` sets a deadline: a call that would answer later ends with DEADLINE_EXCEEDED.
- Messages larger than `grpc.max_message_bytes` (4 MiB) fail with RESOURCE_EXHAUSTED; compressed
  ones with UNIMPLEMENTED and `grpc-accept-encoding: identity`; undecodable ones with
  INVALID_ARGUMENT. An unknown method is UNIMPLEMENTED.
- Requests are validated against their message type (`grpc.validation.request`): a violation
  is INVALID_ARGUMENT with a `google.rpc.BadRequest` in `grpc-status-details-bin`. Fields marked
  `[(google.api.field_behavior) = REQUIRED]` must be set (see
  [gRPC contract testing](#contract-testing-of-a-grpc-api)).
- Errors are sent as Trailers-Only responses; `grpc-message` is percent-encoded, `-bin` metadata
  base64-encoded.
- `grpc.reflection.v1` and `v1alpha` let grpcurl, buf curl, Postman, Kreya and Bruno list the
  services and fetch their descriptors; `grpc.health.v1.Health` answers `Check` and `Watch` with
  SERVING for every service and for `""`.
- Scenario rules match `operationId` (`acme.greeter.v1.Greeter/SayHello`), `path`, or the tags
  `grpc` and the service's name, and set `grpc_status` and `grpc_message` besides delays and drops:

```yaml
scenarios:
  outage:
    rules:
      - match: {operationId: acme.greeter.v1.Greeter/SayHello}
        grpc_status: UNAVAILABLE
        grpc_message: the greeter is down
```

## Proxy and record

```sh
apigenie-server greeter.proto --proxy-to greeter.internal:50051 --record-dir recorded/
apigenie-server greeter.proto --stubs recorded/
```

With `--proxy-to`, calls no stub answers go to the upstream server (every call with
`grpc.proxy.mode: always`), with their metadata and deadline; an unreachable upstream is
UNAVAILABLE. `--record-dir` writes each proxied call as a stub file
(`Greeter.SayHello-1.yaml`, ...) that matches its request and gives the upstream's answer.
Transport headers are left out, and so is the metadata of `grpc.proxy.redact`
(`authorization`, `cookie`, `x-api-key` and `proxy-authorization` by default).

## Contract testing of a gRPC API

The proxy checks both sides of every call it forwards against the `.proto` file. Point your
clients, tests or a load test at the mock, and the traffic becomes a contract test of the server:

```sh
apigenie-server greeter.proto --proxy-to greeter.internal:50051 --report contract.json --report-junit contract.xml --fail-on errors
```

Messages are decoded with their method's types and checked against them: their JSON Schema (the
proto3 JSON form), the fields marked `[(google.api.field_behavior) = REQUIRED]` (proto3 has no
required fields; a REQUIRED field without explicit presence must not have its default value), and
deprecated fields and enum values. The status of the answer is checked against the gRPC protocol
and the `google.rpc` conventions. Each violation has a check and a severity that
`contract.severity` changes:

| Check | Severity | What it means |
| --- | --- | --- |
| `request.method.unknown` | warn | The call is for a method the file doesn't declare; it is answered UNIMPLEMENTED, not forwarded |
| `request.message.malformed` | error | A request message can't be decoded as the input type |
| `request.message.incompatible` | error | A request message doesn't match its type, or misses a REQUIRED field. A field that came with another wire type than its declared type's (a string where an `int64` is declared) is dropped by decoding, as protobuf parsers do, and reported with the code `wire-type` |
| `request.message.deprecated` | info | A request sets a deprecated field or enum value |
| `response.status.invalid` | error | An HTTP status other than 200, a content type other than `application/grpc`, a missing or unknown `grpc-status`, a `grpc-message` that isn't percent-encoded |
| `response.status.details-incompatible` | error | `grpc-status-details-bin` isn't a base64 `google.rpc.Status`, or its `code` isn't `grpc-status`; a warning for a detail of a type the file doesn't know |
| `response.status.server-error` | info | The upstream answered UNKNOWN, INTERNAL, UNAVAILABLE, DATA_LOSS or UNIMPLEMENTED |
| `response.message.malformed` | error | A response message can't be decoded as the output type |
| `response.message.incompatible` | error | A response message doesn't match its type, or misses a REQUIRED field. A field that came with another wire type than its declared type's (a string where an `int64` is declared) is dropped by decoding, as protobuf parsers do, and reported with the code `wire-type` |
| `response.message.count` | error | A unary or client-streaming call ended OK without exactly one message; a warning for messages before an error status |
| `response.message.deprecated` | info | A response sets a deprecated field or enum value |
| `response.metadata.invalid` | warn | A `-bin` metadata value isn't base64, or a metadata name isn't lowercase letters, digits, `-`, `_` and `.` |

Locations are the message's index and a JSON pointer, `message[0]/items/2/sku`; a message that
can't be decoded is located at the field the decoder stopped at.

**Report or enforce.** With `--proxy-to`, `grpc.validation.request` and `grpc.validation.response`
are `log` unless the configuration sets them: violations are logged and reported, and calls go
through as they are, since test suites send invalid calls on purpose. `--strict` (or `enforce`)
checks each request message before it goes to the upstream (a violation is INVALID_ARGUMENT with a
`google.rpc.BadRequest`) and each upstream message before it goes to the client: a violation ends
the call with INTERNAL, `the upstream's answer isn't a valid acme.greeter.v1.HelloReply: ...`.
Enforcing holds each message until it is whole, so streams lag by one message. Without
`--proxy-to`, requests are enforced as before.

**Coverage and reports.** The mock counts the calls of each method of the file (built-in reflection
and health aside): `covered` when a call had no error violation, whatever its status, `partial`
when every call had one, `not-tested` otherwise. Mocked calls count too, under `sources`.
`GET /__apigenie/contract` is the report; when the server stops it prints a summary, writes
`--report` and `--report-junit` (a test case per method, named by service and method, one per
undocumented method and one for the coverage threshold) and exits with 5 when `--fail-on` or
`--min-coverage` (the share of methods) isn't met. The JSON report has
`"specification": "grpc"`, `summary` (`calls`, `documented`, `undocumented`, `sources`,
`messages`, `violations`, `coverage.methods`, `gate`), `services`, `methods` (each with its
`kind`, `status`, `calls`, `statuses` by status name, `messages` and `violations` by check),
`undocumented` and `violations` (with the `method` and the `direction`, `in` or `out`).

The upstream is `HOST:PORT` for cleartext HTTP/2, or `https://host[:port]` for HTTP/2 over TLS
(see [TLS](/docs/reference/server/tls)). Compressed messages (an encoding the client offered and the server chose) are
forwarded unchecked. [`apigenie test`](/docs/reference/cli/test)
sends `GRPC` requests of `.http` files with the same checks and reports.

## Scripts and the log

Scripts see a gRPC call as a `POST` to `/package.Service/Method` whose `operationId` is
`package.Service/Method`, with the metadata as headers and the message as a JSON body. In
`onResponse`, the status is in the `grpc-status` and `grpc-message` headers and the body is the
first message; `onMessage` gets each message of a stream, with `direction` `in` or `out`.

The log has a `grpc` event for each call with its status `code`, and `grpc-in` and `grpc-out`
events for streamed messages.
