# HTTP tests

> What an HTTP test sends, the status pattern, latency, header and body assertions it can make, and how it follows redirects.

An HTTP test sends a request to your endpoint on a schedule and judges the response
against its assertions. This page lists what the request carries and what each assertion
checks. You set the fields in the test editor (type `HTTP endpoint`), or an agent sets them
through the `create_api_test` MCP tool.

## What the test sends

| Field | What it takes |
|---|---|
| `URL` | An `http://` or `https://` URL. It must be reachable from the public internet. |
| Method | `GET`, `POST`, `PUT`, `PATCH`, `DELETE` or `HEAD`. The editor defaults to `GET`. |
| `Request headers` | Name and value pairs, sent as written, after request variables are filled in. |
| `Request body` | Optional text, sent as UTF-8. |
| `Credential` | `None`, or a saved credential that adds authentication. See [Credentials](/api-testing/credentials/). |

:::caution[A JSON body needs its Content-Type header]
Fixter does not add a `Content-Type` header. A body goes out exactly as written, with the headers
you set and nothing else, so a JSON body needs a `Content-Type: application/json` request header
or most frameworks will not parse it.
:::

Three things every HTTP test shares with the rest of the group, each described once elsewhere:

- Fixter adds a `User-Agent` and a `traceparent` header of its own; your headers replace either on
  a name match, and a credential's headers win over everything. See
  [Allow tests through your firewall](/api-testing/firewalls/#what-to-allow) for the User-Agent,
  [What a run records](/api-testing/runs-and-uptime/#one-trace-id-per-run) for `traceparent`, and
  [Credentials](/api-testing/credentials/) for the precedence.
- The URL, header values and body take [request variables](/api-testing/request-variables/) such
  as `{{uuid}}`; the method, header names and a body validation sample do not.
- A URL that resolves to a private or internal address is refused; see
  [Addresses Fixter will not test](/api-testing/runs-and-uptime/#addresses-fixter-will-not-test).

## Status pattern

`Status pattern` is the only required assertion. It is exactly three characters, each a
digit or the wildcard `x` (or `X`), and the first is a digit from 1 to 5. A wildcard matches
any digit in its position; every other character must equal the status code's digit.

| Pattern | Matches |
|---|---|
| `200` | 200 only |
| `2xx` | Any 2xx status (the editor's default) |
| `20x` | 200 to 209 |

A mismatch fails the run with `Expected <pattern>, got <status>`, for example
`Expected 2xx, got 503`.

The status pattern is also how a test opts out of rate-limit handling. Normally an HTTP 429
ends the run as rate limited rather than failed. If the pattern accepts 429 (for example
`429` or `4xx`), a 429 is an ordinary response checked against the assertions. There is no
other per-test setting for it. See [Runs, states and uptime](/api-testing/runs-and-uptime/).

## Max latency

`Max latency` (in ms) is optional; left empty, speed is not checked. It fails when the
response's total time is over the limit. Total time runs from the first request to the last
byte of the response body, so it includes every redirect hop. The failure reads
`Took <actual> ms, limit <limit> ms`.

`Max latency` is separate from the timeout (`Give up after`, at most 30 seconds). A response
that is slow but complete fails the latency assertion; no complete response within the
timeout ends the attempt as timed out, with
`no complete response from the target within <timeout>`.

## Response headers

`Response headers` are optional checks, each a header name and an expected value:

- The name is case-insensitive: `content-type` checks `Content-Type`.
- The value must equal the header's value exactly, case-sensitive. There is no wildcard or
  substring match, so an expected `application/json` fails against
  `application/json; charset=utf-8`.
- When the response repeats a header, only its first value is compared.

A failure reads `Header <name> was <actual>, expected <expected>`, with `nothing` as the
actual value when the header is absent.

## Body validation

`Body validation` has five tiers, from least to most strict.

| Tier | App label | Needs a sample | Passes when | Example |
|---|---|---|---|---|
| `NONE` | `None` | No | Always; the body is not checked. | Any body passes, empty included. |
| `VALID_JSON` | `Valid JSON` | No | The body parses as JSON. | `{"ok": true}` passes; an HTML error page fails. |
| `JSON_CONTAINS` | `Contains fields` | Yes | Every key in the sample is present, at every depth, with the same JSON type. Extra keys are fine; values are ignored. | Sample `{"id": 1}` passes `{"id": 99, "name": "x"}` and fails `{"id": "99"}`. |
| `JSON_SHAPE` | `Exact fields` | Yes | The same check with the exact key set: an extra key fails. Values are ignored; types are compared. | Sample `{"id": 1}` passes `{"id": 7}` and fails `{"id": 7, "name": "x"}`. |
| `EXACT_MATCH` | `Exact match` | Yes | The body equals the sample. | Sample `{"status": "ok"}` passes `{ "status":"ok" }` and fails `{"status": "degraded"}`. |

A tier that needs a sample refuses a blank one with
`sample must not be blank for tier <TIER>`.

How `JSON_CONTAINS` and `JSON_SHAPE` compare:

- A scalar must have the same JSON type as the sample's value: number, string, boolean or
  null. `{"id": "99"}` against the sample `{"id": 1}` fails as
  `$.id: expected number, got string`.
- An object must meet an object and an array an array. An object sample against an array
  fails with `expected object, got array`.
- An array in the body passes when every element matches the first element of the sample's
  array. An empty sample array accepts any array.

`EXACT_MATCH` compares parsed JSON when both sides are JSON, so key order and whitespace do
not matter. When either side is not JSON, it compares the two texts exactly.

A failed body check reads, by tier:

| Tier | Failure |
|---|---|
| `VALID_JSON` | `Response body is not valid JSON` |
| `JSON_CONTAINS` | `Response body is missing expected fields` (a type mismatch too) |
| `JSON_SHAPE` | `Response body does not match the expected shape` |
| `EXACT_MATCH` | `Response body does not match the sample` |

Every assertion above runs on every response, whatever the body validation tier. A status mismatch
does not skip the latency, header or body checks, and the run lists every failure.

## Redirects

An HTTP test follows redirects, so the assertions judge the final response:

- It follows 301, 302, 303, 307 and 308 responses that carry a `Location`, up to five in a
  row. A sixth fails the run with `too many redirects`.
- It never follows a redirect from https to http. A downgrading or unparseable `Location`
  ends the chain, and that 3xx response is the one the assertions judge. To pass on it,
  accept `3xx` in the status pattern, or point the test at the final https URL.
- A 303 turns any method but `HEAD` into `GET`, and a 301 or 302 turns `POST` into `GET`.
  When the method changes, the body and its `Content-Type` and `Content-Length` headers are
  dropped.
- A redirect to another origin (scheme, host or port) drops the `Authorization`,
  `Proxy-Authorization` and `Cookie` headers.
- The timeout and `Max latency` both cover the whole chain, and every hop must also be a
  public address.

MCP tests follow redirects too, under their own rules; see [MCP tests](/api-testing/mcp-tests/).

## Example

A test that creates an order every run and checks the response's shape. It assumes a
`Bearer token` credential named `Orders API token` already exists.

| Field | Value |
|---|---|
| `Name` | `Create order` |
| `Type` | `HTTP endpoint` |
| Method | `POST` |
| `URL` | `https://api.example.com/v1/orders` |
| `Credential` | `Orders API token` |
| `Request headers` | `Content-Type` = `application/json`; `Idempotency-Key` = `{{uuid}}` |
| `Status pattern` | `2xx` |
| `Max latency` | `800` |
| `Body validation` | `Contains fields` |

`Request body`:

```json
{"sku": "PROBE-1", "quantity": 1, "reference": "{{uuid}}"}
```

`Body sample`:

```json
{"id": "", "status": "", "items": [{"sku": "", "quantity": 0}]}
```

The two `{{uuid}}` values are the same within one attempt, so the idempotency key and the
order's reference match. The sample's values only set types: `""` asks for a string, `0` for
a number.

This response passes, because the extra `total` key is tolerated:

```json
{"id": "ord_123", "status": "created", "items": [{"sku": "PROBE-1", "quantity": 1}], "total": 12.5}
```

A response with `"id": 123` fails with `Response body is missing expected fields`, because
the sample asks for a string. A 201 that took 950 ms fails with
`Took 950 ms, limit 800 ms`.

`Create API test` runs the test once before saving it, and saves it only if that run passes.