apigenietest

test

Runs the requests of .http files and requests generated from an OpenAPI or AsyncAPI document, a GraphQL schema or a .proto file against a live server, checks their assertions and scripts, and validates every request, response and message against the document or the schema with the checks of the validating proxy. HTTP requests are checked against an OpenAPI document, GraphQL requests against a GraphQL schema, GRPC requests against a .proto file, and WebSocket and MQTT requests and the requests of HTTP channels against an AsyncAPI document.

Usage

apigenie test [OPTIONS] FILES...
apigenie test --spec SPEC --generate [--base-url URL] [OPTIONS] [FILES...]
apigenie test --spec schema.graphql [--generate --base-url URL] [OPTIONS] [FILES...]
apigenie test --spec service.proto [--proto-path DIR ...] FILES...
apigenie test --spec ASYNCAPI --generate [--base-url ws://HOST:PORT] [--mqtt-url mqtt://HOST:PORT] [OPTIONS]
apigenie test import COLLECTION -o DIR [--environment FILE ...] [--spec SPEC] [--force]

Description

The request files are written in the httpYac flavour of the .http format, which the JetBrains HTTP client and VS Code’s REST Client read too. Directories are searched for .http and .rest files.

@host = http://localhost:4010/v1

### List pets
# @name list
GET {{host}}/pets?limit=2
Accept: application/json

?? status == 200
?? body $ isArray

### Read the first one
GET {{host}}/pets/{{list.response.body.$[0].id}}

{{
  exports.petName = response.parsedBody.name;
  test("has a name", () => assert.ok(response.parsedBody.name));
}}

### Create a pet
POST {{host}}/pets
Content-Type: application/json

{"id": 7, "name": "{{petName}}"}

?? status == 201
apigenie test --spec openapi.yaml pets.http --report report.json --report-junit report.xml
PASS GET http://localhost:4010/v1/pets?limit=2 -> 200 (9 ms) listPets
PASS GET http://localhost:4010/v1/pets/746 -> 200 (4 ms) showPetById
PASS POST http://localhost:4010/v1/pets -> 201 (2 ms) createPets
Requests: 3 passed, 0 failed, 0 skipped
Contract: 3 requests (0 undocumented), 0 errors, 0 warnings
Coverage: 70.0% of 10 branches, 3 of 3 operations

Requests

A file is split into regions by lines starting with ### (the rest of the line is the region’s title). A region has comments and directives (# or //), variable definitions, the request line (METHOD URL [HTTP/1.1]; a URL alone is a GET), headers up to the first blank line, and a body up to the next region, script or assertion. Lines starting with ? or & right after the request line continue the URL. A body of < ./file.json is read from that file, <@ ./file.json with its variables substituted. Requests run in the order of the file, files in the order given.

http:// and https:// URLs both work. For https:// (and grpcs://, wss:// and mqtts:// alike), the server’s certificate is checked against the system’s root certificates and the URL’s host name. --ca-cert FILE adds authorities, such as a test CA or a company’s root, and --insecure skips the check. SSL_CERT_FILE replaces the system’s roots. A certificate that is rejected fails the request with the reason, for example no response: TLS with 10.0.0.5:443: certificate verification failed: The certificate is not correctly signed by the trusted CA.

A server behind mutual TLS asks for a client certificate: --client-cert client.pem --client-key client.key presents one to every https://, grpcs://, wss:// and mqtts:// server that asks (with --insecure too). The key must be PEM and not encrypted; a file that can’t be read, or a key of another certificate, stops the run with exit status 2. Without one, such a server fails the request, for example no response: TLS with api.example.com:443: the peer refused the connection: the server requires a client certificate. TLS sessions aren’t resumed (every connection does a full handshake), and certificates aren’t checked for revocation (no CRL or OCSP).

Directive Meaning
# @name list Names the request, so that others can refer to its response
# @ref list Runs list first, unless it already ran
# @forceRef list Runs list first, every time
# @operation getPet Checks the request as the operation getPet of --spec (an AsyncAPI document’s operation of an HTTP, WS or MQTT channel); for a GraphQL request, the operation to run (operationName)
# @operation off Doesn’t check the request against --spec, nor count it in the coverage
# @channel pickupDesk WS and MQTT: checks the messages against that channel of the AsyncAPI document
# @timeout 8000 WS and MQTT, GraphQL subscriptions, gRPC client and bidi streams: waits up to 8000 ms for messages (--message-timeout, 5000 by default)
# @expect 2 WS and MQTT, GraphQL subscriptions: stops waiting after 2 messages; 0 doesn’t wait
# @property content-type application/json MQTT 5: a property of the messages the region publishes (see MQTT 5 properties)
# @disabled Skips the request
# @postman Enables the collection-import compatibility API for this file
# @title, # @description, # @tag, # @no-log, # @no-redirect, # @verbose, … Accepted and ignored

Any other directive is a warning: the request still runs.

GraphQL requests

A GRAPHQL request has the GraphQL document as its body and, after a blank line, its variables as a JSON object, as in httpYac and the JetBrains HTTP client. It is sent as a POST of {"query", "variables", "operationName"} with Content-Type: application/json; # @operation gives the operationName. The query can also come from a file (< ./book.graphql), without inline variables then.

# @operation GetBook
GRAPHQL {{host}}/graphql

query GetBook($id: ID!) {
  book(id: $id) {
    title
    author { name }
  }
}

{ "id": "1" }

A POST whose body starts with query, mutation, subscription or fragment is sent the same way, when it has no Content-Type or a JSON or application/graphql one; with another Content-Type (text/plain) it is sent as written. A POST of a JSON body that has a query (or a batch of them) and a GET with a query parameter are GraphQL requests as they are. Without --spec, GraphQL requests are only sent and asserted on.

Subscriptions

A GRAPHQL request (or a POST of a query) whose operation is a subscription goes over WebSocket with the graphql-ws protocol (the graphql-transport-ws subprotocol of Apollo Client, urql and GraphiQL). The URL’s http:// becomes ws:// and https:// becomes wss://; ws:// and wss:// URLs are taken as written. The headers go with the handshake and, as an object, as the payload of connection_init, where servers look for credentials.

The events are the response, as for a WS region:

  • The body is the JSON array of the events ({"data": ...} each).
  • The status is 101.
  • Scripts get response.messages.

The subscription ends when the server completes it, after # @expect events, or when # @timeout (--message-timeout, 5000 ms) has passed; then the client completes it. An error message from the server fails the request with its errors.

# @expect 3
GRAPHQL {{host}}/graphql
Authorization: Bearer {{token}}

subscription OnStory($topic: String!) {
  storyAdded(topic: $topic) {
    id
    title
  }
}

{ "topic": "space" }

?? body $.length == 3
?? body $[0].data.storyAdded.id == 1
PASS GRAPHQL http://localhost:4000/graphql -> 1 sent, 3 received (162 ms) OnStory

A server that doesn’t offer graphql-transport-ws fails the request (no connection: the server doesn't speak graphql-transport-ws (graphql-ws)). The legacy graphql-ws subprotocol of subscriptions-transport-ws and server-sent events aren’t spoken.

Validation

With --spec, each request is bound to an operation by its method and path, as the validating proxy does, then checked: parameters, body, security (credentials must be present), and the response’s status, media type, headers and body. The checks, their severities and --severity are those of contract testing. XML request and response bodies are mapped by the schema’s xml objects and checked like JSON ones (see XML bodies). The coverage counts each operation, its request body and each declared response.

A request sent with an OAuth 2 token is also checked against the operation’s security requirements (or the document’s), as warnings by default:

Check Default Meaning
request.authorization.oauth2-scheme warn The operation has no oauth2 or openIdConnect scheme (or no security at all)
request.authorization.oauth2-scope warn A scope of oauth2_scope isn’t declared for the operation, or no requirement has all its scopes in oauth2_scope

When several OAuth 2 schemes apply, the one whose flow’s tokenUrl is the token endpoint is used. Without oauth2_scope the scopes aren’t checked.

A Swagger 2.0 specification is converted to OpenAPI 3.2 in memory, as apigenie-server does (see Swagger 2.0 documents); no file is written. What the conversion can only approximate is told once before the requests are sent, as SPEC:LINE: info: POINTER: MESSAGE lines on the standard error ({"event":"info",...} with --formatter json), and the report’s operation pointers point into the Swagger file.

With a GraphQL schema as --spec (.graphql, .graphqls or .gql), the GraphQL requests are checked by the GraphQL validating proxy’s checks. The operation is validated against the schema before it is sent: an invalid one (a syntax error, an unknown field or argument, an unknown operationName, variables of the wrong type) fails the request, which isn’t sent, with the errors the server would give (invalid request: Cannot query field "nope" on type "Book".), and the run goes on. The response is checked against the request’s selection set: the shape of data and errors, the type of each value, null in non-null fields that no error’s path explains (null-in-non-null), __typename of abstract types, and errors of operations the schema allows (response.schema-drift). Each event of a subscription is checked the same way, at next[0], next[1], … (next[1]/data/storyAdded/title: Story.title is an Int). Deprecated fields selected are notes (request.field.deprecated). Requests that aren’t GraphQL requests aren’t checked.

An answer with errors next to data is valid GraphQL: the operation ran, but fields failed. Each of those errors is a warning (response.field-error), so a server that answers every field with an error doesn’t pass unnoticed. --fail-on warnings fails such requests, and --severity response.field-error=error makes them errors. The coverage counts the fields of the schema’s types that requests selected, whether or not the answers had them, and each root field; --min-coverage is the share of fields.

PASS GRAPHQL http://localhost:4000/graphql -> 200 (7 ms) GetBook
FAIL GRAPHQL http://localhost:4000/graphql Broken
    invalid request: Cannot query field "nope" on type "Book".
Requests: 1 passed, 1 failed, 0 skipped
Contract: 2 requests (0 to other paths), 1 errors, 0 warnings
Coverage: 40.0% of 15 fields, 1 of 5 root fields
  not tested: Query.books, Query.node, Mutation.addBook, Mutation.deleteBook

With an AsyncAPI 3.x --spec, WS and MQTT requests are checked against the document’s channels. The channel of a WS request is the one served at its URL’s path; that of an MQTT request is the one of the topic it publishes to (or of its first subscription without wildcards), and each message received is matched by its own topic. # @channel names the channel when its address can’t be matched (address: null, wildcard filters), # @operation checks the messages sent as that operation, and # @operation off checks nothing. A path or topic of no channel is a note, and its messages aren’t checked.

The messages sent are the client’s: they are checked against the channel’s receive operations. The messages received are the application’s: they are checked against its send operations or, when they answer a request, against the reply’s messages. When a message sent matched an operation with a reply, the first later message on the reply’s channel that is one of the reply’s messages is its reply:

Check Default Meaning
message.reply.missing warn No reply came while the request waited
message.reply.uncorrelated error The reply’s correlationId (in the payload, or in a header over MQTT 5) isn’t the request’s, or is missing; over MQTT 5, the reply doesn’t repeat the request’s Correlation Data
message.properties.invalid error MQTT 5: a message’s properties don’t match its MQTT bindings (see MQTT 5 properties)

Violations are located in the message: sent[0]/message/carrier, received[1]/message/requestId, sent[0]/message/property.contentType. Over MQTT 5 a message’s user properties are its headers: a correlationId or a reply address in $message.header#/... is read from them, and they are checked against the message’s headers (message.headers.invalid). MQTT 3.1.1 has no message headers: such a correlationId or reply address can’t be checked, and the request gets a note saying so. WS and MQTT requests aren’t checked against an OpenAPI document. The coverage counts each message of each operation, those of HTTP channels included, as the AsyncAPI validating proxy does.

Requests of HTTP channels

HTTP requests are checked against the AsyncAPI document’s HTTP channels (the channels of http and https servers) with the checks of the validating proxy, so both report the same violations:

  • The channel and operation are found by the request’s path (under a server’s pathname or not) and method: a receive operation’s method is its HTTP binding’s (POST by default); a send operation is asked for with GET, as the mock serves it. # @operation names the operation instead, whatever the method; a path that isn’t its channel’s is an error.
  • A receive operation’s request: the body against the operation’s messages (body/...), the query against the HTTP binding’s query schema, and the header fields against the message’s headers and its HTTP binding’s headers (its traits’ too). Its 2XX response: the body and header fields against the reply’s messages (response/...).
  • A send operation’s 2XX response with a body: against the operation’s messages. Its header fields aren’t checked: they are those of the webhook the application sends, which doesn’t pass apigenie test.
  • Responses of other statuses, and the reply’s statusCode binding, aren’t checked.
Check Default Meaning
operation.query.invalid error The query doesn’t match the HTTP binding’s query (query.dryRun)
message.headers.invalid error The header fields the message’s header schemas name don’t match them (header.X-Batch-Size)

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. A path of no HTTP channel, or a method its channel doesn’t take, is a note, and the request isn’t checked.

### A carrier's status batch
POST {{http}}/http/carriers/dhl/events?dryRun=true
Content-Type: application/json

{"events":[{"trackingNumber":"CE1234567890","statusCode":"DLV","occurredAt":"2026-10-08T10:00:00Z"}]}

### The webhooks, asked for as the mock serves send operations
GET {{http}}/http/

WebSocket and MQTT requests

A WS (or WEBSOCKET) region connects to a ws:// or wss:// URL, its headers going with the handshake; an MQTT region connects to an mqtt://host:port broker, or mqtts://host:port (also ssl://) over TLS (MQTT 5, or 3.1.1 when the broker refuses it). Without a port, wss:// uses 443, mqtt:// 1883 and mqtts:// 8883. Over TLS the certificate is checked as for https://: --ca-cert and --insecure apply. The body is the message to send; lines === separate several messages, and === wait-for-server waits for a message of the server before sending the next one, as in httpYac. Then the region waits for the server’s messages until it has # @expect of them or # @timeout (--message-timeout, 5000 ms) has passed. The messages received are the response: its body is their JSON array, each message parsed when it is JSON.

### Request a pickup: the reply carries the request's id
# @expect 1
WS ws://localhost:4010/pickups

{"type":"pickup.request","requestId":"6f1c2b9e-7f8a-4c35-9a51-2d1e8b7f0c11","carrier":"dhl", ...}

?? body $[0].type == pickup.confirmed
?? body $[0].requestId == 6f1c2b9e-7f8a-4c35-9a51-2d1e8b7f0c11

### Report a courier's position
# @expect 0
# @property content-type application/json
# @property payload-format-indicator utf-8
MQTT mqtt://localhost:1883
publish: couriers/cou_4821/position
qos: 1

{"courierId":"cou_4821","at":"2026-10-06T07:41:02Z","position":{"type":"Point","coordinates":[21.0067,52.2319]}}

### Receive a job offer
# @channel courierJobOffers
# @expect 1
MQTT mqtt://localhost:1883
subscribe: couriers/+/offers

?? body $.length == 1
PASS WS ws://localhost:4010/pickups -> 1 sent, 1 received (12 ms) requestPickup
PASS MQTT mqtt://localhost:1883 -> 1 sent, 0 received (3 ms) receiveCourierPositions
PASS MQTT mqtt://localhost:1883 -> 0 sent, 1 received (6 ms) offerPickupJob

The headers of an MQTT region are its settings:

Header Meaning
topic The topic messages are published to, also subscribed to (unless subscribe is given)
publish The topic messages are published to
subscribe The topic filters subscribed to before publishing, comma-separated (couriers/+/offers, hubs/#)
qos 0 (the default), 1 or 2, for publishing and subscribing
retain true to publish retained messages
clientId, keepAlive, clean The connection’s client id (a random one by default), keep-alive in seconds (60) and clean session (true)
username, password The CONNECT’s user name and password; without them, APIGENIE_MQTT_USERNAME and APIGENIE_MQTT_PASSWORD
version auto (the default: MQTT 5, else 3.1.1 when the broker refuses it), 5 or 3.1.1

The client’s own messages coming back on a topic it subscribed to are left out. Retained messages of a subscription count as received.

Settings are substituted like any header, so a password can stay out of the file: password: {{$processEnv BROKER_PASSWORD}}, or a variable of an environment file. A setting the region doesn’t give comes from APIGENIE_MQTT_USERNAME or APIGENIE_MQTT_PASSWORD. MQTT 5 sends a password without a user name; MQTT 3.1.1 sends one only with a user name, so with version: 3.1.1 a password alone is a warning and isn’t sent. A broker that refuses them fails the request with the broker refused the connection (reason code 0x86) (bad user name or password) (MQTT 5), or (return code 4) (bad user name or password) (MQTT 3.1.1). Credentials never show in the output or the reports: wherever they would, they read [redacted], and a URL’s user:password@ is shown as ***@. The user name and password of the URL itself aren’t sent.

In checks and scripts Value
?? body $.length == 2 How many messages came (.length also gives the size of any array or string)
?? body $[0].type == pickup.confirmed A value of the first message
?? status == 101 The handshake’s status (WS); the CONNACK’s return code, 0 (MQTT)
response.messages[i] In scripts: topic (the topic, or the WebSocket’s path), body (parsed when JSON), rawBody, binary

MQTT 5 properties

An MQTT region connects with MQTT 5. A broker that refuses it (reason code 0x84, a 3.1.1 broker’s return code 1, or a connection closed before the CONNACK) gets a second connection with MQTT 3.1.1, and the request notes the broker refused MQTT 5. version: 5 doesn’t fall back; version: 3.1.1 doesn’t try MQTT 5.

# @property gives the properties of every message the region publishes:

Directive Property
# @property content-type application/json Content Type
# @property response-topic clients/{{client}}/quotes Response Topic (no wildcards); the region also subscribes to it
# @property correlation-data {{$uuid}} Correlation Data
# @property payload-format-indicator utf-8 Payload Format Indicator: utf-8 or 1, bytes or 0
# @property message-expiry-interval 60 Message Expiry Interval, in seconds
# @property user replyTo dispatch/d-1/replies A user property (repeat the line for more; the value may be empty)

Values are substituted like headers. An unknown property, a missing value or a bad value is a warning, and that property isn’t sent. Over MQTT 3.1.1 none is sent, and the request notes it.

Over MQTT 5 the messages sent and received are checked with their properties, against the message’s and the operation’s MQTT bindings (0.2.0), as message.properties.invalid (error):

Binding Checked
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

A request whose reply address is in a header ($message.header#/replyTo) is answered on the topic in that user property; one whose reply channel has address: null is answered on its Response Topic. Either way the region waits for the reply on that topic, checks it as the reply, and compares its correlation id (header or payload) and, when a binding declares correlationData, its Correlation Data with the request’s.

### Ask for a quote: the reply comes to the Response Topic
# @expect 1
# @property content-type application/json
# @property response-topic clients/7/quotes
# @property correlation-data {{$uuid}}
# @property user correlationId c-42
MQTT mqtt://localhost:1883
publish: quotes
qos: 1

{"parcel":"P1"}

?? body $[0].price exists

A message whose bindings state properties counts in the coverage as checked (MQTT 5) or unchecked (MQTT 3.1.1): summary.properties and operations[].properties in the report, MQTT 5 properties: checked on 2 messages, not on 0 (MQTT 3.1.1) in the summary.

Kafka requests

A KAFKA region produces records to a Kafka cluster and reads what comes on its topics (ADR-170). The URL names a broker, kafka://host:port (port 9092 by default) or kafkas://host:port (9093, over TLS, verified as https:// with --ca-cert, --insecure and --client-cert); several bootstrap servers are separated by commas. The cluster needs Kafka 2.3 or later. A cluster that creates topics on first use creates the region’s topic, as Kafka’s producers ask it to.

### Place an order: the confirmation carries its correlation id
# @expect 1
# @timeout 10000
# @property correlationId {{$uuid}}
KAFKA kafka://localhost:9092
topic: shop.orders
subscribe: shop.confirmations
key: cust-1

{"sku": "A-1"}

?? body $[0].accepted == true

### Await a shipment the application produces
# @channel shipments
# @expect 1
KAFKA kafka://localhost:9092
subscribe: shop.shipments
Header Meaning
topic The topic the records are produced to; also read, unless subscribe is given
subscribe The topics read, separated by commas
key The records’ key
partition The records’ partition; without it, the key’s (murmur2, as Kafka’s producers choose), else the next in turn
username, password SASL PLAIN credentials; without them APIGENIE_KAFKA_USERNAME and APIGENIE_KAFKA_PASSWORD. They are [redacted] in the output and reports

# @property NAME VALUE lines are the records’ headers, the name as written (# @property user NAME VALUE too). The body’s parts (===) are the records; === wait-for-server waits for a record before the next part. Before the first record is produced, the region takes the latest offsets of the topics it reads, so a reply can’t come too early; then it reads until it has # @expect records or # @timeout has passed. Its own records are left out. The records read are the response, as an MQTT region’s messages are.

With an AsyncAPI --spec, what is produced is checked against the receive operations of the topic’s channel and what is read against its send operations, or as the reply to a request: the value, the headers against the message’s headers, the key against the message’s Kafka binding key (message.key.invalid) and the partition against the channel’s Kafka binding partitions (message.partition.invalid). A channel’s topic is its Kafka binding’s topic, else its address. A reply is paired with its request by the correlation id in a header ($message.header#/...) or the payload, and a reply address in a header is followed, as for MQTT 5. Compressed records aren’t read; they are reported as errors.

AMQP requests

An AMQP region publishes to an AMQP 0-9-1 broker and reads what comes to its queues (ADR-171). The URL is amqp://[user:password@]host[:port][/vhost] (port 5672, the virtual host percent-encoded: /%2Fcarpet-express), or amqps:// (5671, over TLS, verified as https://).

### File a declaration: the decision comes to the decisions queue
# @expect 1
# @property delivery-mode 2
# @property correlationId {{$uuid}}
AMQP amqp://localhost:5672/%2Fcarpet-express
exchange: customs
routing-key: customs.GB.filing
subscribe: customs.decisions

{"destinationCountry": "GB"}

### Await what the application publishes to the customs exchange
# @expect 1
AMQP amqp://localhost:5672/%2Fcarpet-express
bind: customs customs.*.filing
Header Meaning
exchange The exchange the messages go to; without it, the default exchange (routing keys are queue names)
routing-key The messages’ routing key
subscribe Queues consumed, separated by commas; a queue’s messages go to the region, not to the application
bind EXCHANGE KEY: a queue of the region’s own, bound to the exchange with that binding key (none for fanout); can be repeated
mandatory false: a message no queue takes isn’t an error (true by default)
username, password PLAIN credentials; without them the URL’s, then APIGENIE_AMQP_USERNAME and APIGENIE_AMQP_PASSWORD, then guest. They are [redacted] in the output and reports

# @property NAME VALUE lines with the name of a basic property set it: content-type, content-encoding, delivery-mode, priority, correlation-id, reply-to, expiration (milliseconds), message-id, timestamp (seconds since the epoch, or now), type, user-id and app-id. Other names, and # @property user NAME VALUE, are application headers. The region consumes its queues before it publishes, then reads until it has # @expect messages or # @timeout has passed; its own messages coming back are left out. RabbitMQ’s direct reply-to works: subscribe: amq.rabbitmq.reply-to with # @property reply-to amq.rabbitmq.reply-to.

With an AsyncAPI --spec, the channel is the one of the exchange and routing key (a queue channel’s for the default exchange and its queue’s name), and a message read from a queue is that queue’s channel’s. What is published is checked against the channel’s receive operations and what is read against its send operations or as a reply: the body, the headers against the message’s headers, and the properties against the AMQP bindings (message.properties.invalid). A reply is paired with its request by its correlation id in a header or a property: properties are seen by their AMQP and AsyncAPI names (correlation_id and correlationId, reply_to and replyTo), so $message.header#/correlationId finds either.

gRPC requests

With a .proto file as --spec, GRPC requests call the methods of its services, as httpYac writes them:

@host = localhost:50051

### say hello
GRPC {{host}}/acme.greeter.v1.Greeter/SayHello
x-tenant: acme

{"name": "Ann"}

?? status == 0
?? body message == Hello Ann
?? header grpc-status == 0
apigenie test --spec greeter.proto greeter.http
PASS GRPC localhost:50051/acme.greeter.v1.Greeter/SayHello -> OK (6 ms) acme.greeter.v1.Greeter/SayHello
Requests: 1 passed, 0 failed, 0 skipped
Contract: 1 calls (0 undocumented), 0 errors, 0 warnings
Coverage: 33.3% of 3 methods
  not tested: acme.greeter.v1.Greeter/SayHelloToAll, acme.greeter.v1.Greeter/Ping
  • The request line is GRPC HOST:PORT/package.Service/Method, or a URL with grpc://, or with grpcs:// for TLS. The method must be one of the file’s: another is a failed request.
  • Headers are the call’s metadata (names in lower case); -bin values are sent as written, base64-encoded.
  • The body is the request message in its proto3 JSON form (field names or their JSON names, 64-bit integers as numbers or strings, enum names, base64 bytes, the well-known types in their own forms); no body is {}. A body that isn’t a valid message fails the request before anything is sent, naming the field: the body isn't a valid acme.greeter.v1.HelloRequest: name: expected a string.
  • The .proto file is --spec, with its imports looked up in --proto-path directories, then next to the file, then among the standard files (google/protobuf/*, google/api/*, google/rpc/*), as apigenie-server --proto-path does. httpYac’s proto < ./file.proto line is read and ignored with a warning.
  • Assertions and scripts see the call as a response: status is the gRPC status code (0 for OK, 5 for NOT_FOUND) and the status text its name, the headers are the response metadata and the trailers (grpc-status, grpc-message among them), and the body is the response message as JSON. A server-streaming or bidi-streaming call’s body is a JSON array of its messages.
  • Every kind of method. Client- and bidi-streaming methods take a stream of messages, written as for WS regions: lines === separate them, and === wait-for-server waits for a message of the server before the next one is sent (bidi methods only). After the last message the call is half-closed, and its end (the trailers) is awaited for # @timeout (--message-timeout, 5000 ms); a call that doesn’t end in time is cancelled and fails. Unary and server-streaming methods take one message: more fail the request.
  • HTTP/2, cleartext with prior knowledge (h2c), or over TLS with ALPN h2 for grpcs:// (and https://) URLs, verified as https:// requests are (--ca-cert, --insecure). --timeout bounds the connection and each wait for the server, and is sent as grpc-timeout.
### a client stream: the messages, then one answer
GRPC {{host}}/acme.chat.v1.Chat/Upload

{"author": "Ann", "text": "hi"}
===
{"author": "Bob", "text": "hello"}

?? body lines == 2

### a bidi stream that waits for each answer before it goes on
GRPC {{host}}/acme.chat.v1.Chat/Talk

{"author": "Ann", "text": "hi"}
=== wait-for-server
{"author": "Ann", "text": "bye"}

?? body $.length == 2
?? body $[1].text == re: bye

Each call is checked with the checks of gRPC contract testing: the request and response messages against their types (fields marked [(google.api.field_behavior) = REQUIRED] included), each message of a stream at message[0], message[1], …, the status and its google.rpc details, the number of messages of a unary or client-streaming call. The coverage counts the file’s methods. Without a .proto file as --spec, GRPC requests are skipped; HTTP requests in the same files are sent but not checked.

Environments and variables

{{name}} is replaced by the variable name. Variables come from, the later winning:

  1. the environment files of the request file’s directory or, when it has none, the working directory: http-client.env.json (its $shared object, then the object of --env), http-client.private.env.json (the same), .env and .env.NAME;
  2. --env-file files (JSON like http-client.env.json, or KEY=VALUE lines);
  3. @name = value lines outside of requests;
  4. what scripts set (exports, client.global.set);
  5. @name = value lines of the request’s region;
  6. --var NAME=VALUE.

Responses of named requests can be used in the requests after them:

Reference Value
{{list.response.status}} The status
{{list.response.headers.Location}} A header
{{list.response.body.$[0].id}}, {{list.response.body./0/id}} A value of the JSON body (a path or a JSON pointer)
{{list.response.body}} The body
{{list.id}} httpYac’s form: a value of the JSON body

Built-in variables: {{$uuid}} (and $guid, $random.uuid), {{$timestamp}}, {{$isoTimestamp}}, {{$datetime iso8601}}, {{$datetime rfc1123}}, {{$randomInt MIN MAX}}, {{$processEnv NAME}} and {{$dotenv NAME}}. A variable that isn’t defined is a warning and stays as it is. \{{ is a literal {{.

Authorization: Basic user:password (or Basic user password) is sent base64-encoded, as httpYac and the JetBrains client do.

OAuth 2

Authorization: oauth2 [GRANT] [PREFIX] gets a token from the token endpoint and sends Authorization: Bearer TOKEN instead, with httpYac’s syntax and variables:

GET {{host}}/pets
Authorization: oauth2 client_credentials
Grant Sent to the token endpoint
client_credentials (when no grant is written) The client’s credentials
password PREFIX_username and PREFIX_password too
refresh_token PREFIX_refreshToken

The token request is configured by variables named after the prefix (oauth2 when no prefix is written; Authorization: oauth2 password keycloak reads keycloak_tokenEndpoint and so on). They come from environment files, @ variables and --var, like any other variable:

Variable Meaning
oauth2_tokenEndpoint The token endpoint’s URL (required)
oauth2_clientId The client’s id (required)
oauth2_clientSecret The client’s secret; none for a public client
oauth2_scope Space-separated scopes
oauth2_audience, oauth2_resource Sent as audience and resource when set
oauth2_username, oauth2_password The password grant’s user
oauth2_refreshToken The refresh_token grant’s refresh token
oauth2_useAuthorizationHeader true (the default): the client’s id and secret go in a Basic Authorization header; false: in the form
oauth2_issuer openid only: the token endpoint is read from ISSUER/.well-known/openid-configuration when oauth2_tokenEndpoint isn’t set

Authorization: openid ... is the same, with the OpenID Connect discovery of oauth2_issuer. The token is requested once per run for each configuration (the endpoint, the client, the scopes, the user) and requested again, with its refresh token when it has one, shortly before its expires_in runs out. The token endpoint is reached like any request: https:// is verified as --ca-cert and --insecure say. A token endpoint that refuses fails the request, which isn’t sent:

FAIL GET http://localhost:8080/pets
    no OAuth 2 token: the OAuth 2 token endpoint http://localhost:8080/token answered 401 (invalid_client: unknown client)

The authorization_code (or code), implicit and device_code grants need a browser or a person, and token exchange isn’t supported: those requests are skipped with a warning. Tokens, refresh tokens, client secrets, passwords and the credentials of Authorization headers are shown as [redacted] wherever they would appear: in URLs, errors, assertion messages, script logs, warnings and the reports.

Assertions

A line ?? SUBJECT [ARGUMENT] PREDICATE [VALUE] checks the response:

Subject Value
status The status
header NAME The header’s value
body The body; body PATH a value of the JSON body ($.items[0].id, items[0].id or /items/0/id)
duration The time to the response, in milliseconds
js EXPRESSION The value of a JavaScript expression with the scripts’ API

Predicates: ==, !=, >, >=, <, <= (numbers when both sides are numbers), startsWith, endsWith, includes (or contains), matches (a regular expression, /.../flags too), exists, isArray, isBoolean, isNumber, isString, isTrue and isFalse. A js subject without a predicate must be true. Variables are substituted in the argument and the value.

Scripts

{{ and }} on lines of their own enclose an httpYac script: before the request line it runs before the request is sent, after it once the response came. JetBrains handlers work too: < {% ... %} before the request line, > {% ... %} after it. Scripts run in a sandboxed JavaScript engine (QuickJS) with no file system or network access, five seconds each. Both flavours get the same API:

Name What it is
response.status, response.statusCode The status
response.headers Headers by lower-case name; headers.valueOf(name), headers.valuesOf(name)
response.body The body, parsed when it is JSON; response.parsedBody, response.rawBody
response.contentType.mimeType, .charset The media type
response.duration Milliseconds
request.method, request.url, request.headers, request.body The request as written
request.variables.set(name, value), .get(name) Variables of this request (pre-request scripts)
exports.name = value, client.global.set(name, value) Variables of the following requests
client.global.get(name), request.environment.get(name) A variable
test(name, fn), client.test(name, fn) A named check: it fails when fn throws (or its promise rejects)
test.status(code), test.header(name, value), test.headerContains(name, text), test.hasResponseBody(), test.totalTime(ms) httpYac’s shortcuts
assert(value, message), client.assert(...), assert.equal, assert.strictEqual, assert.deepStrictEqual, assert.notEqual, assert.fail Throws when the check fails
console.log, console.warn, …, client.log Lines of the request’s log (--verbose, the reports)

Variables are also local names of scripts when they are identifiers. A script that throws fails its request. Changes an ordinary pre-request script makes to request are not sent: set variables instead. Imported files opt into the header-addition API described below.

Importing collections

apigenie test import reads a Postman Collection Format v2.0 or v2.1 JSON export and writes request files that apigenie test can run. It uses an independently implemented reader, not Postman’s JavaScript or the postman-collection SDK. Collection v1 is not supported: export as v2.1 from Postman.

apigenie test import carpet-express.collection.json -o requests \
  --environment mock.environment.json --spec carpet-express.openapi.yaml
apigenie test requests --env mock --spec carpet-express.openapi.yaml \
  --var baseUrl=http://localhost:4010/v2
Import option Meaning
COLLECTION Collection JSON file to import
-o DIR, --output DIR Required output directory
--environment FILE An exported environment JSON file; can be repeated
--spec SPEC Bind matching requests to operation IDs and write saved response examples as an OpenAPI overlay
--force Replace existing generated files; never delete unrelated files

The collection’s root requests go into <collection-slug>.http. A folder Shipping becomes shipping.http; its nested folder Tracking becomes shipping/tracking.http. Every request is a ### region with a stable # @name slug. Collection and inherited folder variables are written as @name = value defaults at the top of each file. Slug collisions get deterministic suffixes. Collection and folder auth and pre-request/test scripts are inherited by descendant requests; explicit noauth disables inherited authentication.

URLs and variables retain {{name}} references. Active headers and query parameters, raw, urlencoded, multipart form-data, GraphQL and file bodies are mapped. GraphQL becomes an HTTP POST with a JSON envelope, not the unsupported GRAPHQL pseudo-method and not GraphQL-specific contract validation. Available upload files are copied into the output with relative < ./path references; missing files are kept as references and reported as warnings. Relative source upload paths are resolved against the collection file’s directory. Multipart file references are read as binary data when the request runs.

Exported environments become named entries in http-client.env.json. Bearer, basic and API-key authentication becomes headers or query parameters with environment-variable references. Literal credentials and known credential defaults are stored only in the generated http-client.private.env.json (with the same $shared and environment entries), not in http-client.env.json, request files, overlays or the import summary; apigenie test reads both files. The private environment file contains secrets: protect it and do not commit it to source control. The importer cannot discover arbitrary secrets hidden in unrelated application data.

OAuth 2 auth with the client credentials or password grant becomes Authorization: oauth2 client_credentials (or password) and the OAuth 2 variables: the access token URL, client id, scope, audience and client authentication (header or body) in http-client.env.json, the client secret, user name and password in http-client.private.env.json. Each distinct OAuth 2 configuration gets its own prefix (oauth2, oauth2_2, …). A token stored in the collection isn’t used: a new one is requested. The authorization code, PKCE and implicit grants need a browser: those requests are written disabled, their auth kept as a comment without its secrets, with a warning. Adding the token to the query and header prefixes other than Bearer are warnings; the token is sent in the Authorization header with the type the token endpoint gives.

Imported files follow the ordinary .http variable precedence: file defaults override environment values, and --var overrides both. This differs from Postman’s precedence. For marked files, environment discovery searches parent directories too, so nested request files can find the generated root environment. Script changes are in memory only, not saved back to the environment file. Files are run in the existing explicit-file or sorted-directory order; splitting folders into separate files does not preserve arbitrary cross-folder collection order.

With --spec, requests that bind by method and path to an operation with an ID get # @operation <operationId>. Saved responses become additive examples in <collection-slug>.overlay.json; the specification is never edited. Apply the overlay explicitly:

apigenie overlay carpet-express.openapi.yaml \
  --overlay requests/carpet-express.overlay.json -o carpet-express.with-examples.yaml

Without a specification or a usable binding, examples are preserved as comments. Unresolved bindings or overlay targets produce warnings rather than guessed operation IDs.

Imported script API

Files carrying # @postman get a small compatibility shim in QuickJS. Unmarked files do not get a pm global. Pre-request blocks go before the request; test blocks go after it. Scripts containing their own standalone }} line are JSON-escaped inside the block so that multiline strings and JavaScript tokens remain intact. Simple pm.test callbacks that only assert a literal response status become ?? status == N instead, so they do not require scripting support.

API Supported subset
pm.test(name, fn) Records a named check; a thrown assertion fails it
pm.expect(value) to.equal, to.eql, to.be.true, to.be.false, to.have.property, to.have.status, to.include, to.be.oneOf, to.be.a
pm.response code, status, json(), text(), case-insensitive headers.get(name), to.have.status(code)
pm.environment get, set, unset
pm.collectionVariables get, set
pm.variables get, set; request-local changes
pm.globals get, set
pm.request.headers.add({key, value}) Adds a header before sending a marked request

This is not the full SDK or Chai. pm.sendRequest, postman.setNextRequest, pm.cookies and pm.visualizer cause the whole affected script block to be kept as comments, with warnings that name the call and its source line. OAuth 2 auth with a grant that needs a browser is preserved as comments and the request is disabled. The summary lists unsupported features; they are never silently dropped. Other unknown script APIs fail explicitly at runtime. Scripts remain subject to the existing QuickJS time limit and lack of filesystem/network access. Imported https:// URLs are sent over TLS like any other request (see --ca-cert and --insecure).

Import returns 0 after a successful conversion, including one with reported warnings, 2 for bad command usage, and 3 for input or output failures. Existing generated filenames are refused unless --force is supplied. A directory literally named import can still be run with apigenie test -- import.

Generated requests

--generate sends one request per operation of --spec, checked as that operation: path parameters, required query, header and cookie parameters, and a body of the first media type (JSON first), each from the document’s examples or generated from its schemas (readOnly properties left out). An XML media type gets an XML body, written as the schema’s xml objects describe it. The URL is --base-url (or the document’s first server) followed by the operation’s path. Request files and --generate can be combined.

With an AsyncAPI document, --generate sends one region per WebSocket or MQTT channel, bound with # @channel (and # @operation when the channel has one receive operation): WebSocket channels at --base-url (ws://host:port) followed by the channel’s address, MQTT channels at --mqtt-url, publishing to the address and subscribing to it when the application sends there. Parameters of the address take their default, first enum value or example. The messages are those of the channel’s receive operations, from their examples or generated from their schemas; MQTT regions get the # @property lines of the first one’s bindings and headers (correlation data and response topics generated from their schemas, user properties from the first example’s headers or generated); # @expect is the number of replies, plus one when the application sends on the channel. HTTP channels get one request per operation, bound with # @operation, at --base-url (a ws:// URL is taken as http://) followed by the channel’s address: a receive operation’s with its HTTP binding’s method and its first message (an example, else generated) as the body, a send operation’s a GET. Channels without an address are left out with a warning, as are the channels of a protocol whose URL wasn’t given.

Kafka channels get one KAFKA region per operation at --kafka-url, bound with # @operation. A receive operation’s region produces its first message (an example, else generated) to the channel’s topic, with a key made from the message’s Kafka binding and # @property headers from its headers; it reads the reply’s topic for one record (# @expect 1) when the operation has a reply, and ends once the record is produced otherwise. A send operation’s region only reads the topic, for one record the application produces.

AMQP channels get one AMQP region per operation at --amqp-url, bound with # @operation. A receive operation’s region publishes its first message to the channel’s exchange and routing key (a queue channel’s queue through the default exchange) with the properties its bindings say and its headers; it reads the reply’s channel, its queue or a queue bound to its exchange, when the operation has a reply. A send operation’s region reads the channel for one message the application publishes.

With a .proto file, --generate sends one GRPC request per method of its services (the built-in reflection and health services aside) to --base-url, which is then required and is HOST:PORT, grpc://HOST:PORT or grpcs://HOST:PORT (TLS). The message is generated from the input type as the mock generates answers ({} for google.protobuf.Empty): REQUIRED fields set, OUTPUT_ONLY fields left out. Client- and bidi-streaming methods get a stream of two generated messages.

With a GraphQL schema, --generate sends one GRAPHQL request per field of Query, Mutation and Subscription (deprecated fields left out), named after the field (Query.book) and bound to its operation (QueryBook). A subscription goes over graphql-ws with # @expect 1: the first event is enough. Required arguments get values made up from their types as apigenie-server makes up values (the scalar’s or the argument’s name: a UUID for ID, an email for email, a date for DateTime, an enum value that isn’t deprecated, an input object with its required fields); optional ones are left out. The selection set has the leaf fields without required arguments, objects two levels down, and __typename with a fragment per type of an interface or a union. --base-url is required: it is the endpoint, with /graphql added when it has no path.

mutation MutationAddBook {
  addBook(input: {title: "Twenty Years After", genre: POETRY}) {
    id
    title
    genre
    pages
    author {
      id
      name
      email
    }
  }
}

Reports

--report writes the contract report of apigenie-server with "report": "apigenie-test", plus tests (the counts and the run’s gate) and requests (each request’s file, line, URL, status, duration, operation, outcome, failures, assertions, violations and log; grpc and grpcStatus for GRPC requests). With a .proto file the contract members are those of the gRPC report ("specification": "grpc", methods, services), with a GraphQL schema those of the GraphQL report ("specification": "graphql", rootFields, fields, uncoveredFields, operations). Without --spec, only report, formatVersion, version, startedAt, endedAt, tests and requests are written. formatVersion is the version of the format, which changes only when a member is removed or changes meaning (see Compatibility); version is its former name, kept for one release. The JSON Schema of the report is apigenie-test-report.json. --report-junit writes a requests suite (a test case per request: failed, or skipped for disabled requests) and, with --spec, the contract suite of apigenie-server. With an AsyncAPI document, the report is the AsyncAPI proxy’s ("specification": "asyncapi", channels, operations with their messages, violations); WS and MQTT requests also have channel, address, messagesSent, messagesReceived and notes (what wasn’t checked, and why), and the coverage --min-coverage checks is the share of the operations’ messages seen.

Options

Option Description
FILES .http and .rest files, or directories to search for them
--spec SPEC The OpenAPI 3.0, 3.1 or 3.2 (or Swagger 2.0) document HTTP requests are validated against, the GraphQL schema (.graphql, .graphqls, .gql) of GraphQL requests, the .proto file of GRPC requests, or the AsyncAPI 3.x document WS, MQTT, KAFKA and AMQP requests and the requests of its HTTP channels are validated against
--proto-path DIR A directory in which the imports of a .proto --spec are looked up; can be repeated
--generate Also send one request per operation (OpenAPI), root field (GraphQL), method (.proto) or channel (AsyncAPI; one per operation of an HTTP, Kafka or AMQP channel) of --spec
--base-url URL The server’s URL for --generate (the endpoint, required, for a GraphQL schema; HOST:PORT for a .proto file; ws:// or http:// for an AsyncAPI document’s WebSocket and HTTP channels), also put before request URLs that start with /
--mqtt-url URL The MQTT broker (mqtt://host:port) of an AsyncAPI document’s channels for --generate
--amqp-url URL The AMQP broker (amqp://host:port/vhost, amqps:// over TLS) of an AsyncAPI document’s AMQP channels for --generate
--kafka-url URL A broker of the Kafka cluster (kafka://host:port, kafkas:// over TLS) of an AsyncAPI document’s Kafka channels for --generate
--env NAME The environment of the environment files
--env-file FILE An environment file to read after the others; can be repeated
--var NAME=VALUE A variable above every other one; can be repeated
--header NAME:VALUE A header for every request that doesn’t have it; can be repeated
--timeout MS How long to wait for a connection and for a response (30000)
--message-timeout MS How long WS, MQTT, KAFKA and AMQP requests, GraphQL subscriptions and gRPC client and bidi streams wait for messages (5000); # @timeout overrides it
--bail Stop at the first failed request
--ca-cert FILE A PEM file of certificate authorities trusted for https://, grpcs://, wss://, mqtts://, kafkas:// and amqps:// URLs, besides the system’s; can be repeated
--insecure Don’t verify the certificates of https://, grpcs://, wss://, mqtts://, kafkas:// and amqps:// servers; the traffic stays encrypted
--client-cert FILE A PEM client certificate (chain) presented to https://, grpcs://, wss://, mqtts://, kafkas:// and amqps:// servers that ask for one (mTLS); needs --client-key
--client-key FILE The unencrypted PEM private key of --client-cert
--report FILE Write the JSON report
--report-junit FILE Write the JUnit XML report
--fail-on none|errors|warnings The contract violations that fail the run (errors)
--min-coverage PERCENT Fail the run below this coverage of --spec (a GraphQL schema: the share of its fields; a .proto file: the share of its methods; an AsyncAPI document: the share of its operations’ messages)
--severity CHECK=LEVEL Change a contract check’s severity: error, warn, info or off (ignore); can be repeated. The checks are those of the OpenAPI, gRPC and AsyncAPI proxies or, with a GraphQL schema, of the GraphQL proxy; an unknown check is a bad option
--seed N Seed of the values --generate makes up
--formatter stylish|json A line per request and a summary (stylish), or a JSON object per line
--quiet Show only failed requests and the summary
--verbose Also show the warnings and notes of the contract checks, and the scripts’ log

–fail-on errors|warnings|none

A failed request (an assertion, a script’s test or error, no response, an unknown @ref) always fails the run. --fail-on says which contract violations do too: errors (the default), warnings (errors and warnings) or none. A request with such violations is shown as FAIL.

–formatter json

Each request is a line {"event":"request",...} with the members of the JSON report’s requests; warnings are {"event":"warning","file":...,"line":...,"message":...}, notes about a Swagger 2.0 specification {"event":"info",...} with the same members; the last line is {"event":"summary","formatVersion":1,"total":...,"passed":...,"failed":...,"skipped":...,"gate":{...}} with the contract’s errors, warnings and coverage when there is a --spec. Its formatVersion versions the whole stream, as formatVersion versions the report.

httpYac compatibility

Supported

Feature Notes
### regions with titles, # and // comments
GRPC requests Every method of the .proto file of --spec (streams of messages separated by ===), over HTTP/2 (cleartext, or TLS for grpcs://); proto < lines are ignored with a warning
Request line with or without method and HTTP version, multi-line query (?, &)
Headers, inline bodies, < file and <@ file bodies <@utf8 file: the encoding is ignored
@name = value variables Outside of requests (file) or in a request’s region
{{name}}, {{$uuid}}, {{$timestamp}}, {{$datetime}}, {{$randomInt}}, {{$processEnv}}, {{$dotenv}} See Environments and variables
# @name, # @ref, # @forceRef, # @disabled Named responses are kept per file
{{ }} scripts before and after the request exports, test, assert, console, request, response
JetBrains < {% %} and > {% %} handlers client.global, client.test, client.assert, client.log, request.variables
?? assertions status, header, body, duration and js subjects
WS and WEBSOCKET requests ws:// and wss://; headers go with the handshake; text messages
MQTT requests mqtt:// and mqtts://, MQTT 3.1.1; topic, publish, subscribe, qos, retain, clientId, keepAlive, clean, username and password headers
=== and === wait-for-server between messages
.env, .env.NAME, http-client.env.json, http-client.private.env.json $shared and the --env environment
Authorization: Basic user:password Encoded before sending
Authorization: oauth2 and openid with the client_credentials, password and refresh_token grants See OAuth 2; refresh_token and OpenID Connect discovery are apigenie’s additions
GRAPHQL requests, POST requests of a query Variables after a blank line; # @operation is the operationName; subscriptions over graphql-ws

Not supported yet

Feature What happens
Server-sent events (SSE), AMQP Not request lines
gql fragment imports (# @import ./fragments.gql) A warning; the request runs without them
GraphQL subscriptions over the legacy graphql-ws subprotocol or SSE The request fails: only graphql-transport-ws is spoken
MQTT 5, client certificates MQTT is 3.1.1; no certificate of the client is sent
# @keepStreaming, binary WebSocket messages sent A warning; messages are sent as text
Redirects Not followed: a 3xx response is checked as it is
OAuth 2 and OpenID authorization_code, implicit and device_code grants, token exchange, oauth2_keepAlive, oauth2_interceptRequest The request is skipped with a warning (the variables are ignored)
AWS and digest authorization The request is skipped with a warning
# @import, # @loop, # @sleep, # @jwt, # @keepStreaming, other directives A warning; the request runs
xpath assertions, sha256/md5 predicates A warning; the assertion is ignored
Offsets of $datetime and $timestamp, $localDatetime time zones A warning; the current time in UTC is used
{{ expression }} JavaScript inside a line, global {{ }} scripts outside of requests Not evaluated (the latter with a warning)
require(), npm packages, client.exit() in scripts The script fails
Response handler files (> ./handler.js) A warning
Changes a pre-request script makes to request Not sent for ordinary files; marked imports support pm.request.headers.add
.httpyac.js, .httpyac.json, httpyac.config.js Not read
Cookie jars No cookies are kept between requests

Exit status

Code Meaning
0 Every request passed and the contract gate held
2 Bad options: no request files without --generate, --generate without --spec, or with a GraphQL schema or a .proto file but no --base-url, --min-coverage without --spec, an unknown --severity check; a request file, an environment file or the specification (an OpenAPI or AsyncAPI document, a GraphQL schema, or a .proto file with its imports) that can’t be read or parsed; --generate without a URL it can use (no --base-url and no absolute server URL in the document, or a URL of the wrong scheme); a report that can’t be written, or an unexpected error
5 A request failed, or contract violations (--fail-on) or the coverage (--min-coverage) failed the run

See also

  • apigenie-server, whose validating proxy checks traffic something else drives, with the same checks and reports
  • lint, which checks the document itself