# server

Source: https://apigenie.pl/docs/reference/server

`apigenie-server` serves an OpenAPI 3.0, 3.1 or 3.2 document (or a Swagger 2.0 one, see
[Swagger 2.0 documents](#swagger-20-documents)) as a mock API, the channels of an
AsyncAPI 3.x document over WebSocket, HTTP and MQTT, the gRPC services of a `.proto` file, or
a GraphQL schema as a GraphQL endpoint with subscriptions. It answers with the document's
examples, with stubs, or with data generated from its schemas, validates requests, responses
and messages, logs every exchange, and can make the network slow or unreliable on purpose. In
front of a real server (`--proxy-to`) it becomes a validating proxy that contract-tests the
server's traffic against the OpenAPI or Swagger document, the GraphQL schema, the AsyncAPI
document (WebSocket, MQTT and HTTP channels) or the `.proto` file, with coverage and JSON or JUnit
reports. It is
installed next to `apigenie` by every package (archives, `.deb`, `.rpm`, npm, Homebrew) and has a
Docker image of its own.

## Usage

```sh
apigenie-server [OPTIONS] SPEC...
apigenie-server session export DIR --har FILE
```

```sh
apigenie-server openapi.yaml
curl http://127.0.0.1:4010/pets
```

The first line the server writes says where it listens:

```text
Listening on http://127.0.0.1:4010/v1 (spec: openapi.yaml, 12 operations)
Admin API: http://127.0.0.1:4010/__apigenie
Dashboard: http://127.0.0.1:4010/__apigenie/ui
```

The [dashboard](#dashboard) shows the traffic as it happens, the operations and what answers
them, the scenarios, the contract report, the data and, for an AsyncAPI document, the channels
and their clients, in the browser.

A GraphQL schema (`.graphql`, `.graphqls`, `.gql`) is served at `/graphql`:

```sh
apigenie-server schema.graphql
curl -X POST http://127.0.0.1:4010/graphql -H 'Content-Type: application/json' \
  -d '{"query": "{ books { title author { name } } }"}'
```

```text
Listening on http://127.0.0.1:4010/graphql (spec: schema.graphql, 12 operations)
GraphiQL: http://127.0.0.1:4010/graphql
Subscriptions: ws://127.0.0.1:4010/graphql
Admin API: http://127.0.0.1:4010/__apigenie
Dashboard: http://127.0.0.1:4010/__apigenie/ui
```

In front of a real server it checks the traffic against the document instead of mocking it
([contract testing](#contract-testing-validating-proxy)):

```sh
apigenie-server openapi.yaml --proxy-to http://localhost:8080
```

```text
Listening on http://127.0.0.1:4010/v1 (spec: openapi.yaml, 12 operations)
Proxy: http://localhost:8080 (requests: log, responses: log, unknown routes: forward, mode: always)
Admin API: http://127.0.0.1:4010/__apigenie
Dashboard: http://127.0.0.1:4010/__apigenie/ui
```

With `--log-format json` it is a JSON object (`{"event":"listening","url":...,"port":...}`), and
every request after it is logged as one JSON object per line, which suits tools and tests.

Several documents are served together on one port, each below a prefix of its own
([several services](#several-services)):

```sh
apigenie-server pets.yaml orders.yaml events.asyncapi.yaml
```

```text
Listening on http://127.0.0.1:4010 (3 services)
  pets    http://127.0.0.1:4010/pets/v1 (spec: pets.yaml, 12 operations)
  orders  http://127.0.0.1:4010/orders (spec: orders.yaml, 5 operations)
  events  http://127.0.0.1:4010/events (spec: events.asyncapi.yaml, 3 channels)
Admin API: http://127.0.0.1:4010/__apigenie
Dashboard: http://127.0.0.1:4010/__apigenie/ui
```

## Options

| Option | Default | Description |
| --- | --- | --- |
| `--host HOST` | `127.0.0.1` | The address to listen on; `0.0.0.0` or `::` for every interface. Also `APIGENIE_SERVER_HOST`. |
| `--port`, `-p PORT` | `4010` | The port; `0` picks a free one. Also `APIGENIE_SERVER_PORT`. |
| `--base-path PATH` | the servers' paths | Serve the operations under this prefix instead of the paths of the document's `servers`. |
| `--config`, `-c FILE` | see below | The configuration file. |
| `--no-config` | | Don't look for a configuration file. |
| `--watch`, `-w` | | Reload when the document or the configuration changes. A broken version is reported and the previous one stays. |
| `--seed N` | random | Seed of generated data and simulated failures: the same seed repeats a run. |
| `--dynamic`, `-d` / `--static` | `--static` | Generate every response from its schema, or answer with examples first. |
| `--strict` | | Answer 500 when a response the mock makes doesn't match the document. With `--proxy-to`: answer requests that don't match the document or schema without the upstream (422 for OpenAPI, GraphQL errors for GraphQL) and replace upstream responses that don't (502); AsyncAPI: messages that don't match aren't forwarded, the client gets a problem message instead; gRPC: requests and upstream messages are checked before they are forwarded, and a call that breaks the contract ends with INVALID_ARGUMENT or INTERNAL. |
| `--log-format FORMAT` | `stylish` | `stylish` or `json`. |
| `--log-level LEVEL` | `info` | `debug`, `info`, `warn` or `error`. |
| `--log-file FILE` | | Also write the log to a file, as JSON lines. |
| `--log-bodies`, `--log-headers` | | Log request and response bodies, request headers. |
| `--quiet`, `-q` | | Only the first line on the standard output. |
| `--latency MS` | | Delay every response: a number of milliseconds, or `MIN-MAX`. |
| `--jitter MS` | | A random extra delay of up to this many milliseconds. |
| `--drop-rate RATE` | | The share of responses (0 to 1) whose connection is reset. |
| `--cors on\|off` | `on` | Answer CORS preflights and add CORS headers. |
| `--admin on\|off` | `on` | The admin API under `/__apigenie`, and the [dashboard](#dashboard) at `/__apigenie/ui`. |
| `--scenario NAME` | | Switch on a scenario of the configuration or the document; can be repeated. |
| `--script FILE` | | A JavaScript module with hooks (see below); can be repeated. |
| `--crud` | | Serve the collection resources of an OpenAPI document (`/things` and `/things/{id}`) from the [document store](#crud-resources): what a `POST` creates, a `GET` lists. The same as `db.crud: auto`. |
| `--db-file FILE` | | Keep the [document store](#document-store) in this JSON file: loaded at the start, written when the server stops. Also `APIGENIE_SERVER_DB_FILE`. |
| `--asyncapi-server NAME` | every server | AsyncAPI: serve this server of the document only. |
| `--mqtt-port PORT` | `1883`, `8883` with `--tls-cert` | AsyncAPI: the port of the MQTT broker; `0` picks a free one. Also `APIGENIE_SERVER_MQTT_PORT`. |
| `--proto-path`, `-I DIR` | the file's directory | gRPC: where imports are looked up, like protoc's `--proto_path`; can be repeated. Also `APIGENIE_SERVER_PROTO_PATH` (separated by `:`, `;` on Windows). |
| `--stubs FILE\|DIR` | | A YAML or JSON stub file, or a directory of them; can be repeated. gRPC: stubs (see [Stubs](#stubs)). OpenAPI: the exchanges `--record-dir` wrote (see [Record and replay](#contract-testing-validating-proxy)). Also `APIGENIE_SERVER_STUBS`. |
| `--proxy-to TARGET` | | OpenAPI: the real server (`http[s]://host[:port][/prefix]` or `HOST:PORT`) that gets every request, both checked against the document (see [Contract testing](#contract-testing-validating-proxy)). GraphQL: the real server's endpoint (`https://host/graphql`), requests and answers checked against the schema (see [GraphQL contract testing](#contract-testing-of-a-graphql-api)). AsyncAPI: the real WebSocket and HTTP server (`ws[s]://host[:port][/prefix]` or `http[s]://...`) and the real MQTT broker (`mqtt[s]://host[:port]`), one or both (repeat the option), the messages of both sides checked against the document (see [WebSocket contract testing](#contract-testing-of-a-websocket-api), [MQTT contract testing](#contract-testing-of-mqtt-channels) and [HTTP channels](#contract-testing-of-http-channels)). gRPC: an upstream server for the calls no stub answers, `HOST:PORT` for cleartext HTTP/2 or `https://host[:port]` for HTTP/2 over TLS, its answers checked against the `.proto` file (see [gRPC contract testing](#contract-testing-of-a-grpc-api)). `https://`, `wss://` and `mqtts://` upstreams are verified (see [TLS](#tls)). Also `APIGENIE_SERVER_PROXY_TO` (one target). |
| `--tls-cert FILE` | | Serve HTTPS, WSS, gRPC and MQTT over TLS with this PEM certificate chain (the server's certificate first); needs `--tls-key`. See [TLS](#tls). Also `APIGENIE_SERVER_TLS_CERT`. |
| `--tls-key FILE` | | The unencrypted PEM private key of `--tls-cert`. Also `APIGENIE_SERVER_TLS_KEY`. |
| `--tls-client-ca FILE` | | Require a client certificate issued by the authorities of this PEM file on every HTTPS, WSS, gRPC and MQTTS connection (mTLS); the subject goes into the log and the contract report. Needs `--tls-cert`. See [Client certificates](#client-certificates-mtls). Also `APIGENIE_SERVER_TLS_CLIENT_CA`. |
| `--ca-cert FILE` | the system's roots | A PEM file of certificate authorities trusted for `https://`, `wss://` and `mqtts://` upstreams, besides the system's; can be repeated. Also `APIGENIE_SERVER_CA_CERT` (separated by `:`, `;` on Windows). |
| `--insecure` | | Don't verify the certificates of `https://`, `wss://` and `mqtts://` upstreams; the traffic stays encrypted. Also `APIGENIE_SERVER_INSECURE=1`. |
| `--client-cert FILE` | | A PEM client certificate (chain) presented to `https://`, `wss://`, `mqtts://` and gRPC TLS upstreams that ask for one (mTLS); needs `--client-key`. Also `APIGENIE_SERVER_CLIENT_CERT`. |
| `--client-key FILE` | | The unencrypted PEM private key of `--client-cert`. Also `APIGENIE_SERVER_CLIENT_KEY`. |
| `--proxy-mode always\|fallback` | OpenAPI and GraphQL `always`, gRPC `fallback` | `always`: the upstream gets every request. `fallback`: OpenAPI: every request still goes to the server, and the mock answers when the server can't be reached, doesn't answer in time, or answers 404 or 501 for an operation of the document (see [Fallback mode](#contract-testing-validating-proxy)); GraphQL: the mock's examples, scenarios and scripts answer first; gRPC: stubs answer first. An AsyncAPI document's proxy relays every message. |
| `--record-dir DIR` | | OpenAPI: write the exchanges the server answered as stub files, replayed with `--stubs`. gRPC: write the proxied calls as stub files. GraphQL: write the proxied answers as examples. Needs `--proxy-to`. Also `APIGENIE_SERVER_RECORD_DIR`. |
| `--record-har FILE` | | OpenAPI: write every exchange through the proxy (the server's answers and the mock's own) as a HAR 1.2 file when the server stops. Needs `--proxy-to`. Also `APIGENIE_SERVER_RECORD_HAR`. |
| `--record DIR` | | Record the session: every service's exchanges and messages in the order they happened, each as a file, and `session.yaml`; an exchange's test is its `X-Apigenie-Test` header (see [Session recording](#session-recording)). Every document, with or without `--proxy-to`. The directory must be new or empty. Also `APIGENIE_SERVER_RECORD`. |
| `--record-redact HEADER\|/POINTER` | | What `--record` hides besides `Authorization`, `Cookie`, `Set-Cookie`, `Proxy-Authorization` and `X-Api-Key`: a header name, or a JSON pointer (`/password`) to a value of JSON bodies and messages; can be repeated. |
| `--replay DIR` | | Answer from the session `--record` wrote: each request gets the next unused recorded exchange of its test (`X-Apigenie-Test`) that matches it, on every protocol (see [Session replay](#session-replay)). When the server stops, exit status 5 if a recorded exchange was never replayed or a request matched none. Also `APIGENIE_SERVER_REPLAY`. |
| `--replay-ignore HEADER\|/POINTER` | | What `--replay` doesn't compare besides dates, `readOnly` fields and volatile headers: a header name, or a JSON pointer (`/id`, `/items/*/id`) into JSON bodies and messages; can be repeated. |
| `--replay-missing fail\|generate\|passthrough` | `fail` | What a request no recorded exchange matches gets: a 502 problem naming the closest recording and how the request differs (`fail`), the mock's own answer (`generate`), or the answer of `--proxy-to` (`passthrough`). |
| `--replay-update` | | With `--replay-missing passthrough`: record the run and, when the server stops, replace the `--replay` session with it. |
| `--proxy-unknown forward\|reject` | `forward` | OpenAPI proxy: forward requests for no operation of the document (reported), or answer them with 404 or 405. |
| `--report FILE` | | Write the contract report (coverage and violations) as JSON when the server stops. Also `APIGENIE_SERVER_REPORT`. |
| `--report-junit FILE` | | Write the contract report as JUnit XML when the server stops. Also `APIGENIE_SERVER_REPORT_JUNIT`. |
| `--fail-on none\|errors\|warnings` | `none` | Exit with 5 when the server stops after violations of that severity. |
| `--min-coverage PERCENT` | | Exit with 5 when the server stops with less of the document covered (a GraphQL schema: its fields; an AsyncAPI document: its operations' messages; a `.proto` file: its methods; [several services](#several-services): their combined coverage). |
| `--credits`, `--license [LIBRARY]`, `--license-list` | | Like `apigenie credits` and `apigenie license`. |
| `--help`, `-h` / `--version`, `-v` | | Show the help (with the exit statuses and the per-request directives), or the version. |

## Exit status

| Code | Meaning |
| --- | --- |
| 0 | Stopped with Ctrl+C (or `POST /__apigenie/shutdown`) |
| 2 | Bad options, configuration, scripts or stub files, or an unexpected error |
| 3 | The document can't be served (missing, unreadable, not OpenAPI 3, Swagger 2.0 or AsyncAPI 3, no channels the mock serves, a `.proto` file with errors, missing imports or no services) |
| 4 | The address can't be used (the port is taken, ...) |
| 5 | The contract report failed when the server stopped (`--fail-on`, `--min-coverage`), or the replay did (`--replay`: a recorded exchange was never replayed, or a request matched none) |
| 130 | Stopped at once with a second Ctrl+C |

## Swagger 2.0 documents

A Swagger 2.0 document (`swagger: "2.0"`) is served, proxied and contract-tested like the
OpenAPI 3.2 document `apigenie convert --to openapi32` would make of it. The
conversion happens in memory each time the document is read (at startup and on reloads); no file
is written. Body and form parameters become request bodies, `produces` and `consumes` become
media types, `host`, `basePath` and `schemes` become the servers, and `definitions`, `parameters`,
`responses` and `securityDefinitions` move into `components`.

Locations stay those of the Swagger file: warnings about the document give its lines, the problems
of `x-apigenie-mock` extensions its lines and JSON pointers, and the contract report's `pointer`
of each operation is a pointer into it. What the conversion can only approximate (a
`collectionFormat` without counterpart, form parameters next to a body parameter, ...) is told
once at startup, on the standard error, as an `info:` line with the file's line, column and JSON
pointer:

```text
info: pets.yaml:7:71: /paths/~1pets/get/parameters/0/collectionFormat: collectionFormat tsv has no counterpart and becomes style form (csv)
```

`GET /__apigenie/spec` returns the converted document, which is what is served. The document is
converted alone: one that references other files can't be served (exit status 3) until
`apigenie bundle` has put them into it.

## Responses

- **Routing.** Paths are served under the path of the document's first server (`/v1` for
  `https://api.example.com/v1`) and as they are. Literal paths win over templated ones
  (`/pets/mine` over `/pets/{id}`). An unknown path is answered with 404, an unknown method with
  405 and an `Allow` header, both as [problem details](https://www.rfc-editor.org/rfc/rfc9457).
  OpenAPI 3.2's `query` method and the methods of `additionalOperations` (`COPY`, ...) are routed
  like the others.
- **Status.** The lowest 2xx response, else `2XX`, else the lowest declared, else `default`.
- **Body.** Content negotiation follows `Accept` (406 when nothing fits). Without an `Accept`, or
  with `*/*`, the response takes the media type of the request's body when it has it: a SOAP 1.2
  request (`application/soap+xml`) to an operation answering both SOAP versions gets a SOAP 1.2
  envelope. In the default static
  mode the media type's `example`, its first named example, or the schema's example answer; what
  has none is generated from the schema. Generated values respect types, the formats of the
  OpenAPI format registry (`uuid`, `date-time`, `int8`, `decimal`, `http-date`, ...; see
  [Formats](/docs/reference/cli/formats)), patterns, lengths, ranges, `enum`, `const`, `allOf`,
  `oneOf`, `readOnly` and `writeOnly`. A value for `application/xml`, `text/xml` or a `+xml`
  type is written as XML (see [XML bodies](#xml-bodies)); a string example is sent as it is.
- **Headers.** Response headers are filled in from their examples or schemas.
  `X-Apigenie-Mock-Operation` and `X-Apigenie-Mock-Source` (`example:cat`, `generated`,
  `scenario:outage`, `stub:getPet-1`) say where a response came from.
- **Stubs.** `--stubs DIR` (or `stubs:` in the configuration) answers with recorded exchanges
  (see [Record and replay](#contract-testing-validating-proxy)) before the document's examples:
  a request's `Prefer` and the rules of scenarios and overrides still come first.

## Choosing a response per request

The `Prefer` header (or query parameters, which are removed before validation) asks for a
particular answer:

| `Prefer` | Query | Effect |
| --- | --- | --- |
| `code=404` | `__code=404` | That status (500 when the operation doesn't declare it). |
| `example=cat` | `__example=cat` | That named example. |
| `dynamic=true` | `__dynamic=true` | A generated body. |
| `delay=1500` | `__delay=1500` | Answer after 1.5 seconds. |
| `drop=reset` | | Reset the connection (`close`, `timeout` and `truncate` too). |
| `scenario=outage` | | Apply that scenario to this request. |

## Validation and security

Requests are checked against their operation: path, query, header and cookie parameters in every
style, and JSON, XML, form and multipart bodies. A request that doesn't match is answered with 422
and the problems, a body that can't be parsed (JSON, XML, multipart) with 400, an unknown media type
with 415. Security
requirements are checked for presence (API keys, HTTP basic and bearer, OAuth2 and OpenID Connect
tokens; 401 with `WWW-Authenticate`), and against configured credentials in `enforce` mode (403).
Responses the mock makes are checked too: problems are logged and counted in
`X-Apigenie-Mock-Violations`, or answered with 500 under `--strict`.

Schemas' `format` values are checked as the
[OpenAPI format registry](/docs/reference/cli/formats) defines them, for the JSON types each
format applies to (`int8` ranges, `http-date`, `decimal`, `language`, ...);
`validation.formats: false` turns these checks off. Formats of your own, declared in
[`openapi.custom_formats`](/docs/reference/cli/configuration#openapicustom_formats) of the
configuration file, are accepted as they are, and an entry there naming a registry format
replaces that format's type and its check.

### XML bodies

Bodies of `application/xml`, `text/xml` and `+xml` media types are parsed as XML 1.0 with
namespaces, mapped to a JSON value as the schema's `xml` objects describe it, and checked against
the schema like JSON bodies; the mock, the validating proxy and `apigenie test` do the same, and
the mock and `apigenie test --generate` write XML the same way in reverse. For this schema

```yaml
Order:
  type: object
  xml: {name: order, namespace: "urn:orders", prefix: o}
  properties:
    id: {type: integer, xml: {nodeType: attribute}}   # OpenAPI 3.0/3.1: attribute: true
    customer: {$ref: "#/components/schemas/Customer"} # Customer has xml: {name: Buyer}
    note: {type: string, xml: {nodeType: cdata}}
    tags: {type: array, items: {type: string, xml: {name: tag}}}
    lines:
      type: array
      xml: {name: Lines, nodeType: element}           # 3.0/3.1: wrapped: true
      items: {$ref: "#/components/schemas/Line"}      # Line has xml: {name: Line}
```

the body is

```xml
<o:order xmlns:o="urn:orders" id="7">
  <Buyer><name>Ann</name></Buyer>
  <![CDATA[Leave at the <door>]]>
  <tag>a</tag><tag>b</tag>
  <Lines><Line sku="x"><quantity>2</quantity></Line></Lines>
</o:order>
```

- The root element is named by the schema's `xml.name`, else by the component a `$ref` points to;
  another name is a `xml-root` violation. A schema without either names no root.
- A property is a child element named by its `xml.name` (its own, or that of the schema it
  references), else by the property name. `nodeType: attribute` makes it an attribute, `text`
  and `cdata` the element's character data.
- An array's items repeat in the parent element, named by the items' `xml.name` or the property
  name; with `nodeType: element` (`wrapped: true`) they sit in a wrapper element.
- A declared `xml.namespace` must match; without one, elements are matched by local name in any
  namespace. An element with a property's local name in another namespace isn't that property: it
  is kept as `{namespace}name`, so the property is missing and `additionalProperties: false`
  reports the element.
- Child elements are written in the order the schema declares its properties, as XML Schema
  sequences require.
- Text becomes an integer, number, boolean or null where the schema allows it (white space around
  such values is ignored); `xsi:nil="true"` is null. Elements and attributes the schema doesn't
  declare are kept by local name, so `additionalProperties: false` reports them.
- A body that isn't well-formed is `malformed-xml` (`request.body.malformed`, answered with 400;
  `response.body.malformed`), with its line and column. Document type declarations are refused,
  so external entities and entity expansion are errors.

A SOAP service described in WSDL 1.1 is served after
[`apigenie convert --to openapi31`](/docs/reference/cli/convert#wsdl-11-input): the converted
document describes the SOAP envelopes with these `xml` objects.

## Contract testing (validating proxy)

With `--proxy-to`, an OpenAPI document is not mocked: every request goes to the real server, and
the mock checks the request and the server's response against the document. (A GraphQL schema
and an AsyncAPI document's WebSocket, MQTT and HTTP channels work the same way, with checks of
their own: see [GraphQL contract testing](#contract-testing-of-a-graphql-api), [WebSocket contract
testing](#contract-testing-of-a-websocket-api), [MQTT contract
testing](#contract-testing-of-mqtt-channels) and [HTTP channels](#contract-testing-of-http-channels).) Point your tests,
your frontend or a load test at the mock instead of the server, and the traffic becomes a contract
test. To send the requests yourself, from `.http` files or generated from the document, with the
same checks and reports, use [apigenie test](/docs/reference/cli/test).

```sh
apigenie-server openapi.yaml --proxy-to http://localhost:8080 --report contract.json --fail-on errors
```

The upstream gets the request as it came, without the mock's `__` query parameters and
`X-Apigenie-Mock-*` headers, with `Via`, `X-Forwarded-For`, `X-Forwarded-Host` and
`X-Forwarded-Proto`, and `Accept-Encoding: identity` so that bodies can be checked. A path in
the URL (`http://localhost:8080/api`) is put before the request's path. The admin API, CORS
preflights and answers of scripts' `onRequest` stay local; the global network simulation, a
request's `Prefer: delay=` or `drop=`, and scripts' `onResponse` apply to the upstream's responses.
Scenario rules and examples don't: nothing is mocked, unless the server fails in fallback mode
(below).

**What is checked.** Each problem is a violation with a check, a severity and a location:

| Check | Severity | When |
| --- | --- | --- |
| `request.path-or-method.unknown` | warn | The document has no operation for the method and path. |
| `request.operation.deprecated`, `request.parameter.deprecated` | info | A deprecated operation or parameter is used (the response gets `Deprecation: true`). |
| `request.authorization.missing`, `request.authorization.rejected` | error | No credentials for any security requirement; credentials other than the configured ones (`security.mode: enforce`). |
| `request.authorization.oauth2-scheme`, `request.authorization.oauth2-scope` | warn | `apigenie test` only: a request with an OAuth 2 token for an operation that has no OAuth 2 or OpenID Connect scheme; a token requested with scopes the operation doesn't declare, or without those it requires. |
| `request.path.incompatible`, `request.query.incompatible`, `request.header.incompatible`, `request.cookie.incompatible` | error | A parameter is missing or doesn't match its schema (every style). |
| `request.query.unknown` | warn | An undeclared query parameter (`validation.unknown_query_parameters: error`). |
| `request.content-type.incompatible`, `request.body.malformed`, `request.body.incompatible` | error | The body's media type isn't declared, the body can't be parsed (JSON, XML, multipart), or doesn't match its schema (for XML, see [XML bodies](#xml-bodies)). |
| `response.status.unknown` | error for 2xx, else warn | No response covers the status (exact code, then `2XX`, then `default`). |
| `response.status.default` | info | Only `default` covers the status. |
| `response.status.server-error` | info | The server answered with 5xx. |
| `response.content-type.incompatible` | error | The response's media type isn't one of the declared ones. |
| `response.body.incompatible`, `response.body.malformed` | error | The body is missing, doesn't match its schema (`writeOnly` properties aren't required), or isn't JSON or XML where the media type says so. |
| `response.body.unexpected` | warn | A body where the response declares no content. |
| `response.body.unvalidatable` | info | A body that can't be checked: binary, compressed. |
| `response.header.incompatible` | error | A declared header is missing when required, or doesn't match its schema. |
| `response.header.undefined` | off | A header the response doesn't declare (common ones such as `Date` or `ETag` aside). |

`contract.severity` in the configuration changes a check's severity (`error`, `warn`, `info`) or
turns it off (`ignore`).

**Report or enforce.** By default a proxy reports only (`validation.request` and
`validation.response` are `log` unless the configuration says otherwise), since test suites send
invalid requests on purpose to see the server refuse them. Every response then carries the
violations of its exchange:

```http
X-Apigenie-Mock-Source: proxy
X-Apigenie-Mock-Operation: getPet
X-Apigenie-Mock-Violations: 1
X-Apigenie-Mock-Violations-Detail: [{"location":["response","body","/id"],"check":"response.body.incompatible","code":"type","severity":"error","message":"expected type integer, found string"}]
```

The detail is cut at 8 KB, with a last item counting what was left out. The log has the same
violations, `upstreamStatus` for each request, and `fallback: true` for the mock's own answers in
fallback mode.

`--strict` (or `enforce` in the configuration) turns errors into answers. A request with an error
doesn't reach the server: it gets 401 or 403 (security), 415 (media type), 400 (malformed body) or
422. A response with an error is replaced by a 502:

```json
{
  "type": "https://apigenie.pl/mock/errors#invalid-upstream-response",
  "title": "The upstream response does not match the specification",
  "status": 502,
  "detail": "response.body/id: expected type integer, found string",
  "validation": [{"location": "response.body/id", "code": "type", "message": "expected type integer, found string",
                  "severity": "error", "check": "response.body.incompatible", "path": ["response", "body", "/id"]}],
  "upstream": {"status": 200, "contentType": "application/json"}
}
```

Warnings and notes never block anything. A server that can't be reached is answered with 502
(`upstream-unreachable`, `upstream-bad-response`, `upstream-response-too-large`), one that doesn't
answer in time with 504 (`upstream-timeout`).

**Fallback mode.** With `--proxy-mode fallback` (or `proxy.mode: fallback`), every request still
goes to the server first, but the mock answers itself, as it would without `--proxy-to`
(examples, generated data, scenario rules, `Prefer`), when the server can't be reached, doesn't
answer in time, or answers 404 or 501 for an operation of the document. A frontend can then work
against a server that is down or only partly implemented. The answer says so:

```http
X-Apigenie-Mock-Source: generated
X-Apigenie-Mock-Upstream-Status: 404
```

The log record has `fallback: true` and `upstreamStatus` (absent when the server didn't answer).
The server's 404 or 501 isn't checked against the document, so it isn't a `response.status.unknown`
violation. Requests for no operation of the document are not the mock's to answer: the server's
404 reaches the client. An answer that isn't HTTP or is too large is still a 502: the server did
answer.

**Record and replay.** `--record-dir DIR` writes each exchange the server answered as a stub
file, `getPet-1.yaml` (`GET_health-1.yaml` for a route of no operation; numbers already taken
are skipped), and `--stubs DIR` makes a mock without `--proxy-to` answer with them:

```sh
apigenie-server openapi.yaml --proxy-to http://localhost:8080 --record-dir recorded
apigenie-server openapi.yaml --stubs recorded
```

```yaml
stubs:
  - name: getPet-1
    operationId: getPet
    request:
      method: GET
      path: /pets/1                # as the client sent it, without the proxy's prefix
      query: full=true             # without the mock's __ parameters
      headers: {Accept: application/json, Authorization: "[redacted]"}
    # match: {headers: [X-Tenant]} # these request headers must be equal too
    response:
      status: 200
      headers: {Content-Type: application/json, ETag: '"v1"'}
      body: {id: 1, name: Rex}     # JSON; text: for other text, base64: for bytes
```

A request matches a stub when its method, path, query (in any order) and body (JSON compared as
JSON) are the same; headers count only when `match.headers` names them. The first stub that
matches answers, files in name order; its source is `stub:getPet-1`. Requests still go through
security and request validation; the answer isn't checked again. Answers the mock gave or
replaced (fallback, `--strict`) aren't recorded. The values of `Authorization`, `Cookie`,
`Set-Cookie`, `Proxy-Authorization` and `X-Api-Key` are written as `[redacted]`
(`proxy.redact` sets the list), and what the connection or the proxy adds (`Host`,
`User-Agent`, `Via`, `X-Forwarded-*`, `Date`, ...) is left out. A recorded header that matters
for matching has to be restored by hand.

**HAR.** `--record-har session.har` writes every exchange through the proxy as an HTTP Archive
(HAR 1.2) when the server stops: the server's answers and the mock's own (fallback answers,
rejected requests, replaced responses, the problems of a failed server). Each entry's
`comment` is `proxy` or `mock`, and `_apigenie` has the source, the operation, the server's
status, `fallback` and the violations. Headers of `proxy.redact` are written as `[redacted]`;
binary bodies are in Base64, and bodies above 1 MiB are left out (their sizes stay). Browsers'
developer tools, Postman and HAR viewers read it.

**Coverage.** The mock counts what the traffic exercised: each operation, its request body (seen
with a 2xx or 3xx answer) and each declared response (`200`, `4XX`, `default`). An operation is
`covered` when all of these were seen, `partial`, or `not-tested`; requests for no operation are
listed as undocumented. Mocked traffic is counted too, without `--proxy-to` or in fallback mode;
the report tells the two apart: `summary.sources` and each operation's `sources` count the answers
of the server (`proxy`) and of the mock (`mock`, also the problems of a failed server and rejected
requests), `mockedResponses` the statuses the mock answered with, and, when the mock answered
any, the summary printed at the end has a `Sources:` line.

**Reports.** `GET /__apigenie/contract` returns the report at any time, and `DELETE` starts it
over (a reload does too). When the server stops (Ctrl+C or `POST /__apigenie/shutdown`) it prints
a summary, writes `--report` (JSON) and `--report-junit` (one test case per operation, skipped
when not tested, failed with error violations; one per undocumented route; one for the coverage
threshold), and exits with 5 when `--fail-on` or `--min-coverage` isn't met. In CI:

```sh
apigenie-server openapi.yaml --proxy-to http://localhost:8080 --port 4010 \
  --report contract.json --report-junit contract.xml --fail-on errors --min-coverage 80 &
API_URL=http://localhost:4010 npm test
curl -X POST http://localhost:4010/__apigenie/shutdown
wait $!   # 5 when the contract failed
```

The JSON report has `summary` (requests, their sources, violations by severity, coverage of
branches and operations, the gate with its reasons), `operations` (the JSON `pointer` of the
operation in the document, e.g. `/paths/~1pets/get`, requests, responses seen and missing,
undeclared statuses, sources and the mock's statuses, violations by check), `undocumented`
and `violations` (each distinct one with its count and the first time it was seen). Its
`formatVersion` is the version of the format, which changes only when a member is removed or
changes meaning (see [Compatibility](/docs/reference/cli/compatibility)); `version`, its
former name, is deprecated and will be removed in the next release. The JSON Schema of the
members the OpenAPI, AsyncAPI, GraphQL and gRPC reports share is
[apigenie-contract-report.json](https://apigenie.pl/schemas/apigenie-contract-report.json).

## Network simulation

Latency (fixed, uniform, normal or log-normal), jitter, dropped connections (reset, closed without
an answer, never answered, or truncated bodies) and bandwidth limits apply to every response, an
operation's, a scenario's or one request's.

## Scenarios and extensions

A scenario is a named set of rules switched on and off together. A rule matches requests (by
`operationId`, `method`, `path`, `tag`, headers or query) and sets the status, an example, a
literal body, headers, the network simulation, or a probability. Scenarios come from the
configuration, from `x-apigenie-mock` in the document, or from the admin API.

```yaml
paths:
  /orders:
    get:
      x-apigenie-mock:
        delay: 200
        headers: {X-Region: eu}
        scenarios:
          empty: {body: []}
          broken: {status: 500, drop: {probability: 0.5, mode: reset}}
```

A schema can name the [faker](/docs/reference/cli/faker) provider that generates its values
with `x-apigenie-mock-faker` or `x-faker`: a name (`person.firstName`, `internet.email`,
`location.city`...), `{"number.int": [1, 10]}` with arguments by position, `{"string.alpha":
{length: 6}}` with arguments by name, or `{provider: number.int, args: [1, 10], options: {...}}`.
Arguments are numbers, strings and booleans. Names of older faker.js versions (`name.firstName`,
`address.city`, `datatype.uuid`) work too. An unknown name, or arguments the provider doesn't take,
are answered with 500 and faker's message. Records (`person.profile`, `location.address`,
`finance.card`...) are objects, or their JSON text for a string schema.

```yaml
properties:
  owner: {type: string, x-faker: person.fullName}
  age: {type: integer, x-faker: {number.int: [18, 90]}}
  code: {type: string, x-faker: {string.alpha: {length: 6, casing: upper}}}
  customer: {type: object, x-faker: {person.profile: {country: PL}}}
```

With `generation.smart_names`, plain string properties called like `email`, `first_name`,
`city`, `zip_code` or `phone` get such values without an extension. `generation.null_probability`
sets how often a schema with OAS 3.0 `nullable: true` is answered with `null`.

`x-apigenie-mock` on an operation (or its path item) accepts `status`, `example`, `dynamic`,
`body`, `body_file`, `headers`, `latency`, `jitter`, `drop`, `bandwidth`, `delay`, `probability`,
`validate_request`, `scenarios` and `resource` (see [CRUD resources](#crud-resources)); on a response, `delay`, `latency`, `jitter` and `headers`.

## AsyncAPI

For an AsyncAPI 3.x document the mock plays the application the document describes. Servers
with the `ws`, `http` and `mqtt` protocols are served (and `wss`, `https` and `mqtts` over TLS
with `--tls-cert`, cleartext without). Kafka and AMQP channels aren't served, but are read from a
real cluster or broker with `--proxy-to kafka://` ([below](#contract-testing-of-kafka-channels))
and `--proxy-to amqp://` ([below](#contract-testing-of-amqp-channels)); the others are listed as not
served.

- **WebSocket.** A client connects to a channel's address under its server's `pathname`
  (`ws://127.0.0.1:4010/v1/rooms/42` for `rooms/{roomId}`). It gets the messages of the
  channel's `send` operations, one after another, every second by default: the message's
  examples in turn, or values generated from its payload schema (always with `--dynamic`). What
  it sends is checked against the `receive` operations: a match is answered with the operation's
  `reply`, anything else with a problem details message.
- **HTTP.** A `receive` operation takes messages with `POST` (or its HTTP binding's method) and
  answers 202, or 200 with its reply; an invalid message gets 422. A `send` operation answers
  `GET` with a message.
- **MQTT.** A broker on its own port takes subscriptions to the channels' topics (wildcards
  included: a parameter of an address takes its default, first enum value or example) and
  publishes the `send` operations' messages with their MQTT binding's QoS. Messages clients
  publish go to the other subscribers, as with any broker, once they match a `receive`
  operation; its reply is published to the reply channel's topic. MQTT 3.1.1 and 5 clients are
  served; MQTT 5 subscribers also get the MQTT 5 properties of a message: its headers as user
  properties (the first example's, else generated from the header schemas), and the payload format
  indicator, content type, correlation data and response topic of its MQTT binding (generated
  from their schemas when they are schemas) and the operation binding's message expiry. An MQTT 5
  client's properties and user properties are checked as the
  [MQTT proxy](#contract-testing-of-mqtt-channels) checks them; its request is answered on the
  topic of its reply address user property, or on its Response Topic when the reply channel has
  `address: null`, with its correlation id and Correlation Data. Retained messages expire with
  their Message Expiry Interval.

`x-apigenie-mock` on a `send` operation sets `interval` (milliseconds), `count` (messages per
client, 0 for no limit), `jitter`, `messages` (the names to send) and `publish: false`.

```yaml
operations:
  sendNotices:
    action: send
    channel: {$ref: '#/channels/room'}
    x-apigenie-mock: {interval: 500, count: 10}
```

The log has `ws-open`, `ws-in`, `ws-out` and `ws-close` events for WebSocket clients and
`mqtt-subscribe`, `mqtt-in`, `mqtt-out` and `mqtt-unsubscribe` ones for MQTT, with the channel's
address, the operation and the message. When the server stops, WebSocket clients get the close
code 1001 and MQTT 5 clients a DISCONNECT; after a reload they reconnect to the new version.

### Contract testing of a WebSocket API

With `--proxy-to ws://host:port` an AsyncAPI document's WebSocket channels aren't mocked: each
client of a channel gets a connection to the real server, and every message of either side is
checked against the document. Point a frontend or a test suite at the mock instead of the server.

```sh
apigenie-server chat.yaml --proxy-to ws://localhost:9000 --report contract.json --fail-on errors
```

The mock connects to the server first, with the client's path under the URL's prefix, its query,
`Origin`, cookies, `Authorization` and `Sec-WebSocket-Protocol`, plus `Via` and `X-Forwarded-*`;
the client gets the subprotocol the server chose. A server that can't be reached answers the
client's handshake with 502 (504 when it doesn't answer in time), and a server that refuses it
(401, 404, ...) has its answer passed on. What clients send is checked against the channel's
`receive` operations, what the server sends against its `send` operations. A client's message of
an operation with a `reply` on the same channel waits for its reply: the first message the server
sends that is one of the reply's messages is checked as that reply (logged as `name (reply)` with
the request's operation), not against the `send` operations. Messages are relayed
whole (fragments are put together first); pings are answered on each side; a close frame goes on
with its code. No WebSocket extension is negotiated (`permessage-deflate` is left out of the
handshake). HTTP channels go to the same server (see [below](#contract-testing-of-http-channels));
the MQTT channels to the broker of a second `--proxy-to mqtt://...`, or they keep being mocked.

| Check | Severity | When |
| --- | --- | --- |
| `message.incompatible` | error | The message matches none of the messages of the channel's operations in its direction. |
| `message.malformed` | error | The message isn't JSON where JSON is expected. |
| `message.frame.unexpected` | warn | A binary frame for a text or JSON message, or a text frame for a binary one (`image/png`, `application/octet-stream`, Protobuf, Avro, ...). |
| `message.qos.unexpected` | warn | MQTT: a client published at a QoS other than the one of the operation's MQTT binding. |
| `channel.parameter.invalid` | error | A parameter of the channel's address (a path segment, a topic level) has a value outside the parameter's `enum`. |
| `channel.receive.undeclared` | warn | A client sent a message on a channel the application has no `receive` operation for. |
| `channel.send.undeclared` | warn | The server sent a message on a channel it has no `send` operation for. |
| `message.reply.uncorrelated` | error | The reply's `correlationId` (in the payload) isn't the request's, or is missing. |
| `operation.query.invalid` | error | HTTP channels: the request's query doesn't match the `query` of the operation's HTTP binding. |
| `message.headers.invalid` | error | HTTP channels: the request's or the reply's header fields don't match the message's `headers` or its HTTP binding's `headers`. |

`validation.request` sets what happens to the clients' messages and `validation.response` to the
server's: `log` by default for a proxy (reported, and forwarded), `enforce` with `--strict`. In
enforce mode a message with an error isn't forwarded, and the client gets a problem message
instead (`asyncapi.validate_incoming` stays the mock's setting):

```json
{"type": "https://apigenie.pl/mock/errors#invalid-upstream-message", "title": "The upstream's message doesn't match the specification",
 "status": 422, "channel": "rooms/1", "direction": "upstream", "validation": [{"location": "message/text", "code": "type",
 "message": "expected type string, found integer", "check": "message.incompatible", "severity": "error"}]}
```

A client's message gets `#invalid-message` with `"direction": "client"`. The log has the
`ws-open`, `ws-in`, `ws-out` and `ws-close` events with the operation, the message's name, the
violations and `source: proxy` (`mock` for what the proxy answered itself). `contract.severity`
changes the checks' severities as for the other proxies.

**Coverage and reports.** Each message of each operation is a branch: it is covered once a valid
one went by. An operation is `covered`, `partial` or `not-tested`; a message that matches none is
counted for the operation it was checked against first. `GET /__apigenie/contract` returns the
report (`"specification": "asyncapi"`, connections, messages in and out, channels, operations,
violations), `--report` and `--report-junit` write it when the server stops (a test case per
operation), and `--fail-on` and `--min-coverage` (the share of messages) make the exit status 5.

### Contract testing of MQTT channels

With `--proxy-to mqtt://host:port` the mock's broker relays to a real broker: each client that
connects to the mock gets a connection of its own to the broker, and every message published
either way is checked against the document.

```sh
apigenie-server devices.yaml --proxy-to mqtt://localhost:1884 --mqtt-port 1883 --report contract.json
apigenie-server events.yaml --proxy-to ws://localhost:9000 --proxy-to mqtt://localhost:1884
```

The upstream connection has the client's MQTT version, client identifier (the one the mock
assigns when the client gives none), clean start, keep-alive, will, user name and password. The
client's CONNECT gets the broker's CONNACK, a refusal included; a broker that can't be reached is
answered with "server unavailable" (return code 3, MQTT 5 0x88). Subscriptions are the broker's:
the SUBACK is its answer, and the broker decides who gets which message. What clients publish is
checked against the topic's channel's `receive` operations, with the QoS of the operation's MQTT
binding when it gives one (`message.qos.unexpected`); what the broker delivers against the `send`
operations. A subscriber also gets other clients' messages, so on a channel without `send`
operations a delivery is checked against the `receive` operations. Topics of no channel are
relayed unchecked (debug level in the log).

In enforce mode a client's message with an error isn't forwarded (MQTT 5 clients get reason code
0x99 in their acknowledgement), and a broker's message with an error isn't delivered. A client
that disconnects is disconnected from the broker; one whose connection is lost has its upstream
connection dropped too, so the broker publishes its will; a client the broker disconnects is
disconnected. A reload keeps the clients connected. The log has `mqtt-connect`,
`mqtt-subscribe`, `mqtt-in`, `mqtt-out`, `mqtt-unsubscribe` and `mqtt-disconnect` events with
`source: proxy` (`mock` for what the proxy decided itself), and coverage and reports work as for
WebSocket.

MQTT 5 properties are relayed both ways: user properties, payload format indicator, content type,
response topic, correlation data and message expiry (not the topic alias and subscription
identifiers, which belong to one connection). An MQTT 5 message's are checked against the MQTT
bindings (0.2.0):

| Binding | Checked (`message.properties.invalid`, `error`) |
| --- | --- |
| message `payloadFormatIndicator` | The Payload Format Indicator is the same (absent counts as 0) |
| message `contentType` | The Content Type is the same, ignoring case |
| message `correlationData` | Correlation Data is present and matches the schema |
| message `responseTopic` | A Response Topic is present, the string given or matching the schema |
| operation `messageExpiryInterval` | A Message Expiry Interval is present and no longer |

Its user properties are its headers, checked against the message's `headers`
(`message.headers.invalid`). MQTT 3.1.1 messages have no properties: nothing is checked for them.
The report counts the messages whose bindings state properties, checked or not:
`summary.properties: {"checked": 3, "unchecked": 1}`, `properties` of each operation, and a
summary line `MQTT 5 properties: checked on 3 messages, not on 1 (MQTT 3.1.1)`. A client's QoS 1
and 2 messages are acknowledged by the mock before the broker
acknowledges them, and the mock delivers at most QoS 1. An `mqtts://` broker (port 8883 by default) is reached over TLS
and verified as `https://` upstreams are (see [TLS](#tls)).

### Contract testing of Kafka channels

There is no Kafka broker in the mock. With `--proxy-to kafka://host:port` (port 9092 by
default; `kafkas://`, port 9093, over TLS) the server is a client of the real cluster, a validating
tap: it reads the topics of the document's Kafka channels and checks every record on them,
whoever produced it.

```sh
apigenie-server shop.yaml --proxy-to kafka://localhost:9092 --report contract.json
apigenie-server events.yaml --proxy-to mqtt://localhost:1883 --proxy-to kafka://localhost:9092
```

A channel with a `kafka` or `kafka-secure` server is a Kafka channel; its topic is the channel's
Kafka binding `topic`, else its address. Parameters of the address match the topic's segments, so
`stock.{region}` reads `stock.eu` and `stock.us`; the cluster's topics are looked at again every
10 seconds for new ones. The tap reads every partition from the offset it had when the server
started, outside any consumer group, so it never takes partitions from the application's consumers
and commits no offsets. A topic the cluster doesn't have yet is reported once and read once it
appears; a lost connection is reported and tried again.

A record doesn't say who produced it, so the channel's operations tell. On a channel with only
`send` operations it is the application's (a `kafka-out` event, checked against them); with only
`receive` operations it is a client's (`kafka-in`); with both it is checked as the application's,
and as a client's when that doesn't match. What is checked:

| What | Checked against | Check |
| --- | --- | --- |
| The value | The operations' messages | `message.incompatible`, `message.malformed` |
| The headers | The message's `headers` (values read as the type the schema says) | `message.headers.invalid` |
| The key | The message's Kafka binding `key` (an error too when the record has none) | `message.key.invalid` (`error`) |
| The partition | The channel's Kafka binding `partitions` | `message.partition.invalid` (`warn`) |
| The topic | The address's parameters | `channel.parameter.invalid` |

Records are on the cluster before the tap sees them, so enforce mode reports them like log mode.
Kafka operations count in the coverage, `GET /__apigenie/contract`, `--report` and
`--report-junit` like any other, and the report's `upstream` lists the `kafka://` URL. The log has
`kafka-in` and `kafka-out` events with the topic as `channel`, the record's partition and offset
in the message, and its key (as `kafka-key`) and headers as the event's headers; what the tap
found or failed at are `server` events with `source: kafka`.

The SASL PLAIN user and password come from `APIGENIE_KAFKA_USERNAME` and
`APIGENIE_KAFKA_PASSWORD` (`apigenie test` reads them too), never from the URL. `kafkas://` brokers
are verified as `https://` upstreams are (see [TLS](#tls)). The cluster needs Kafka 2.3 or later.
Compressed record batches aren't read: they are reported (`3 record(s) at offset 40 compressed
with zstd`) and skipped. A `kafka://` upstream goes with one document, not with
[several services](#several-services). See ADR-170 for what the client does and doesn't do.

### Contract testing of AMQP channels

There is no AMQP broker in the mock either. With `--proxy-to amqp://host:port/vhost` (port 5672 by
default; `amqps://`, port 5671, over TLS; the path is the virtual host, `/%2Fcarpet-express` for
`/carpet-express`, none for `/`) the server is an AMQP 0-9-1 client of the real broker, a
validating tap.

```sh
APIGENIE_AMQP_USERNAME=conformance APIGENIE_AMQP_PASSWORD=conformance \
  apigenie-server customs.yaml --proxy-to amqp://localhost:5672/%2Fcarpet-express --report contract.json
```

A channel with an `amqp` or `amqps` server is an AMQP channel. Its AMQP channel binding says what it
is:

- **`is: routingKey`** (the default): the address is a routing key of `exchange.name` (of
  `exchange.type`, topic by default). The tap binds a queue of its own (exclusive, deleted when it
  disconnects) to the exchange: for a topic exchange with the address's parameters as `*` words
  (`customs.{country}.filing` is `customs.*.filing`), for a fanout or headers exchange with any key.
  Direct exchanges bind the address itself, so their addresses can't have parameters. It reads that
  queue, so it gets a copy of every message the exchange routes there, and the application's
  queues are left alone. An exchange the broker doesn't have is reported, and looked for again every
  10 seconds.
- **`is: queue`**, and routing keys of the default exchange: a queue the application consumes.
  Reading it would take the application's messages, so the tap doesn't, and says so when it starts.
  What `apigenie test` and the admin API send there is checked.

Messages are checked as the [Kafka tap](#contract-testing-of-kafka-channels) checks records, in the
same direction: their body and headers against the operations' messages, and their properties
against the AMQP bindings (0.3.0) (`message.properties.invalid`, `error`):

| Binding | Checked |
| --- | --- |
| operation `deliveryMode`, `priority`, `userId`, `replyTo` | The property is present and the same |
| operation `expiration` | An expiration is present and no longer |
| operation `timestamp: true` | A timestamp is present |
| message `contentEncoding` | The content encoding is the same |
| message `messageType` | The `type` property is the same |

The log has `amqp-in` and `amqp-out` events with the routing key as `channel` and the exchange in the
message; coverage and reports work as for Kafka, and enforce mode reports too. The PLAIN user and
password come from `APIGENIE_AMQP_USERNAME` and `APIGENIE_AMQP_PASSWORD` (`guest` without them),
never from the URL. An `amqp://` upstream goes with one document. See ADR-171.

### Contract testing of HTTP channels

With `--proxy-to http://host:port` (or the `ws://` server of the WebSocket channels) an AsyncAPI
document's HTTP channels go to that server through the same transport as an OpenAPI document's
proxy (`Via`, `X-Forwarded-*`, 502 and 504 problems), checked against the document instead of
an OpenAPI one:

- A `receive` operation takes requests with its HTTP binding's method (POST by default). The
  request's body is checked against the operation's messages, its query against the binding's
  `query` schema (`operation.query.invalid`) and its header fields against the message's
  `headers` and HTTP binding `headers`, its traits' included (`message.headers.invalid`). A 2XX
  response's body and header fields are checked against its `reply`.
- A `send` operation is asked with GET, as the mock serves it; a 2XX response with a body is
  checked against its messages (not its header fields: they are the webhook's).
- Other paths get 404 and other methods 405, without the server.

Query parameters and header fields are read as the type their schema says (`integer`, `number`,
`boolean`, an `array` from repeated parameters); header fields the schemas don't name are left
out. `apigenie test` checks the requests of HTTP channels with the same code and the same checks
(see [Requests of HTTP channels](/docs/reference/cli/test#requests-of-http-channels)).

In enforce mode an invalid request gets 422 (`#invalid-message`) without the server, and an
invalid response is replaced by 502 (`#invalid-upstream-message`). Responses carry
`X-Apigenie-Mock-Source`, `X-Apigenie-Mock-Operation` and `X-Apigenie-Mock-Violations`, and each
exchange counts as a message of its operation in the coverage. Responses other than 2XX and the
reply's `statusCode` binding aren't checked; fallback mode, `--record-dir` and `--record-har` are
for OpenAPI documents.

## Protobuf / gRPC

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](#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](#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.

## GraphQL

A GraphQL schema is served as a GraphQL endpoint, `/graphql` under `--base-path`
(`graphql.path` changes it). Every query, mutation and subscription the schema allows is
answered, with no resolvers to write.

- **GraphQL over HTTP.** `POST` with `application/json` (`{query, variables, operationName,
  extensions}`) or `application/graphql`, `GET` with the same as query parameters (queries only:
  a mutation over `GET` gets 405), and batches (a JSON array of requests, `graphql.max_batch`).
  Clients that accept `application/graphql-response+json` get 400 for requests that fail before
  execution; `application/json` clients get 200 for every well-formed request, as the
  specification asks.
- **Validation.** Requests are checked against the schema with the rules of graphql-js and its
  messages (unknown fields and arguments, wrong values and variable types, fragments that can't
  apply, fields that can't be merged...). Errors have `locations` and `extensions.code`
  (`GRAPHQL_PARSE_FAILED`, `GRAPHQL_VALIDATION_FAILED`, `BAD_USER_INPUT`), and the log shows each
  as a violation.
- **Execution.** Fragments, inline fragments, aliases, variables with defaults, `@skip`,
  `@include` and `__typename` behave as in a real server. Interfaces and unions return one of
  their object types. A field that fails is null and its error has the field's `path`; a
  non-null field's null goes up to the nearest nullable one.
- **Introspection.** `__schema` and `__type` are complete, so GraphiQL, Apollo Sandbox and
  code generators work against the mock (`graphql.introspection: false` turns it off). The SDL is
  at `/graphql/schema.graphql` and `GET /__apigenie/spec`.
- **GraphiQL.** A browser opening the endpoint gets GraphiQL, loaded from esm.sh
  (`graphql.playground: false` turns it off).

### Generated data

Values follow the field's type: enum values, list lengths between `graphql.lists.min` and
`max` (or a `first`, `last`, `limit`, `count`... argument, the field's own or its
connection's), nulls with `graphql.null_probability`, and realistic values for fields named like
`email`, `city`, `phone`, `url` or `firstName`, for `name`, `title` and `genre` by the type
they belong to (`Author.name` is a person's, `Book.title` a book's), and for custom scalars by
their name (`DateTime`, `Date`, `URL`, `UUID`, `JSON`, `Long`, `Decimal`, `EmailAddress`...).
`--seed` repeats a run.

The schema can say more, with graphql-faker's directives or the mock's own. They need no
definitions in the schema and don't show in introspection:

```graphql
type User {
  name: String @fake(type: fullName)
  email: String @fake(type: email, options: {emailProvider: "example.com"})
  joined: String @fake(type: pastDate, options: {dateFormat: "YYYY-MM-DD"})
  role: Role @examples(values: ["ADMIN", "EDITOR"])
  tags: [String!] @listLength(min: 1, max: 3)
  score: Int @mock(min: 0, max: 100)
  code: String @mock(faker: "string.numeric", args: [6])
  nickname: String @mock(nullProbability: 0.5)
}
scalar Money @mock(min: 1, max: 500)
```

- `@fake(type:, options:)` takes the 64 types of graphql-faker and the options most used
  (`minMoney`, `maxMoney`, `decimalPlaces`, `minNumber`, `maxNumber`, `precisionNumber`,
  `dateFormat`, `dateFrom`, `dateTo`, `loremSize`, `imageSize`, `imageKeywords`,
  `passwordLength`, `emailProvider`, `useFullAddress`). Data is English whatever the `locale`.
- `@examples(values:)` picks one of the values; `@listLength(min:, max:)` sizes a list.
- `@mock` takes `faker` (any faker provider, as `x-faker`) with `args` (a list by position or an
  object by name), `value` or `values`,
  `min` and `max` for numbers, `length` or `minLength` and `maxLength` for lists, and
  `nullProbability`. On a root field, `delay` (milliseconds) delays the response and
  `errorRate` with `error` makes it fail; on a subscription field, `interval` and `count` pace its
  events.

`graphql.fields` does the same from the configuration, for a field (`Type.field`) or a scalar,
over the schema's directives. A schema that defines a directive of the same name keeps it: the
mock leaves it alone.

### The store

Objects whose type has an `id` field are entities: once made, their fields keep their values
for the next requests, so `{ user(id: "7") { name } }` answers the same name every time. The
usual root fields work on them, by their names and types:

| Field | What it does |
| --- | --- |
| `user(id: ID!): User`, `node(id: ID!): Node` | The entity with that id; a new one when there is none (`graphql.store.unknown_ids: null` answers null). |
| `users: [User!]!` | The users, made up to the list's length once, then the same. |
| `usersConnection(first:, after:, last:, before:)` | The users as a Relay connection: `edges { cursor node }`, `nodes`, `pageInfo`, `totalCount`. |
| `createUser(input: ...)`, `addUser(...)`, `newUser`, `registerUser`... | A new user with the input's fields; payload types get it in their field and `clientMutationId` back. |
| `updateUser(id:, input:)`, `editUser`, `setUser`... | The user, with the input's fields. |
| `deleteUser(id:)`, `removeUser`... | Removes the user; returns it, `true`, its id or a payload. |

`--dynamic` and `graphql.store.enabled: false` turn the store off; `POST /__apigenie/seed`
empties it, and so does a reload. `graphql.store.max_entities` (10000) bounds it.

### Examples

An example answers a request with data of its own. Operation examples match an operation's
name, its document (layout and comments don't count) and some of its variables; field examples
are the value of a field for some arguments, in any operation:

```yaml
# schema_examples/users.yaml, next to schema.graphql: an example, or a list of them
- name: ada
  operation: GetUser
  variables: {id: "1"}
  response:
    data: {user: {id: "1", name: Ada Lovelace}}
- name: grace            # a field example
  field: Query.user
  arguments: {id: "2"}
  data: {name: Grace Hopper}
```

Example data is cut to the request's selection set, and fields it lacks are generated, so one
example serves every query of its operation. Examples come from `graphql.examples` in the
configuration, from `graphql.examples_files` (files and directories of `.json`, `.yaml` and
`.yml`, each an example or a list of them) and from the directory named after the schema
(`schema.graphql` takes `schema_examples/`). `Prefer: example=ada` picks one by name;
`--dynamic` and `Prefer: dynamic=true` leave them out. An example's `errors` and `extensions`
are added to the response, and `status` sets the HTTP status.

### Scenarios and errors

Rules of scenarios and `operations` match GraphQL operations by the operation's name
(`operationId: GetUser`) or by a root field (`operationId: Query.user`, or `tag: Query.user`).
Besides `status`, `headers`, `body` (the whole response), `example`, `dynamic` and the
simulation, they take `errors`:

```yaml
scenarios:
  users-down:
    rules:
      - match: {operationId: Query.user}
        errors:
          - message: The users service is down.
            path: [user]
            extensions: {code: UNAVAILABLE}
          - Partial results      # no path: added to the response's errors
```

An error with a path fails that field: it is null (or null up to the nearest nullable field)
and the error has its locations. `Prefer: code=`, `example=`, `dynamic=`, `delay=`, `drop=` and
`scenario=` apply as for OpenAPI.

### Subscriptions

WebSocket clients of the endpoint speak `graphql-transport-ws` (the `graphql-ws` library: Apollo
Client, urql, GraphiQL) or the legacy `graphql-ws` of `subscriptions-transport-ws`, by the
subprotocol they offer. A subscription gets an event every `graphql.subscriptions.interval`
(1000 ms), each a fresh execution of its operation, until `count` events (0: until the client
completes it); a field's `@mock(interval:, count:)` wins. Queries and mutations over WebSocket get
one result. A request that accepts `text/event-stream` gets the same as server-sent events
(`next` events, then `complete`: graphql-sse's distinct connections mode). The log has `ws-open`,
`ws-in`, `ws-out` and `ws-close` events; the server's stop closes the clients with 1001 and a
reload with 1012.

### Contract testing of a GraphQL API

With `--proxy-to`, the GraphQL endpoint goes to a real GraphQL server, and the mock checks every
request against the schema and every answer against the schema and the request's selection set.
Point your frontend, your tests or a load test at the mock, and the traffic becomes a contract
test of the server:

```sh
apigenie-server schema.graphql --proxy-to http://localhost:4000/graphql --report contract.json --fail-on errors
```

```text
Listening on http://127.0.0.1:4010/graphql (spec: schema.graphql, 12 operations)
Proxy: http://localhost:4000/graphql (requests: log, responses: log, unknown routes: forward, mode: always)
```

The URL's path is the server's endpoint (`HOST:PORT` alone keeps the mock's, `/graphql`). The
upstream gets the request as it came (a batch stays a batch), without the mock's `__` query
parameters and `X-Apigenie-Mock-*` headers, with `Via` and `X-Forwarded-*`, and
`Accept-Encoding: identity`. GraphiQL, `/graphql/schema.graphql`, the admin API and `OPTIONS`
stay local. Requests for other paths are forwarded unchecked (`request.path.unknown`), or answered
404 with `--proxy-unknown reject`. A proxy makes up no subscription events: WebSocket and
server-sent events subscriptions aren't served.

**What is checked.** Requests as the mock checks them (graphql-js's rules and messages), and each
answer as graphql-js would have produced it, field by field:

| Check | Severity | When |
| --- | --- | --- |
| `request.body.malformed` | error | Not a GraphQL request: not JSON, no query, an unsupported content type. |
| `request.query.malformed`, `request.operation.unknown` | error | The query can't be parsed; `operationName` names no operation, or is missing. |
| `request.query.invalid`, `request.variables.incompatible` | error | The operation isn't valid against the schema; a variable doesn't match its type. |
| `request.method.incompatible` | error | A mutation over `GET`. |
| `request.field.deprecated` | info | A deprecated field, argument, input field or enum value is used. |
| `request.path.unknown` | warn | A request for another path than the endpoint (forwarded unchecked). |
| `response.status.incompatible` | error | A 1xx or 3xx status, or a 4xx with `data`: an executed operation that says it failed. |
| `response.status.server-error` | info | The server answered with 5xx. |
| `response.content-type.incompatible` | error | Neither `application/json` nor `application/graphql-response+json`. |
| `response.body.malformed` | error | Not JSON, or a batch not answered with a list of as many results. |
| `response.shape.incompatible` | error | Not a map of `data`, `errors` and `extensions`: other keys, neither `data` nor `errors`, `data: null` without `errors`, empty `errors`. |
| `response.errors.incompatible` | error | An error without a string `message`, with `locations` that aren't `{line, column}`, a `path` of other than names and indexes, `extensions` that aren't an object. |
| `response.errors.unexpected`, `response.errors.path-unknown` | warn | An error's entries outside `extensions`; a `path` that names no field the operation selects. |
| `response.data.incompatible` | error | A selected field (or alias) missing; `null` in a non-null position that no error's `path` explains; a list, an object or a scalar where the type says otherwise; `Int` beyond 32 bits, a non-finite `Float`, a `String`, `Boolean` or `ID` of another kind, an unknown enum value; a `__typename` that isn't a possible type. |
| `response.data.unexpected` | warn | A key the operation didn't select. |
| `response.data.typename-missing` | info | An object of an interface or union came without `__typename`: only the selections outside type conditions are checked. |
| `response.data.unvalidatable` | info | Nesting deeper than 64, or more than 100 violations in one answer. |
| `response.field-error` | warn | The server ran the operation, but fields failed: `errors` next to `data` (`null` or partial), one per error, e.g. `the field book failed: not implemented`. Valid GraphQL, but a contract test shouldn't pass it silently; `--fail-on warnings` (or `contract.severity`) fails on it. |
| `response.request-error` | warn | The server didn't execute the operation (authentication, persisted queries...). |
| `response.schema-drift` | error | The server rejected an operation the schema allows (`GRAPHQL_VALIDATION_FAILED`, "Cannot query field..."): its schema differs. |
| `schema.drift`, `schema.addition` | error, info | The server's schema (introspected) lacks or changed something of the schema; has more. |
| `schema.unchecked` | info | The server's schema can't be introspected. |

Fragments, inline fragments, `@skip` and `@include` (with the request's variables), aliases and
merged fields are followed as the server must; custom scalars take any JSON value.
`contract.severity` changes a check's severity or turns it off, as for OpenAPI.

**The server's schema.** With the first request it forwards, the proxy also sends the server the
usual introspection query (with that request's `Authorization`, `Cookie` and `X-Api-Key`), turns
the answer into a schema and compares it with the served one, as `apigenie diff` does: what the
server lacks or changed incompatibly is `schema.drift` (located by `Type.field`), what it adds
`schema.addition`. The log has a `server` event with the result (`schema of
http://localhost:4000/graphql: 1 breaking difference, 2 additions`), and the report a `schema`
section. A server that isn't up yet is asked again with the next request; `POST
/__apigenie/contract/schema-check` asks now. `proxy.schema_check: false` turns it off.

**Report or enforce.** As for OpenAPI, the proxy reports by default: answers carry
`X-Apigenie-Mock-Violations` and `X-Apigenie-Mock-Violations-Detail` (locations such as
`["response", "body", "/data/book/title"]`, `/2/data/...` for the third result of a batch). With
`--strict`, a request with errors isn't forwarded: it gets the GraphQL errors the mock answers with
(`GRAPHQL_VALIDATION_FAILED`...). An answer with errors is replaced by the `invalid-upstream-response`
502 problem. Warnings and notes never block.

**Fallback mode.** `--proxy-mode fallback` (`proxy.mode`) lets scenario rules, examples and the
scripts' `onGraphQL` answer first, as for a mock; what nothing answers goes to the server. A batch
goes to the server whole as soon as one of its operations does.

**Recording.** `--record-dir DIR` writes the answer to each valid operation as an example file
(`GetBook-1.yaml`: `name`, `operation`, `query`, `variables`, `response`, and `status` when not
200), which the mock serves as it is: point `graphql.examples_files` at the directory, or name it
after the schema (`schema_examples/`). Bodies are recorded as they are.

**Coverage and reports.** The mock counts the schema's fields that requests selected (every field
of its object and interface types, as GraphQL Inspector's coverage does) and its root fields
(`Query.book`): `covered`, `partial` (some fields under it never selected) or `not-tested`. Mocked
traffic counts too. `GET /__apigenie/contract` is the report; when the server stops it prints a
summary, writes `--report` and `--report-junit` (a test case per root field, one for the server's
schema, one for the coverage threshold) and exits with 5 when `--fail-on` or `--min-coverage`
(the share of fields) isn't met. The JSON report has `summary`, `schema`, `rootFields`, `fields`,
`uncoveredFields`, `operations` (by name), `otherRoutes` and `violations`.

## Scripts

A script is an ES module whose default export has any of five hooks:

```js
import { limit } from './settings.mjs';

export default {
  // before the document's operations: a response answers the request
  onRequest(req, ctx) {
    if (req.path === '/pets/13') {
      return ctx.response(404, { title: 'No such pet' });
    }
  },
  // the response about to be sent: change it in place
  onResponse(req, res, ctx) {
    if (req.operationId === 'listPets') {
      res.body = res.body.slice(0, limit);
      res.headers['x-total-count'] = '3';
      res.delay = 250;
    }
  },
  // AsyncAPI messages, in and out: change msg.payload, or return false to drop the message
  onMessage(msg, ctx) {
    if (msg.direction === 'out') msg.payload.sentAt = ctx.now();
  },
  // GraphQL operations (each subscription event too): a result answers, its data completed
  onGraphQL(operation, ctx) {
    if (operation.name === 'GetUser') {
      return { data: { user: { id: operation.variables.id, name: ctx.fake('person.fullName') } } };
    }
  },
  // after the scripts were loaded, at the start and after each reload
  onReload(ctx) {},
};
```

- `req` has `method`, `path`, `query` (an object; repeated parameters are arrays), `headers`
  (lower-case names), `body` (parsed JSON, or the text), `rawBody`, `operationId` and `route`
  (in `onResponse`), `remote` and `connectionId`.
- `res` has `status`, `headers`, `body`, `source`, and `delay` (milliseconds) and `drop` (`reset`,
  `close`, `timeout`, `truncate`) to set. A response returned by `onRequest` has the same fields.
- `msg` has `direction` (`in` or `out`), `channel`, `operation`, `message`, `contentType` and
  `payload`.
- `operation` has `name`, `type` (`query`, `mutation`, `subscription`), `query`, `variables` and
  `rootFields` (`["Query.user"]`). The result's data is cut to the selection set and completed
  with generated values, like an example's.
- `ctx` has `state` (`get`, `set`, `delete` and `keys` of JSON values kept between requests, the
  admin API's `/state`), `db` (collections of documents, see [Document store](#document-store)),
  `log(level, ...values)`, `random()` (seeded by `--seed`), `now()`,
  `response(status, body, headers)`, `fake(name, ...arguments)` (the faker providers of `x-faker`,
  with arguments by position and a last object by name: `fake('string.alpha', {length: 4})`)
  and `scenarios` (`activate`, `deactivate`, `active`).

Scripts run in a sandbox: the language only, modules next to the script, no file system,
network or timers. Each call has `scripting.timeout_ms` (1000) and `scripting.memory_limit_mb`
(64); a hook that throws or runs out of time is logged, and answered with 500 when
`scripting.fail_mode` is `error`. `--watch` reloads changed scripts, and a broken script keeps
the previous version.

## Session recording

`--record DIR` keeps a run whole: every exchange the server answered, of every service and every
protocol, in the order it happened, each marked with the test that sent it, as files a replay
can read and a reviewer can diff:

```sh
apigenie-server openapi.yaml --proxy-to http://localhost:8080 --record sessions/checkout
apigenie-server --config apigenie-server.yaml --record sessions/checkout   # several services
apigenie-server session export sessions/checkout --har checkout.har
```

```text
sessions/checkout/
  session.yaml                     # the session and the exchanges in order
  pets/0001-getPet.yaml            # an HTTP exchange of the service pets (a stub)
  pets/0002-createPet.yaml
  events/0003-ws-in-sendMessage.yaml   # a WebSocket message from a client
  events/0004-ws-out-sendMessage.yaml  # a message to it
  chat/0005-Chat.Talk.yaml         # a gRPC call with its messages (a stub)
```

A server of one document writes the files in `DIR` itself (`--stubs DIR` and the GraphQL
examples skip `session.yaml`). `session.yaml` names the session (the
directory's name unless `record.name` or `/record/start` says), the server, when it started and
stopped, the services (name, document, `specification`, the SHA-256 of the document's bytes,
mount) and the exchanges:

```yaml
name: checkout
server: apigenie-server 1.0.0
started: 2026-10-13T10:00:00.000Z
stopped: 2026-10-13T10:00:42.512Z
services:
  - {name: pets, spec: pets.yaml, specification: openapi, sha256: cfada1c0..., mount: /pets}
  - {name: events, spec: events.yaml, specification: asyncapi, sha256: 9b2f..., mount: /events}
exchanges:
  - {sequence: 1, id: 2, service: pets, test: checkout-happy, kind: http, time: 2026-10-13T10:00:01.555Z,
     durationMs: 3, source: proxy, summary: GET /pets/1 200, file: pets/0001-getPet.yaml}
  - {sequence: 3, id: 7, service: events, test: checkout-happy, kind: ws, time: ..., source: mock,
     summary: ws-in rooms/7 notice, file: events/0003-ws-in-sendMessage.yaml}
```

**The exchange files.** Each has a `session` note (the index's line) next to the protocol's own
form, so what already reads that form reads the file:

- `http`: `{session, stubs: [stub]}`, the stub form of [`--record-dir`](#contract-testing-validating-proxy),
  so `--stubs sessions/checkout/pets` makes a mock answer with the service's exchanges as before
  (the first matching stub answers, whatever the test). OpenAPI exchanges, AsyncAPI HTTP channels
  and GraphQL requests that aren't one operation answered with JSON (batches, errors, GraphiQL).
  The answer is kept as the client got it, less the CORS headers the server adds to every
  answer. The request's `X-Apigenie-Test` header is left out.
- `graphql`: one operation and its answer, in the [example form](#examples) (`name`, `operation`,
  `query`, `variables`, `response`, `status`) the mock's examples load; the note's `path` is the
  endpoint.
- `grpc`: `{session, stubs: [stub]}`, the stub form of a `.proto` file's
  [`--record-dir`](#proxy-and-record), with the whole streams (at most 100 messages per side);
  the note's `order` (`[in, out, in, out]`) says which side each message came from, `code` the
  status. The `x-apigenie-test` metadata is left out.
- `ws` and `mqtt`: one file per message of either side:

  ```yaml
  session: {sequence: 3, id: 7, service: events, test: checkout-happy, kind: ws, time: ..., source: mock}
  message:
    direction: in                  # from the client; out: to it
    channel: rooms/7               # the channel's address, and the route it matched
    route: rooms/{roomId}
    operation: sendMessage
    name: notice
    contentType: application/json
    connection: 4
    json: {text: hi}               # text: for other text, base64: for bytes
  ```

**Tests.** An exchange's `test` is the `X-Apigenie-Test` header of the request or of the
WebSocket handshake, or the `x-apigenie-test` metadata of a gRPC call; MQTT messages have none
of their own. Without one, it is the current **mark**: `POST /__apigenie/record/mark {"test": "checkout-happy"}`
names the test of the exchanges to come (`{}` clears it), for clients that can't send the
header. A Playwright suite sends the header with `page.setExtraHTTPHeaders` and marks the test in
`beforeEach`.

**Order.** `sequence` numbers the exchanges in the order they completed, which is the log's
order; a request still in flight when a short one is answered comes after it. Each exchange
keeps its `time` and `durationMs` (a gRPC call its `started`), so a tool can sort by arrival.
What isn't an exchange isn't recorded: requests the client abandoned or the simulation dropped,
CORS preflights, the admin API's own traffic, the dashboard.

**Redaction.** The values of `Authorization`, `Cookie`, `Set-Cookie`, `Proxy-Authorization` and
`X-Api-Key` are written as `[redacted]`, as in `--record-dir`. `--record-redact` (or
`record.redact`) adds header and metadata names, and JSON pointers, which start with `/`, to
values of JSON bodies, message payloads, gRPC messages and GraphQL variables and answers:
`--record-redact X-Session --record-redact /owner/password`. A pointer is matched exactly
(`/items/0/key` is the first item's key). A redacted request body no longer matches on replay.

**Start and stop.** `--record` records from the start and writes `session.yaml` when the server
stops; the startup line says `Recording the session to DIR` (`record` in JSON). The admin API
records a part of a run: `POST /__apigenie/record/start {"dir": "sessions/run-7", "name": "run 7"}`
(`dir` defaults to `record.dir`; 409 while a session is on, 400 for a directory that isn't
empty), `POST /__apigenie/record/stop` (the session's `dir`, `name`, `exchanges`, `file`; 409
without one), `GET /__apigenie/record` (`recording`, and the same while one is on, `test` for the
mark). The [dashboard](#dashboard)'s Traffic screen has Record session and Save session. A
session has at most 10 000 exchanges: past that it counts on but writes no more files, with one
warning in the log and `truncated: true` in the index. When the server dies before writing
`session.yaml`, the exchange files tell: `session export` rebuilds the index from their notes.

**Export.** `apigenie-server session export DIR --har FILE` writes the HTTP exchanges, GraphQL
operations (as `POST` requests to the endpoint) and gRPC calls (as `POST` requests with
`application/grpc+json` bodies, the messages as JSON arrays, and a `grpc-status` header) as a HAR
1.2 file, in the session's order, each entry with `_apigenie.service`, `test`, `sequence` and
`kind`; WebSocket and MQTT messages have no form in HAR and are left out (the command says how
many). URLs keep the services' mount prefixes.

The design is ADR-168.

## Session replay

`--replay DIR` serves the services of a [recorded session](#session-recording) the way they
answered when it was recorded, without the real servers: each request gets the next unused
recorded exchange of its test that matches it, so a test suite replays the same run every time,
and fails when the application asks for something the recording doesn't have.

```sh
apigenie-server openapi.yaml --record sessions/checkout --proxy-to http://localhost:8080   # once
apigenie-server openapi.yaml --replay sessions/checkout                                    # every run
apigenie-server --config apigenie-server.yaml   # replay: {dir: sessions/checkout}, several services
```

**Tests and order.** A request's test is its `X-Apigenie-Test` header (of a WebSocket
handshake too) or a gRPC call's `x-apigenie-test` metadata. It draws from the exchanges recorded
under that test, in their order: the first `GET /pets/1` of test `checkout` gets the first one
recorded for it, the second the second, and a third matches none. Tests replay side by side,
each with its own counters, so the parallel workers of a suite don't take each other's
exchanges. Requests without a tag share one test that draws from the whole session in order, for
suites that don't tag their requests (and MQTT, whose clients send none).
`POST /__apigenie/replay/reset {"test": "checkout"}` (or with the header) restarts a test's
counters, at the start of each test; without a test it restarts every test.

**What is compared.** The method and path; the query, in any order; the headers the recording
kept (not `Host`, `User-Agent`, `Accept-Encoding` and the other headers a client changes from
run to run: `Date`, `Origin`, `Referer`, `Accept-Language`, `Cache-Control`, the `Sec-*` headers,
`traceparent`, `X-Request-Id`, ...), a recorded `[redacted]` value standing for any; and the
body, JSON regardless of key order and number formatting. Left out:

- strings that are both RFC 3339 dates or date-times (`2026-10-13`, `2026-10-13T10:00:00Z`), in
  bodies and query parameters;
- the fields of an OpenAPI request body that its schema marks `readOnly`, which a client sends
  back as it got them (`id`, `createdAt`);
- what `--replay-ignore` (or `replay.ignore`) names: headers, and JSON pointers into bodies,
  messages, gRPC messages and GraphQL variables (`--replay-ignore /cartId --replay-ignore
  /items/*/addedAt`; `*` stands for any index or key).

**When nothing matches.** `--replay-missing fail`, the default, answers with a 502
`application/problem+json` naming the closest recording (of the same test and kind; the same
operation first, then the fewest differences) and how the request differs from it:

```json
{
  "type": "https://apigenie.pl/mock/errors#replay-missing",
  "title": "No recorded exchange matches",
  "status": 502,
  "detail": "no recorded exchange of test 'checkout' matches POST /carts; the closest, pets/0004-createCart.yaml, differs at /items/0/quantity",
  "test": "checkout",
  "closest": {"file": "pets/0004-createCart.yaml", "sequence": 4, "summary": "POST /carts 201", "used": false},
  "differences": [{"where": "/items/0/quantity", "recorded": 1, "received": 2}]
}
```

`used: true` says the matching exchange was replayed already in that test (a request sent once
more than when it was recorded). `generate` lets the mock answer (stubs, the document store,
examples, generated values); `passthrough` forwards the request to `--proxy-to`, and with
`--replay-update` records the run (replayed and forwarded exchanges alike) into `DIR.update` and
puts it in place of `DIR` when the server stops: the way to refresh a recording after the
application changed on purpose. A request that matched none is reported either way.

**Every protocol.**

- HTTP (OpenAPI, AsyncAPI HTTP channels, GraphQL batches): the recorded status, headers and body,
  with `X-Apigenie-Mock-Source: replay:0002-getPet.yaml`.
- GraphQL queries and mutations: by operation name, query (formatting aside) and variables;
  the recorded answer. One none matches gets a `REPLAY_MISSING` error (status 502) with the
  same members in `extensions.replay`.
- GraphQL subscriptions (graphql-transport-ws, graphql-ws, SSE) and AsyncAPI WebSocket channels:
  a connection replays a recorded connection of its route and test as the server's side: what
  the server sent first (`connection_ack`, the first scheduled messages) at once, then, after each
  message the client sends that is the recorded one, the messages that followed it. The
  subscriptions' ids are the client's own. A message that isn't the recorded one gets the problem
  as a message (GraphQL: an `error` message). The scheduled messages and generated replies are
  off.
- MQTT: a subscriber of a topic gets the messages a subscriber got before any client published;
  a published message that is the next recorded one gets the messages that followed it.
- gRPC: unary and server-streaming calls by their metadata and first request, client streams by
  all their requests, bidirectional streams in their recorded interleaving (the session's
  `order`): a message the client sends that isn't the recorded one fails the call. A call none
  matches fails with `FAILED_PRECONDITION`, the problem in the `apigenie-replay-problem` trailer.

WebSocket and MQTT messages can't be forwarded one at a time: under `passthrough` they are
answered as under `fail`.

**The report.** When the server stops it prints how many recorded exchanges were replayed, the
ones never replayed and the requests that matched none, with their tests (with
`--log-format json`, one `{"event": "replay", ...}` line), and exits with status 5 if there are
any; `--replay-update` prints the report but doesn't fail. `GET /__apigenie/replay` gives the
same report at any time, with `passed` and the exchanges each test used since its reset. The
startup line says `Replaying the session in DIR (N exchanges, missing: fail)` (`replay` in
JSON). A document that changed since the session was recorded (another SHA-256) and a service
the session lacks are warned about at startup. `--stubs`, the GraphQL examples and the gRPC stubs
stay: the per-exchange forms, where the first match answers whatever the test.

The design is ADR-169.

## Replay in a test suite

A Playwright suite recorded against the real backend once replays it in every run, each test
with its own exchanges. The server starts with the suite and every request of a test carries
its name:

```ts
// playwright.config.ts
import { defineConfig } from "@playwright/test";

export default defineConfig({
  webServer: {
    command: "apigenie-server openapi.yaml --port 4010 --replay recordings/checkout",
    url: "http://127.0.0.1:4010/__apigenie",
    reuseExistingServer: false,
  },
  globalTeardown: "./replay-teardown.ts",
});
```

```ts
// checkout.spec.ts
import { expect, test } from "@playwright/test";

const admin = "http://127.0.0.1:4010/__apigenie";

test.beforeEach(async ({ page }, testInfo) => {
  const name = testInfo.titlePath.join(" > ");
  // every request of the page names the test, the API's included
  await page.setExtraHTTPHeaders({ "X-Apigenie-Test": name });
  // the test starts at its first recorded exchange, also when it is retried
  await fetch(`${admin}/replay/reset`, { method: "POST", body: JSON.stringify({ test: name }) });
});

test("checks out a cart", async ({ page }) => {
  await page.goto("/cart");
  await page.getByRole("button", { name: "Check out" }).click();
  await expect(page.getByText("Thank you")).toBeVisible();
});
```

```ts
// replay-teardown.ts: the run fails when the application drifted from the recording
export default async function teardown() {
  const report = await (await fetch("http://127.0.0.1:4010/__apigenie/replay")).json();
  if (!report.passed) {
    throw new Error(
      `the replay failed: ${report.neverReplayedCount} exchanges never replayed, ` +
        `${report.unmatchedCount} requests matched none: ${JSON.stringify(report.unmatched, null, 2)}`,
    );
  }
}
```

The server answers the CORS preflight of the header for a page served from another origin.
To record the suite, run it once with `--record recordings/checkout --proxy-to
https://staging.example.com` in place of `--replay`: the same header tags the recorded exchanges
with the tests. Playwright stops its web servers without letting them finish, so such a
recording has no `session.yaml`; the replay reads the index from the exchange files, as
`session export` does. On CI, a request the recording lacks gets a 502, so the test that sent it fails
at that step, with the closest recording and the differences in the response; the teardown
fails the run when a test no longer sends what was recorded (exchanges never replayed). A job
that runs the server itself sees the same as its exit status:

```sh
apigenie-server openapi.yaml --port 4010 --replay recordings/checkout --log-format json > replay.log &
server=$!
npx playwright test
curl -s -X POST http://127.0.0.1:4010/__apigenie/shutdown
wait $server   # 5 when the run drifted from the recording; replay.log has the report
```

## Several services

One `apigenie-server` serves several documents at once: an application's backends, a gateway's
services, the HTTP and the messaging side of a system. Each document is a **service** with a name
and a **mount**, where its clients reach it. Several `SPEC`s on the command line are services named
after their files (`pets.openapi.yaml` is `pets`) and mounted at `/<name>`; a `.proto` file gets a
port of its own. A single `SPEC` is served as before, without a prefix. The configuration's
`services` list says more:

```yaml
version: "1.0"
server: {port: 4010}
validation: {request: enforce}   # the services' defaults
services:
  - name: pets
    spec: pets.yaml               # mounted at /pets
  - name: orders
    spec: orders.yaml
    mount: /shop/orders
    proxy: {target: "http://localhost:8080", mode: fallback, record_har: session.har}
    scenarios:
      outage: {rules: [{status: 503}]}
  - name: events
    spec: events.asyncapi.yaml
    mount: {host: events.localhost}
  - name: greeter
    spec: greeter.proto
    mount: {port: 50051}
```

- **What a service sets**: `name` and `spec` (required), `mount`, and the settings of its document:
  `proxy`, `stubs`, `scenarios`, `overrides`, `validation`, `security`, `operations`, `responses`,
  `generation`, `headers`, `asyncapi`, `graphql` and `grpc`. These keys at the top of the file are
  every service's defaults; a service's `stubs`, `scenarios`, `overrides` and `proxy` replace the
  defaults rather than adding to them. `server`, `cors`, `simulation`, `tls`, `admin`, `log`,
  `contract`, `scripts`, `db`, `request_overrides` and `asyncapi.mqtt_port` are the server's.
- **Mounts.** A path prefix (`/shop/orders`, default `/<name>`) on the server's port: the service
  gets the requests below it, without the prefix, so its own base path and routes are unchanged.
  `{host: events.localhost}` takes the requests whose `Host` header names that host (any port), and
  may add a `prefix`. `{port: N}` is a listener of the service's own (`0` picks a free one), which a
  `.proto` file's service needs: gRPC clients can't add a prefix. Requests the server's port gets
  go to the services by host first, then to the one with the longest prefix (prefixes end at a `/`);
  the others get 404 `no-service`, which lists the mounts. Two services can't share a name, nor
  can one's mount hide another's (`/shop` and `/shop/orders`), nor can a mount cover the admin API.
- **Proxies** forward the path without the mount's prefix, after their own (`--proxy-to`'s path).
  `--proxy-to`, `--stubs`, `--record-dir` and `--record-har` name one document's upstream and
  recordings, so with several services they are refused: set them per service.
- **WebSocket**: an AsyncAPI service's channels and a GraphQL service's subscriptions share the
  server's port, below the service's mount (`ws://127.0.0.1:4010/events/v1/rooms/1`).
- **MQTT**: one broker for every service (`--mqtt-port`). Topics are served as the documents name
  them, unless the MQTT channels of two services can match the same topic (`doors/+/open` in both,
  or `status` and `#`): then the topics of each of those services start with its name
  (`front/doors/7/open`). The startup line shows the prefix (`MQTT topics under front/`). An
  `mqtt://` upstream goes with one document.
- **The log** names the service of each event (`"service": "orders"` in JSON, `[orders]` in the
  stylish log); event paths are within the service, without its prefix. Ids are unique across the
  services.
- **The admin API** (`/__apigenie`) covers every service: `GET /__apigenie/services` lists them with
  their mounts and URLs, the items of `/routes` (full paths), `/scenarios`, `/overrides`, `/log` and
  the status's `documents` name their service, and `?service=NAME` narrows any endpoint to one
  service. An endpoint of one service's resources (a scenario of two services, `PUT /overrides`,
  `/state`, `/db`, `/spec`) needs it when more than one service could be meant, and answers 400
  `admin-service-required` otherwise; a service's own endpoints (the gRPC ones,
  `/contract/violations`) answer 404 without it. `PUT /simulation`,
  `POST /seed` and `POST /reload` apply to every service unless one is named.
- **The contract report** (`--report`, `GET /__apigenie/contract`) has a section per service in
  `sections`, each a whole report of its document with its `name` and `mount`, and a `summary` that
  counts them together: requests, violations and `coverage.combined`, the units of every service
  added up (OpenAPI branches, GraphQL fields, gRPC methods, AsyncAPI messages). `--fail-on` and
  `--min-coverage` apply to that summary: a violation of any service fails the run, and the
  combined coverage must reach the threshold (the reason names the service with the lowest). The
  JUnit report has a suite per service.
- **The HAR file** of the proxies is one per server: every service that records names the same
  file, and each entry's `_apigenie.service` names its service; URLs keep the mount's prefix.
- **Reloads** (`--watch`): a service reloads when its document, stubs or scripts change; a change
  of the configuration reloads every service. Adding or removing a service needs a restart.

## Document store

Scripts keep documents in collections: `ctx.db.<collection>`, made the first time a script names
it. Writes are kept in memory for the run (see `--db-file`), shared by every script and shown by
the admin API's `/__apigenie/db`.

```js
export default {
  onRequest(req, ctx) {
    if (req.method === 'POST' && req.path === '/notes') {
      return { status: 201, body: ctx.db.notes.insert(req.body) };
    }
    if (req.path === '/notes') {
      return { body: ctx.db.notes.find({ tags: { $contains: 'home' } }, { sort: '-id', limit: 10 }) };
    }
  },
};
```

| Method | Returns |
| --- | --- |
| `insert(document)` | The document as stored, with its id. |
| `get(id)` | The document, or `undefined`. `"3"` finds `3`. |
| `find(where, {limit, offset, sort})` | An array of documents. |
| `count(where)` | A number. |
| `all()` | Every document, in the order they were inserted. |
| `update(id, patch)` | The document after a [JSON merge patch](https://www.rfc-editor.org/rfc/rfc7396) (`null` removes a property), or `undefined` for an unknown id. The id stays. |
| `replace(id, document)` | The new document with the old id, or `undefined`. |
| `delete(id)` | `true` when there was such a document. |
| `clear()` | How many documents went. |

`ctx.db.collections()` lists the collections and `ctx.db.reset()` (`reset('notes')` for one) puts
them back to their seeds.

- **Queries.** `where` is an object of fields and values, all of which must match: `{status:
  'open'}`, `{'address.city': 'Kraków'}` (a path), or operators: `$eq`, `$ne`, `$in` (an array of
  values), `$gt`, `$gte`, `$lt`, `$lte` (numbers and strings), `$contains` (a substring, or an item
  of an array): `{weight: {$gte: 5, $lt: 20}}`. Numbers and strings are never equal. `sort` is
  `'field'`, `'-field'` (descending), `'a,-b'`, an array of them or `[{field, descending}]`;
  missing values sort last. Anything fancier is a JavaScript filter on a limited result.
- **Ids.** A document keeps its own `id` (a string or an integer), else gets the next one: 1, 2,
  3... (`"1"`, `"2"`... for a resource whose id is a string, a UUID made from the run's `--seed` for
  `format: uuid`). An id in use is refused, and the script's call throws.
- **Schemas.** The collection of a [resource](#crud-resources) is bound to its schema: a new
  document gets the `readOnly` properties it lacks (generated, like `createdAt`), and every write is
  checked. A document that doesn't match is kept and reported in the log, or refused (422 for a
  request, a throw for a script) when `validation.response` is `enforce` (`--strict`).
- **Seeds.** `db.seed` names a file of documents (a JSON or YAML array) per collection, or a
  directory of them (`seed/shipments.json` seeds `shipments`). A resource's collection without one
  is seeded from its list operation's example, else with `db.generate` (3) documents generated with
  the run's seed. `ctx.db.reset()` and `POST
  /__apigenie/db/reset` go back to the seeds, not to nothing. A seed that doesn't match the schema
  is kept and reported at the start; a file that can't be read stops the server with exit status
  2.
- **Tests.** A request with an `X-Apigenie-Test: <id>` header reads and writes a namespace of its
  own, which starts as a copy of the seeds: parallel test runs don't see each other's documents.
  The admin API's `/db` endpoints take the same header (or `?test=<id>`).
- **Snapshots.** `PUT /__apigenie/db/snapshots/{name}` copies the documents of every collection,
  `POST /__apigenie/db/snapshots/{name}/restore` puts them back.
- **Persistence.** In memory by default, so a restart is a clean slate. With `--db-file` (or
  `db.file`), the store is read from that JSON file at the start (a file that can't be read is
  reported, and the store starts empty) and written there when the server stops or on `POST
  /__apigenie/db/save`. The file holds `formatVersion` 1, the collections' seeds, the documents of
  each namespace and the snapshots. Seed files don't replace what the file holds.
- **Log.** Every write is a `db` event: the collection, `insert 3`, `update 3`, `delete 3`, `clear
  2 documents`, `reset`, `restore <name>`, `seed 3 documents from file`, who wrote (`script:mock.mjs`,
  `admin`) and the test's namespace (`test`). `onMessage` and `onGraphQL` use the default
  namespace.
- **After a reload.** The documents stay; collections are bound again, new ones seeded, and
  documents that no longer match their schema are reported.
- **Several services.** Each [service](#several-services) has a store of its own, which its
  scripts' `ctx.db` and its resources use; `db.seed` seeds every service's collections, and the
  admin API's `/db` endpoints take `?service=` when several could be meant. `--db-file` (and
  `db.file`) keep the store of a server of one document; with several services it exits with
  status 2.
- **Limits.** A collection keeps up to `db.max_documents` (10,000) documents per namespace. The
  store is not a database: no transactions, relations or durability promises.

### CRUD resources

With `db.crud: auto` (or `--crud`), a path and the path of its items, such as `/shipments` and
`/shipments/{id}`, make a resource whose collection (`shipments`) its operations read and write;
no script is needed. Without it (`db.crud: explicit`, the default), only the paths an
`x-apigenie-mock: {resource: ...}` extension names are resources; `db.crud: off` serves none.

| Operation | Answer |
| --- | --- |
| `GET /shipments` | The documents, paged and filtered by the operation's query parameters. |
| `POST /shipments` | The document stored, with its id and `readOnly` properties; 201 when the operation declares it. |
| `GET /shipments/{id}` | The document, or 404. |
| `PUT /shipments/{id}` | The document replaced (the id and `readOnly` properties stay), or 404. |
| `PATCH /shipments/{id}` | The document after a JSON merge patch, or 404. |
| `DELETE /shipments/{id}` | 204 when the operation declares it, else the document that went; 404 for an unknown id. |

- **The schema** is the list response's items, or the items of its object's array property (an
  envelope such as `{data: [...], total: 3}`: one array property, or the one called `data`,
  `items`, `results` or like the collection), else the item response's or the request body's. The
  id is the schema's `id` property, else the path parameter when it is a property; integers count,
  strings with `format: uuid` are UUIDs. Answers leave `writeOnly` properties out.
- **Lists** page by the list operation's parameters: `offset` and `limit`, `page` (from 1) and
  `per_page` (`page_size`, `pageSize`, `size`, `limit`), or `cursor` (`after`, `page_token`) and a
  size, the size's `default` when not given (20 without one). An envelope gets the page in its
  array property, and `total` (`totalCount`, `total_count`...), `hasMore`, `nextCursor`, `page`,
  `offset` and `limit` filled in when it declares them; its other members are generated. Other
  query parameters named like a property of the documents filter by it (`?status=open`), and `sort`
  (`order_by`) sorts: `-createdAt`, `status:desc,id`.
- **What comes first.** Requests are validated before the store sees them (422 under
  `validation.request: enforce`). A request's `Prefer` (or `__` parameters), a scenario's or an override's rule,
  an extension that sets the status, an example or a body, and stubs answer instead of the store.
  The store's answers are validated like any other, and say `X-Apigenie-Mock-Source: db`.
- **The extension**, on a path item or an operation of either path: `resource: shipments` (the
  collection's name), `resource: false` (not a resource, also with `db.crud: auto`) or
  `resource: {name: parcels, id: code, schema: Parcel, list: entries}` (the id property, the schema
  under `components/schemas` or as a `#/` reference, the envelope's array property).
- **Proxies.** A document served with `--proxy-to` has no resources: the server's state is the
  upstream's.

```yaml
paths:
  /shipments:
    x-apigenie-mock: {resource: {name: shipments, schema: Shipment}}
    get: ...
  /shipments/{id}:
    get: ...
```

## Configuration

`apigenie-server` reads `--config`, else `apigenie-server.yaml` (`.yml`, `.json`), then
`apigenie-mock.yaml` (`.yml`, `.json`; the name before the rename, still read) or `.apigenie.yaml`
in the current directory, else the shared `apigenie.yaml` of the project (in the current directory
or a parent, up to the root of the repository; see
[configuration](/docs/reference/cli/configuration#where-the-file-is-found)). In a file of its
own the settings are at the top level; in a shared configuration they are under `commands.mock`.
The command line wins over the environment variables, which win over the file;
[`apigenie config --for mock --origin`](/docs/reference/cli/config) shows which one set each
value. The JSON Schema of a file of its own is
`https://apigenie.pl/schemas/apigenie-server-config/latest.json`.

```yaml
version: "1.0"
specs: openapi.yaml
server: {host: 127.0.0.1, port: 4010, base_path: /api, trailing_slash: ignore, watch: true}
                         # strict_base_path: serve the paths only under the prefix (true with base_path)
cors: {enabled: true, origins: ["*"], allow_credentials: true, expose_headers: ["*"], max_age: 600}
responses: {mode: static}
headers: {diagnostics: true}
generation:
  seed: 42
  max_depth: 8
  null_probability: 0
  smart_names: false
  limits: {min_items: 1, max_items: 5, min_length: 1, max_length: 32, min_properties: 0, max_properties: 10, minimum: 0, maximum: 1000}
validation: {request: enforce, response: log, formats: true, unknown_query_parameters: allow}
security:
  mode: check            # off, check (presence) or enforce (the credentials below)
  credentials: {api_keys: {apiKey: [secret]}, bearer_tokens: [token], basic: [{user: u, password: p}]}
request_overrides: {header: true, query_params: true}
simulation: {latency: 100-300, jitter: 20, drop: {probability: 0.01, mode: reset}}
operations:
  getPet: {status: 404}
  GET /pets: {latency: 1000}
stubs: [recorded]          # OpenAPI: recorded exchanges to answer with (--stubs)
scenarios:
  outage:
    description: the backend is down
    rules:
      - match: {tag: pets}
        status: 503
        headers: {Retry-After: "30"}
        body: {title: Service unavailable}
overrides: []
scripts: [mock.mjs]      # relative to the configuration file
scripting: {timeout_ms: 1000, memory_limit_mb: 64, fail_mode: log}
asyncapi:
  server: null           # the one server to serve
  publish: {enabled: true, interval: 1000, count: 0, jitter: 0}
  validate_incoming: enforce   # off, log or enforce
  mqtt_port: 1883        # 8883 when tls.cert serves MQTT over TLS
grpc:
  proto_path: [protos]   # relative to the configuration file
  stubs: [stubs]
  max_message_bytes: 4194304
  reflection: true
  health: true
  validation: {request: enforce, response: log}   # off, log or enforce; both log with --proxy-to unless set here
  generation: true       # false: UNIMPLEMENTED when nothing answers
  proxy: {target: null, mode: fallback, record_dir: null, redact: [authorization, cookie, x-api-key, proxy-authorization]}   # target: HOST:PORT (h2c) or https://host[:port] (TLS)
  history_size: 1000
graphql:
  path: /graphql         # under server.base_path
  introspection: true
  playground: true       # GraphiQL
  schema_endpoint: true  # /graphql/schema.graphql
  allow_get_mutations: false
  lists: {min: 1, max: 3}
  null_probability: 0
  max_depth: 0           # deeper operations are rejected; 0 for no limit
  max_batch: 10
  fields:
    User.email: {faker: internet.email}
    Book.pages: {min: 50, max: 900}
    Status: {values: [OPEN, CLOSED]}
  examples: []           # as the files of examples_files
  examples_files: [more_examples]
  discover_examples: true      # schema_examples/ next to schema.graphql
  store: {enabled: true, max_entities: 10000, unknown_ids: generate}
  subscriptions: {enabled: true, interval: 1000, count: 0, jitter: 0, legacy_protocol: true, init_timeout: 3000}
proxy:                   # OpenAPI, GraphQL: contract testing against a real server (gRPC: grpc.proxy)
  target: http://localhost:8080   # or https://, HOST:PORT, ws:// or wss:// (AsyncAPI); OpenAPI, AsyncAPI: a path is put before the requests' paths,
                                  # AsyncAPI: a list may add an mqtt:// or mqtts:// broker: [ws://localhost:9000, mqtt://localhost:1884]
                                  # GraphQL: the path is the server's endpoint
  unknown_routes: forward         # or reject (404, 405)
  mode: always                    # or fallback: OpenAPI: the mock answers when the server can't be
                                  # reached, times out or answers 404/501 for an operation;
                                  # GraphQL: examples, scenarios, scripts answer first
  schema_check: true              # GraphQL: introspect the server and compare the schemas
  record_dir: null                # OpenAPI: write the exchanges as stub files there;
                                  # GraphQL: write the answers as examples there
  record_har: null                # OpenAPI: write the session as a HAR file
  redact: [authorization, cookie, set-cookie, proxy-authorization, x-api-key]
  timeout_ms: 30000
  connect_timeout_ms: 5000
  max_in_flight: 64
  max_response_bytes: 16777216
contract:
  report: contract.json           # relative to the configuration file
  report_junit: contract.xml
  fail_on: none                   # none, errors or warnings: exit status 5
  min_coverage: 0                 # percent of operations, request bodies and responses (GraphQL: fields; AsyncAPI: messages; gRPC: methods)
  severity: {response.header.undefined: warn, response.status.default: ignore}   # GraphQL's, AsyncAPI's and gRPC's checks too
record:                   # see Session recording; the directory relative to the configuration file
  dir: null               # record the session there from the start (--record); new or empty
  name: null              # the session's name; the directory's when not given
  redact: []              # hidden besides proxy.redact: header names, and JSON pointers (/password) to values of bodies and messages
replay:                   # see Session replay; the directory relative to the configuration file
  dir: null               # answer from the session recorded there (--replay)
  missing: fail           # fail (502 problem), generate (the mock answers) or passthrough (proxy.target answers)
  ignore: []              # not compared besides dates, readOnly fields and volatile headers: header names, JSON pointers (/id, /items/*/id)
  update: false           # with missing: passthrough, record the run and replace the session with it at stop
admin: {enabled: true, path: /__apigenie, port: null, allow_remote: false, log_size: 1000}
log: {level: info, format: stylish, bodies: false, body_limit: 4096, headers: false, file: null}
tls:                      # see TLS; paths relative to the configuration file
  cert: null              # serve HTTPS with this certificate chain and key (both or neither)
  key: null
  client_ca: null         # require client certificates of these authorities (mTLS); needs cert
  ca_certs: []            # trusted for https://, wss:// and mqtts:// upstreams, besides the system's roots
  insecure: false         # don't verify upstream certificates
  client_cert: null       # presented to upstreams that ask for a client certificate (with client_key)
  client_key: null
db:                       # see Document store; paths relative to the configuration file
  crud: explicit          # auto: serve /things and /things/{id} from the store (--crud); explicit: only x-apigenie-mock resources; off
  seed: {notes: seed/notes.json}   # or a directory of <collection>.json/.yaml files: seed, seed/*.json
  generate: 3             # documents generated for a resource's collection nothing else seeds
  max_documents: 10000    # per collection and namespace; 0 for no limit
  file: null              # keep the store across runs (--db-file)
```

A mistake in the file stops the server with exit status 2, the position and the JSON pointer of
the value; an unknown setting is reported with the closest known name.

## Dashboard

`http://127.0.0.1:4010/__apigenie/ui` is a page served by the server itself, for a browser on the
same machine: it follows the [admin API](#admin-api)'s rules (local clients only unless
`admin.allow_remote`, on `admin.port` when the admin API has a port of its own, off with
`--admin off`). The header on every screen names the documents and how they answer (mock, proxy,
generated), where they listen, the seed, the uptime, the requests and violations so far, and has
Reload and Stop; with [several services](#several-services) a switcher narrows every screen to
one.

| Screen | What it shows and does |
| --- | --- |
| Traffic | Every exchange as it happens (the [event stream](#the-event-stream)), with filters; an exchange's request and answer (headers and bodies, captured while the page is open) and its validation problems with where they point in the document. Copy as curl, Replay, Save as stub (`POST /stubs`), Add to `.http` (a file built up in the browser, to copy or download), Copy as YAML (the answer as an `operations` entry of the configuration). Record session and Save session: a [session recording](#session-recording) of what happens from here, written where the server runs. |
| Operations | The operations by tag, with their hits, their last status and what answered them last (an example, a generated body, a scenario, a proxy). For one operation: a fixed answer (a declared status, one of its examples, a generated body, a dropped connection), a latency and whether its requests are validated, saved as an always-on [override](#scenarios-and-extensions) (`PUT /overrides`) until the server stops, with the `Prefer` header (and `__` parameters) that do the same for one request. |
| Scenarios and network | The scenarios of the configuration, the document and the admin API, switched on and off, with their rules and hits; a builder for a new one (by operation, tag, method and path, header or query parameter; status, example, latency, probability); the network simulation of every response (fixed, uniform, normal or log-normal latency, jitter, dropped connections, bandwidth, the seed) with the response times of the last requests. |
| Contract | Whether the server would pass `--fail-on` and `--min-coverage` if it stopped now, the coverage, the violations by check (and the exchanges they were seen in), the operations never called, the report as JSON or JUnit to download, and Start over (`DELETE /contract`). |
| Data | The [document store](#document-store)'s collections with their documents, Reset to seed, snapshots to save and restore; and `ctx.state`, the values the scripts keep, to edit or reset. |
| Channels | For an [AsyncAPI](#asyncapi) document only (the screen is listed when a served document has channels): the channels by WebSocket, MQTT and HTTP, with their operations, the clients connected and the messages counted (`GET /asyncapi/channels`); for a channel, its messages in and out as the event stream brings them (with an exchange's payload and validation problems, and Open in Traffic), its clients (`GET /asyncapi/clients`, followed through the `ws-open`, `ws-close`, `mqtt-subscribe` and `mqtt-unsubscribe` events), and Publish: a message sent as the application would (`POST /asyncapi/publish`), to every client of the channel or to one, with a payload started from the document's examples or left to the server; a payload the message's schema refuses shows the violations and goes nowhere, until Send anyway; Copy as curl. |

Every panel copies its settings as the configuration's YAML, or as `x-apigenie-mock` for the
document, so what was tried in the dashboard is kept in a file (Channels changes no setting: it
copies the publish as curl). The page follows the system's light or dark theme, or the one chosen
in the header.

The page is built from the web components package (`src/app_web`, the `apg-server-*`
elements) and embedded in the binary; nothing is fetched from the network. The design is ADR-167.

![The Traffic screen of the dashboard](/server-dashboard-light.png)

## Admin API

On by default for local clients only (`admin.allow_remote` opens it, `admin.port` moves it to a
port of its own). The `/db` endpoints work on the namespace of an `X-Apigenie-Test` header (or a
`?test=` parameter), the default one without it:

| Endpoint | Description |
| --- | --- |
| `GET /__apigenie` | Status: version, `url` (where clients reach the server), document (with its proxies and stubs; an AsyncAPI document's `mqttProxy` and `mqttClients`), seed, uptime (`uptimeSeconds`, `startedAt`), requests (the clients', not the admin API's), active scenarios, simulation, `logSize`. |
| `GET /__apigenie/ui/` | The [dashboard](#dashboard): the page and its files (`GET /__apigenie/ui` redirects to it). |
| `GET /__apigenie/routes` | The operations served, with their `hits`, `lastStatus`, `lastSource` (what answered last) and `lastAt`; an OpenAPI operation's `tags`, `deprecated` and `responses` (each status with its content types and the names of its examples). |
| `DELETE /__apigenie/hits` | Start the hits of routes and scenarios over (a reload keeps them). |
| `GET /__apigenie/services` | The services: name, mount, URL, document (see [several services](#several-services)). |
| `GET /__apigenie/scenarios`, `PUT` / `DELETE /__apigenie/scenarios/{name}` | List (with `hits`, `lastStatus`, `lastSource` and `lastAt`, and the rules as `ruleList` next to their count `rules`), add or remove scenarios. |
| `POST /__apigenie/scenarios/{name}/activate`, `/deactivate` | Switch a scenario on or off. |
| `GET`, `PUT /__apigenie/overrides` | The always-on rules, whole: what `GET` answers, `PUT` takes back. |
| `GET`, `PUT /__apigenie/simulation` | The network simulation of every response. |
| `POST /__apigenie/seed` | Reseed: `{"seed": 42}`. |
| `GET`, `DELETE /__apigenie/log?limit=&after=&level=` | The recent events, with headers and bodies when they were captured (see [log entries](#log-entries)). |
| `GET /__apigenie/events?type=&level=&service=&after=` | The events as they happen, as Server-Sent Events (see [the event stream](#the-event-stream)). |
| `POST /__apigenie/stubs` | A stub file of a logged exchange: `{"id": 42, "dir": "stubs", "name": "rex"}` (see [stubs from the log](#stubs-from-the-log)). |
| `GET /__apigenie/record` | The [session recording](#session-recording): `recording`, and the session's `dir`, `name`, `started`, `exchanges` while one is on; `test`, the mark. |
| `POST /__apigenie/record/start`, `/stop` | Record a session in `{"dir": "sessions/run-7", "name": "run 7"}` (`dir` defaults to `record.dir`), or stop it and write `session.yaml`. |
| `POST /__apigenie/record/mark` | `{"test": "checkout-happy"}`: the test of the exchanges to come that carry no `X-Apigenie-Test` of their own; `{}` clears it. |
| `GET /__apigenie/replay` | The [session replay](#session-replay)'s report so far: `exchanges`, `replayed`, `neverReplayed`, `unmatched` (with their tests and closest recordings), `passed`, and `tests`, the exchanges each test used since its reset. |
| `POST /__apigenie/replay/reset` | Restart a test's counters: `{"test": "checkout"}`, or the `X-Apigenie-Test` header, or `?test=`; without one, every test's. 204. |
| `POST /__apigenie/reload`, `POST /__apigenie/shutdown` | Reload, or stop gracefully. |
| `GET /__apigenie/spec` | The served document, bundled (converted to OpenAPI 3.2 for a Swagger 2.0 document; a `.proto` file with its imports, or a GraphQL schema, as text). |
| `GET`, `PUT`, `DELETE /__apigenie/state/{key}` | Values kept between requests. |
| `GET /__apigenie/db` | The [document store](#document-store): its file, namespaces, snapshots and collections (documents, seeds, id, and the schema and operations of a resource's collection). |
| `GET /__apigenie/db/{name}?limit=&offset=` | A collection's documents (100 by default) and `total`. |
| `PUT`, `DELETE /__apigenie/db/{name}` | Replace every document of a collection (a JSON array; all or nothing: 422 when the schema refuses one, 409 for an id used twice), or remove them. |
| `POST /__apigenie/db/reset` | Back to the seeds: with `X-Apigenie-Test`, that namespace; without it, the default one, and the tests' namespaces go. `{"collections": ["notes"]}` resets some. |
| `GET /__apigenie/db/snapshots`, `PUT`, `DELETE /__apigenie/db/snapshots/{name}` | List, take (or replace) or remove a snapshot of the documents. |
| `POST /__apigenie/db/snapshots/{name}/restore` | Put a snapshot's documents back. |
| `POST /__apigenie/db/save` | Write the store to `--db-file` now (501 without one). |
| `DELETE /__apigenie/db/namespaces/{test}` | Forget a test's namespace. |
| `GET`, `DELETE /__apigenie/contract` | The contract report (coverage and violations), or start it over. |
| `GET /__apigenie/contract.json`, `GET /__apigenie/contract.xml` | The contract report as `--report` (JSON) and `--report-junit` (JUnit XML) would write it now, to save. |
| `GET /__apigenie/contract/violations?operation=&check=&limit=` | The violations seen, by operation (id or `GET /pets/{id}`; GraphQL: the operation's name or a root field such as `Query.book`; AsyncAPI: the operation's key; gRPC: `package.Service/Method` or its path) and check. |
| `POST /__apigenie/contract/schema-check` | GraphQL proxy: introspect the server and compare its schema now (202; the result is logged and reported). |
| `GET /__apigenie/asyncapi/channels` | AsyncAPI: the channels, their operations and messages (with the document's examples), the clients connected, and the messages counted (see [AsyncAPI channels](#asyncapi-channels-from-the-admin-api)). |
| `GET /__apigenie/asyncapi/clients` | AsyncAPI: the WebSocket clients and MQTT subscriptions of the channels. |
| `POST /__apigenie/asyncapi/publish` | AsyncAPI: send a message to a channel's clients, or to one of them: `{"channel": "rooms/42", "payload": {"text": "hi"}}`, `{"channel": "room", "client": 7, ...}`. |
| `GET /__apigenie/grpc/services` | gRPC: the services and their methods. |
| `GET`, `POST`, `DELETE /__apigenie/grpc/stubs` | gRPC: list, add (one stub or a list) or remove the stubs added at run time (`?scope=all`: every stub). |
| `GET`, `PUT`, `DELETE /__apigenie/grpc/stubs/{name}` | gRPC: one stub. |
| `POST /__apigenie/grpc/stubs/reset`, `GET /__apigenie/grpc/stubs/schema` | gRPC: back to the stub files, and the stubs' JSON Schema. |
| `GET`, `DELETE /__apigenie/grpc/history?method=&limit=&after=&q=` | gRPC: the calls received, newest last. |
| `POST /__apigenie/grpc/verify` | gRPC: count the calls that match `{method, request, metadata, stub, code}`. |

A server of [several services](#several-services) names the service of each item, and takes
`?service=NAME` on every endpoint.

### The event stream

`GET /__apigenie/events` keeps the connection open and writes each event of the log as it happens,
as [Server-Sent Events](https://html.spec.whatwg.org/multipage/server-sent-events.html): `id:` is
the log entry's id, `event:` its kind, and `data:` the same JSON object as a `/log` entry. A
dashboard reads it with `EventSource`; a script with `curl -N`:

```bash
curl -N 'http://127.0.0.1:4010/__apigenie/events?type=request,violation'
```

```text
: connected

id: 12
event: request
data: {"event":"request","id":12,"time":"2026-10-12T09:00:00.000Z","level":"info","method":"POST","path":"/pets","operationId":"addPet","status":201,...}

id: 13
event: request
data: {"event":"request","id":13,...,"status":422,"violations":[...]}

id: 13
event: violation
data: {"event":"request","id":13,...,"status":422,"violations":[...]}
```

| Event | When |
| --- | --- |
| `request` | An exchange, request and answer together (there is no separate response event). |
| `violation` | After an event with violations: the same id and data. |
| `ws-open`, `ws-in`, `ws-out`, `ws-close`, `mqtt-*`, `grpc`, `grpc-in`, `grpc-out` | WebSocket, MQTT and gRPC traffic, as in the log. |
| `scenario` | A scenario was added, replaced, removed, activated or deactivated through the admin API (`message` says which). |
| `reload` | The documents were reloaded (`POST /reload` or `--watch`). |
| `contract-updated` | The contract report's coverage changed (at most once a second), or it started over (`message: "reset"`, after `DELETE /contract` or a reload). |
| `db` | A write to the [document store](#document-store): the collection, what changed, who wrote. |
| `admin`, `script`, `server` | The admin API's own exchanges, the scripts' logs, the server's errors. |

`?type=` keeps the named events, `?level=` those at a level and above (default `debug`),
`?service=` one service's. `?after=ID`, or the `Last-Event-ID` header a reconnecting `EventSource`
sends, first replays the log's later entries, so a client that reconnects misses nothing the log
still holds. A comment every 15 seconds keeps the connection alive. A client that falls more than
1 MiB behind is disconnected. The stream follows the admin API's rules: local clients only unless
`admin.allow_remote`, on `admin.port` when the admin API has a port of its own.

### Log entries

While at least one event stream is open, the server captures the headers and bodies of every
exchange, as `log.headers: true` and `log.bodies: true` do. `/log` entries and stream events
then carry `headers` and `responseHeaders` (arrays of `[name, value]`), `requestBody` and
`responseBody`, and `captured: {"headers": true, "bodies": true}`. A body that isn't UTF-8 is in
Base64 with `requestBodyEncoding` (or `responseBodyEncoding`) `"base64"`. A body is kept up to
`log.body_limit` bytes (4096 by default); a longer one is cut and marked `requestBodyTruncated` or
`responseBodyTruncated`, and `requestSize` and `responseSize` keep the whole sizes.

### Stubs from the log

`POST /__apigenie/stubs` makes a [stub file](#contract-testing-validating-proxy) of a logged exchange of an OpenAPI document,
as `--record-dir` writes them: the client's headers without the connection's, the hidden headers
of `proxy.redact` as `[redacted]`, and the query without the mock's own `__` parameters.

```bash
curl -X POST http://127.0.0.1:4010/__apigenie/stubs -d '{"id": 12, "dir": "stubs", "name": "new-pet"}'
```

With `dir`, or else the service's `proxy.record_dir`, the stub is written to a new file there,
`<operationId>-N.yaml`, and the answer (201) has the `file` and the `stub`. Without either the
answer (200) is the file's content, `{"stubs": [...]}`. The entry must still be in the log, and
must have been captured whole (an open event stream, or `log.headers` and `log.bodies`, and a
`log.body_limit` above its bodies' sizes): else 404 or 409. The running mock answers with the new
stub after `POST /__apigenie/reload` when `dir` is one of its `stubs` directories.

### AsyncAPI channels from the admin API

`GET /__apigenie/asyncapi/channels` lists the channels: `key`, `address`, `protocols`, `paths`,
`messages` (their names) and `messageList` (each message's `name`, `contentType`, `summary` and
`examples`, the document's, each with its `payload` and `headers`: a starting point for a payload
to publish), `operations` (with `qos` and `retain` for MQTT), the `clients` connected now and the
contract report's `connections`, `messagesIn`, `messagesOut` and `unmatched` counts.
`GET /__apigenie/asyncapi/clients` lists the WebSocket clients and MQTT subscriptions: `id`,
`protocol`, `remote`, `channel`, `address`, `route`, `path` (the WebSocket path, or the MQTT
filter), `clientId` (MQTT), `connectedAt` and `messagesOut`.

`POST /__apigenie/asyncapi/publish` sends a message to the clients of a channel, as the schedule
sends one: scripts see it, and each delivery is logged as `ws-out` or `mqtt-out` with
`source: "admin"`.

```bash
curl -X POST http://127.0.0.1:4010/__apigenie/asyncapi/publish \
  -d '{"channel": "rooms/42", "message": "notice", "payload": {"text": "maintenance at noon"}}'
```

```json
{"channel": "room", "address": "rooms/42", "operation": "sendNotices", "message": "notice", "source": "admin",
 "contentType": "application/json", "payload": "{\"text\":\"maintenance at noon\"}", "delivered": 1,
 "webSocket": 1, "mqtt": 0, "clients": [7]}
```

With a [Kafka tap](#contract-testing-of-kafka-channels), a Kafka channel's message is produced to
the cluster instead. The record's `key`, `headers` (an object of strings) and `partition` may be
given; without them the key and headers are made from the message's Kafka binding and headers, and
the partition is the key's. The record is checked as the tap checks records (422 with the
`violations`, nothing produced, unless `"validate": false`), produced, and answered with where it
went; a cluster that can't be reached or refuses it answers 502. The tap then reads it like any
other record.

```bash
curl -X POST http://127.0.0.1:4010/__apigenie/asyncapi/publish \
  -d '{"channel": "orders", "payload": {"sku": "A-1"}, "key": "cust-1", "headers": {"correlationId": "c-1"}}'
```

```json
{"channel": "orders", "operation": "placeOrder", "message": "order", "source": "admin",
 "contentType": "application/json", "payload": "{\"sku\":\"A-1\"}",
 "kafka": {"upstream": "kafka://localhost:9092", "topic": "shop.orders", "partition": 1, "offset": 42,
 "key": "cust-1", "headers": {"correlationId": "c-1"}}}
```

With an [AMQP tap](#contract-testing-of-amqp-channels), an AMQP channel's message is published to
the broker: a queue channel's through the default exchange, any other to its exchange with
`routingKey` (else the address given, else the address with its parameters filled). Its
`properties` (`contentType`, `contentEncoding`, `deliveryMode`, `priority`, `correlationId`,
`replyTo`, `expiration`, `messageId`, `timestamp`, `type`, `userId`, `appId`) and `headers` may be
given; without them they come from the message's and the operation's AMQP bindings and the message's
headers. The message is checked (422), published mandatory with publisher confirms, and answered
with `{"amqp": {"upstream", "exchange", "routingKey", "headers"}}`; a message no queue takes answers
409 `asyncapi-unroutable`.

`channel` is a channel's key (every client of the channel gets the message) or an address with its
parameters (that address's clients only); `client`, an `id` of `/asyncapi/clients`, narrows it to
that one client. `message` names one of the channel's messages; without it, the first message of
its first `send` operation is sent. `payload` is the message as JSON, `raw` as text, and without
either one is made as for the schedule. A payload that doesn't match the message's schema answers
422 with the `violations` and is sent to nobody, unless `"validate": false`. Unknown channels and
messages answer 404, as does a `client` that isn't connected to the channel
(`asyncapi-unknown-client`). An admin publish isn't retained by the MQTT
broker. Only clients the mock matched to a channel get it. Clients of a validating proxy are counted
in the status, not listed. A server of several services lists every AsyncAPI service's channels and
clients, and needs `?service=` to publish when several services serve AsyncAPI.

## TLS

`apigenie-server` serves TLS itself, and its proxies reach `https://` and `wss://` servers and
`mqtts://` brokers. TLS 1.2 and 1.3 come from Mbed TLS. It's built with what modern servers use:
AES-GCM and ChaCha20-Poly1305, ECDHE key exchange on x25519, P-256, P-384 and P-521, and RSA and
ECDSA certificates. A server that offers only static RSA or DHE key exchange, or other ciphers,
can't be reached.

**Serving.** `--tls-cert server.pem --tls-key server.key` (or `tls.cert` and `tls.key` in the
configuration) turns the port into HTTPS. WebSocket channels and GraphQL subscriptions become
WSS, and clients that ask for `h2` by ALPN get HTTP/2, which is how browsers and gRPC clients
over TLS connect. The admin API on a port of its own (`admin.port`) uses the same certificate. The MQTT
broker of an AsyncAPI document serves MQTT over TLS too, on port 8883 unless `--mqtt-port` says
otherwise. The startup line shows `https://` and `mqtts://` URLs and, as JSON, `"tls": true`. The
certificate file may hold the chain, the server's certificate first. The key must be PEM and not
encrypted. For local development, `mkcert localhost 127.0.0.1` makes a pair your system trusts:

```sh
apigenie-server openapi.yaml --tls-cert localhost+1.pem --tls-key localhost+1-key.pem
curl https://localhost:4010/pets
```

**Upstreams.** `--proxy-to https://api.example.com` (OpenAPI, GraphQL, AsyncAPI HTTP channels),
`wss://...` (AsyncAPI WebSocket channels), `mqtts://...` (AsyncAPI MQTT channels, port 8883 unless
given) and `https://host[:port]` (gRPC, HTTP/2 over TLS) connect over TLS, port 443 unless given.
Certificates are verified against the system's roots:

- macOS: the system keychain, and the certificates an administrator or the user marked as always
  trusted;
- Windows: the ROOT store;
- Linux: the first CA bundle found (`/etc/ssl/certs/ca-certificates.crt`, `/etc/pki/tls/certs/ca-bundle.crt`, ...).

`SSL_CERT_FILE` and `SSL_CERT_DIR` replace them on every system. `--ca-cert FILE` adds authorities,
such as a company's own root or a test CA, and `--insecure` skips verification altogether. The host
name of the URL is checked against the certificate (an IP address against its IP addresses). An
upstream whose certificate is rejected gets the usual 502 `upstream-unreachable`, with the reason
in the detail.

```yaml
tls:
  cert: certs/server.pem     # relative to the configuration file
  key: certs/server.key
  ca_certs: [certs/company-root.pem]
  insecure: false
```

### Client certificates (mTLS)

**Requiring them.** `--tls-client-ca clients-ca.pem` (or `tls.client_ca`) makes every HTTPS, WSS,
gRPC and MQTTS connection present a client certificate issued by an authority of that PEM file, as
a service behind mutual TLS does. It needs `--tls-cert`. The certificate's extended key usage must
allow client authentication when it has one.

- A client without a certificate, or with one the authorities didn't issue, fails the TLS
  handshake. The log gets a warning that says why, for example `TLS handshake with 127.0.0.1
  failed: the client presented no certificate`.
- The subject of an accepted certificate (`CN=billing, O=Example`) is in each event of the JSON log
  (`"clientSubject"`), on a `~ client` line of the stylish log, and in the admin API's log.
- The contract report lists the clients: `clients` in JSON (`[{"subject": "CN=billing, O=Example",
  "exchanges": 12}]`, counting HTTP requests, gRPC calls, WebSocket connections and MQTT
  messages), a `client` property per subject in JUnit, and a `Clients:` line in the summary.
- The admin API asks for a certificate too, on the main port and on `admin.port`.

```sh
apigenie-server openapi.yaml --tls-cert server.pem --tls-key server.key --tls-client-ca clients-ca.pem
curl --cacert ca.pem --cert client.pem --key client.key https://localhost:4010/pets
```

**Presenting one.** `--client-cert client.pem --client-key client.key` (or `tls.client_cert` and
`tls.client_key`) is the certificate the proxies present to `https://`, `wss://`, `mqtts://` and
gRPC TLS upstreams that ask for one. Upstreams that don't ask never see it. It works with
`--insecure` too. The key must be PEM and not encrypted. An upstream that refuses the proxy for
want of a certificate gets the usual 502 `upstream-unreachable`, which says so in the detail (an
MQTT client's CONNECT gets return code 3).

```yaml
tls:
  cert: certs/server.pem
  key: certs/server.key
  client_ca: certs/partners-ca.pem
  client_cert: certs/proxy.pem
  client_key: certs/proxy.key
```

**Not supported.** There is no session resumption: every connection does a full handshake. Neither
certificate revocation lists (CRL) nor OCSP are checked, so a revoked client or upstream
certificate is accepted as long as its chain is valid. Remove a revoked client's authority from
`--tls-client-ca`, or issue client certificates with short lifetimes.

## Docker

```sh
docker run --rm -p 4010:4010 -v "$PWD:/mnt/data" mykulyak/apigenie-server openapi.yaml
```

It serves GraphQL schemas the same way (`mykulyak/apigenie-server schema.graphql`).

The image listens on every interface of the container (`APIGENIE_SERVER_HOST=0.0.0.0`) and exposes
port 4010, and 1883 for the MQTT broker (`-p 1883:1883`). It has the CA certificates of Alpine
Linux, so `https://` upstreams are verified; mount a certificate and its key to serve HTTPS
(`-v "$PWD/certs:/certs" ... --tls-cert /certs/server.pem --tls-key /certs/server.key`).

## npm

```sh
npx --package @mykulyak/api-genie apigenie-server openapi.yaml
```

## Deprecated names

`apigenie-server` was called `apigenie-mock` before. The old names keep working until 1.1, with a
one-line notice on standard error:

- `apigenie-mock`: a symlink to `apigenie-server` in the archives, `.deb`, `.rpm` and Homebrew, a
  copy in the Windows archive, and a command of the npm package.
- `APIGENIE_MOCK_*` variables: read when the `APIGENIE_SERVER_*` variable of the same name is
  unset.
- `mykulyak/apigenie-mock`: its `latest` tag only prints the new image name and exits with 2; the
  older tags still hold the old server.

What did not change: the `apigenie-mock.yaml` configuration file (still read, after
`apigenie-server.yaml`), the `commands.mock` section of a shared configuration, the
`x-apigenie-mock` extensions, the `X-Apigenie-Mock-*` headers and the `/__apigenie` admin API.

## Limits

- TLS: no session resumption, no revocation checks (OCSP, CRLs) of server or client certificates
  (see [Client certificates](#client-certificates-mtls)). The OpenAPI and
  GraphQL proxies speak HTTP/1.1 to the server, a connection per request; the AsyncAPI one connects
  to WebSocket servers without extensions. The AsyncAPI proxy relays WebSocket messages up to
  16 MiB; of MQTT 5 it relays the message properties, not those of CONNECT, SUBSCRIBE or
  DISCONNECT, nor subscription options and identifiers, and the broker publishes wills without
  their delay. Request and response bodies are buffered (16 MiB by default), so streams such as
  server-sent events don't fit. A HAR file is kept in memory until the server stops. OpenAPI
  stubs match exact paths and queries, not templates.
- gRPC: native gRPC over HTTP/2 (cleartext, or TLS with `--tls-cert`) only; gRPC-Web, the Connect protocol and REST
  transcoding (`google.api.http`) aren't served, and messages can't be compressed. Reflection
  doesn't include custom options.
- Several services: one MQTT broker for all of them, so an `mqtt://` upstream goes with one
  document; one HAR file. Services can't be added or removed without a restart.
- Session recording: exchanges are numbered in the order they completed; a gRPC call keeps at
  most 100 messages per side, a session at most 10 000 exchanges; a `.proto` document's hash
  covers the file, not its imports; MQTT messages keep neither QoS nor retain nor MQTT 5
  properties; `session export` leaves WebSocket and MQTT messages out of the HAR file.
- The document store is the OpenAPI document's and the scripts'; the GraphQL mock keeps its own
  [store](#the-store), and AsyncAPI and gRPC mocks see the documents only through scripts. CRUD
  resources are a path and its items' path: nested collections (`/users/{id}/orders`) are
  collections of their own, not filtered by the parent's id, and `readOnly` properties are filled
  in at the top level only.
- AsyncAPI: Kafka, AMQP and other protocols aren't served; Avro and Protobuf payloads are sent
  from their examples only, and not validated. The MQTT broker keeps no sessions between
  connections and sends at most QoS 1.
- XML bodies: no DTDs (and so no entities but the five predefined ones), UTF-8 and UTF-16 only;
  mixed content and `xsi:type` aren't mapped, and `nodeType: none` is honoured on arrays only.
- Callbacks, webhooks and links are not mocked. OpenAPI 3.2 streams described by
  `itemSchema` (server-sent events, JSON Lines) are answered with an empty body.
- GraphQL: no federation (`_entities`, `_service`), `@defer` and `@stream`, persisted queries or
  file uploads. Scenarios don't apply to subscriptions over WebSocket or server-sent events. The
  proxy doesn't forward subscriptions, and its schema check leaves out what only recent servers
  introspect (`@specifiedBy` URLs, `@oneOf`, repeatable directives, deprecated arguments) and the
  directives applied in the schema.
