How the mock picks a response

apigenie-server openapi.yaml answers every request from the document, without a line of code. This page explains the decisions behind one answer, in the order the server takes them, so that you can predict what a request gets and change it where you need to. The Responses page of the reference states the same rules in short.

The examples run against this document, saved as pets.yaml and served with apigenie-server pets.yaml; curl -i shows the headers that say what was decided.

openapi: 3.1.0
info: {title: Pets, version: 1.0.0}
servers:
  - url: https://api.example.com/v1
paths:
  /pets/{id}:
    get:
      operationId: getPet
      parameters:
        - {name: id, in: path, required: true, schema: {type: integer}}
      responses:
        "200":
          description: The pet
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Pet"}
              examples:
                cat: {value: {id: 1, name: Tom, kind: cat}}
                dog: {value: {id: 2, name: Rex, kind: dog}}
        "404":
          description: No such pet
        default:
          description: An error
components:
  schemas:
    Pet:
      type: object
      required: [id, name]
      properties:
        id: {type: integer}
        name: {type: string}
        kind: {type: string, enum: [cat, dog]}

1. The route

The paths of the document are served under the path of its first server, /v1 here, and as they are: GET /v1/pets/1 and GET /pets/1 both reach getPet. --base-path replaces the prefix, and server.strict_base_path in the configuration serves the paths only under it. When a literal path and a templated one both match, the literal one wins: /pets/mine over /pets/{id}.

A path the document doesn’t have is answered with 404, a method the path doesn’t declare with 405 and an Allow header, both as problem details:

$ curl -i -X DELETE http://127.0.0.1:4010/v1/pets/1
HTTP/1.1 405 Method Not Allowed
Content-Type: application/problem+json
Allow: GET

2. The request is checked

Before an answer is chosen, the request is checked against its operation: the parameters, the body and the security requirements. A request that doesn’t match gets 422 with the problems instead of an answer, and one without the credentials a security requirement asks for gets 401:

$ curl -i http://127.0.0.1:4010/v1/pets/abc
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json

{
  "type": "https://apigenie.pl/mock/errors#invalid-request",
  "title": "The request does not match the specification",
  "status": 422,
  "detail": "path.id: expected type integer, found string",
  ...

validation.request: log in the configuration reports such a request and answers it anyway. The __ query parameters of the next step are removed before the check, so ?__code=404 is not an unknown parameter. Validation and security lists what is checked.

3. Who answers

Several sources can answer a request. From the one that wins to the last resort:

  1. A script. The onRequest hook of a script that returns a response answers before the document’s operations.
  2. The request itself. The Prefer header, or the __ query parameters, ask for a status, a named example, a generated body, a delay or a dropped connection: Prefer: code=404, ?__example=dog.
  3. Scenarios and overrides. The rules of the scenarios that are on (--scenario, the configuration, x-apigenie-mock in the document, the admin API) and the operations overrides of the configuration set the status, an example, a body or headers for the requests they match.
  4. Stubs. With --stubs DIR, a recorded exchange whose method, path, query and body match answers (see Record and replay).
  5. The document. Its examples, else values generated from its schemas.

Each layer sets what it names and leaves the rest to the next. With this scenario on, getPet answers 503:

# apigenie-server.yaml
version: "1.0"
scenarios:
  outage:
    description: the backend is down
    rules:
      - match: {operationId: getPet}
        status: 503
        headers: {Retry-After: "30"}
$ apigenie-server pets.yaml --scenario outage
$ curl -i http://127.0.0.1:4010/v1/pets/1
HTTP/1.1 503 Service Unavailable
Retry-After: 30
X-Apigenie-Mock-Operation: getPet
X-Apigenie-Mock-Source: scenario:outage

A request with Prefer: code=200 still gets a 200 with the cat example, because the request’s own ask comes first; one with ?__example=dog gets the 503, because the scenario set a status that has no content, so there is no example to pick.

4. The status

When nothing above sets the status, the operation’s responses decide: the lowest 2xx response, else 2XX, else the lowest declared status, else default. For getPet that is 200. An operation with 204, 202 and 400 answers 202; one with 2XX alone answers 200; one with 404 and 500 only answers 404.

A status the request asks for has to be declared, by its code, a range such as 4XX, or default: Prefer: code=418 gets a 418 here, because default covers it, and a 500 from an operation without default, whose detail says which statuses the operation has.

5. The body

The body is picked for the status, in this order:

  • The media type. Content negotiation follows the request’s Accept; a request that accepts only text/plain gets 406. Without an Accept, or with */*, the response takes the media type of the request’s body when it has one.
  • An example, else a generated value. In the default static mode the media type’s example, else its first named example (cat here), else the schema’s example answer. What has none is generated from the schema, and --dynamic (or responses.mode: dynamic) generates every body. Generated values respect the types, the formats of the format registry, patterns, lengths, ranges, enum, const, allOf, oneOf, readOnly and writeOnly; x-faker on a property names the generator of its values (Scenarios and extensions).
  • XML. A value for application/xml, text/xml or a +xml media type is written as XML, as the schema’s xml objects describe it (XML bodies).
$ curl http://127.0.0.1:4010/v1/pets/1
{"id":1,"name":"Tom","kind":"cat"}
$ curl 'http://127.0.0.1:4010/v1/pets/1?__example=dog'
{"id":2,"name":"Rex","kind":"dog"}
$ curl -H 'Prefer: dynamic=true' http://127.0.0.1:4010/v1/pets/1
{"id":179,"name":"necessitatibus sint qui q"}

A status without content answers with an empty body, whatever example the request asks for.

6. The headers

The headers the response declares are filled in from their examples or schemas. Two headers of the mock’s own say what was decided, so that a surprising answer can be traced:

Header Says
X-Apigenie-Mock-Operation The operation that answered: getPet.
X-Apigenie-Mock-Source Where the answer came from: example:cat, schema-example, generated, scenario:outage, stub:getPet-1, empty for a status without content, proxy for a real server’s answer.

The log prints the same two facts on each line, and the dashboard shows them per operation. headers.diagnostics: false in the configuration leaves the two headers out.

See also

Was this helpful?