TestingAPIs is still being built. If something breaks, email support@testingapis.com.

Reading a response

Status codes, response headers and the body.

Every request gets exactly one response (unless the connection breaks, which is its own kind of answer and comes up in a later lesson). A response has three parts: a status line, headers and a body. New testers tend to look only at the body, because that is where the data is. Most of the bugs that slip past them are in the other two parts.

What a response looks like

Like the request, an HTTP/1.1 response is plain text. This is the response to GET /api/health, as curl shows it with the -i option (which prints the headers along with the body). A few infrastructure headers are removed.

Raw response
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
x-powered-by: TestingAPIs

{"status":"healthy","uptimeSeconds":3542,"timestamp":"2026-09-30T18:28:12Z"}

The first line is the status line: protocol version, a three-digit status code and a short reason phrase. Then come headers, one per line, then an empty line, then the body. The reason phrase is only for humans. Clients and tests should look at the number. HTTP/2 drops the reason phrase entirely, so a test that checks for the text "OK" can break when the server upgrades.

Status code classes

The first digit of the status code tells you the class of the response. You can often judge a result from that digit alone.

ClassMeaningWho is at fault
1xxInformational. The request is still in progress.Nobody. You rarely see these directly.
2xxSuccess. The server did what you asked.Nobody.
3xxRedirection. Look somewhere else.Nobody. The client has to follow up.
4xxClient error. The request was wrong.The client: bad input, no permission, wrong URL.
5xxServer error. The request may have been fine.The server or something behind it.

That last column is the reason status codes matter so much in testing. A 4xx says "fix your request". A 5xx says "something broke on our side". If you send garbage and get a 500, that is a bug even though the request was bad, because the server crashed where it should have rejected the input politely with a 400. Finding those 500s is one of the most productive things a new tester can do.

The codes you will see most

CodeNameTypical use
200OKThe request worked and the body has the result.
201CreatedA POST created a new record.
204No ContentIt worked and there is deliberately no body. Common after DELETE.
301 / 302Moved Permanently / FoundThe resource is at the URL in the Location header.
304Not ModifiedYour cached copy is still current. Covered with caching.
400Bad RequestThe request is malformed or a value is invalid.
401UnauthorizedNo credentials, or wrong ones. Despite the name, it means unauthenticated.
403ForbiddenCredentials are fine, but this user may not do this.
404Not FoundNothing at this URL, or no record with this ID.
405Method Not AllowedThe URL exists but not with this method.
409ConflictThe request clashes with the current state, such as a duplicate email.
422Unprocessable ContentThe syntax is fine but the data breaks a rule.
429Too Many RequestsYou hit a rate limit. Slow down.
500Internal Server ErrorAn unhandled error on the server.
502Bad GatewayA proxy got a bad answer from the server behind it.
503Service UnavailableThe server is overloaded or down for maintenance.

APIs disagree about some of these. One team returns 400 for every validation error and another uses 422. Some return 200 for a delete with a message in the body, others 204 with nothing. Neither is wrong. What you check is that the API matches its own documentation and is consistent from one endpoint to the next.

Trying status codes

/api/status/{code} answers with whatever code you put in the path. It is useful for seeing how a tool or a client library behaves when it gets a particular status. Try a few:

GET/api/status/201
GET/api/status/503

From curl, the 503 looks like this:

Response
HTTP/1.1 503 Service Unavailable
Content-Type: application/json; charset=utf-8
retry-after: 5

{
  "status": 503,
  "statusText": "Service Unavailable",
  "category": "server-error",
  "message": "This is a 503 response. Use /api/status/{code} to request any status."
}

Notice the retry-after: 5 header. A 503 or 429 often tells the client how many seconds to wait before trying again, and a well behaved client respects it. You check that the header is there, not only the status.

Codes outside the valid range are rejected. /api/status/999 comes back as a 400 with the message Status code must be between 200 and 599 (1xx cannot be sent as a final response). And /api/status/abc returns a 404 with an empty body, because abcdoesn't match the route at all. Two bad inputs, two different kinds of failure. You will see this pattern often: a value of the wrong type tends to fail in the routing layer, before the application code ever runs.

Redirects

A 3xx response usually has no useful body. The important part is the Location header, which says where to go next:

Response to GET /api/status/302
HTTP/1.1 302 Found
Content-Type: application/json; charset=utf-8
location: /api/echo

Browsers and most HTTP libraries follow redirects automatically and show you only the final response. That is convenient until you need to test the redirect itself. curl doesn't follow them unless you add -L, which makes it the better tool for this.

Response headers

Response headers describe the body and give the client instructions. Ask for page 3 of the product list, two products per page:

GET/api/Products?limit=2&page=3
Response headers
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
x-page: 3
x-per-page: 2
x-total-count: 20
x-total-pages: 10

Content-Type says the body is JSON in UTF-8. The four x-page and x-total headers carry the pagination details: there are 20 products, 10 pages, and this is page 3. The body is only an array of two products, so a client that ignores these headers has no way of knowing there is more. Headers that start with X- are custom ones the API invented. They are documented by the API, not by the HTTP standard.

Some headers hold times as Unix timestamps: the number of seconds since 1 January 1970, UTC. Rate-limited APIs often send one in x-ratelimit-reset to say when your allowance resets. Convert it before trusting it: a reset time in the past, or years away, is a bug you only catch by reading headers instead of skipping them. (The timestamp converter does the conversion.)

You may expect a Content-Length header. This API mostly sends Transfer-Encoding: chunkedinstead, which means the body is sent in pieces and the total size isn't announced up front. Both are valid. A response has one or the other, not both.

The body

The body is the data: a product, a list, an error message, an image. Content-Type says how to read it. Most of your checks will be on the body, and Reading JSON covers that in detail. Two special cases come first.

No body at all

A 204 means success with no body, on purpose. The response has no Content-Type either, because there is nothing to describe:

GET/api/formats/no-content
This request uses an endpoint for signed-in members.Sign inCreate a free account
Response
HTTP/1.1 204 No Content
x-powered-by: TestingAPIs

Client code that always calls response.json()will crash on this, because an empty string isn't valid JSON. If you write test helpers that parse every response, they need to handle an empty body.

When the headers lie

Status, headers and body are produced by different parts of the server, and nothing forces them to agree. Here is a response that claims to be HTML:

GET/api/formats/wrong-content-type
This request uses an endpoint for signed-in members.Sign inCreate a free account
Response
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8

{"id":1,"name":"Alice Johnson","email":"alice@example.com","roles":["admin","editor"],"active":true,"score":98.5,"manager":null}

The body is perfectly good JSON. A person reading it would never notice a problem. A browser would try to render it as a web page, and some HTTP libraries will refuse to parse it as JSON or return it as a string. This endpoint is broken on purpose. In real systems the same bug usually comes from an error page generated by a proxy or a framework default. Your test should check the Content-Type explicitly.

The mirror image happens too: a 200 status with a body that says {"success": false}. Some APIs report every error that way. Monitoring tools that count 5xx responses never see those failures, and neither does a test that checks only the status code. If an API does this, your tests have to read the body on every response.

What to check on every response

Before you look at a single field of data, confirm the basics. It takes a few seconds and catches a surprising number of bugs.

  • The status code is the one the docs promise for this situation.
  • Content-Type matches the body you actually got.
  • Headers the docs mention (pagination, Location, Retry-After) are present and make sense.
  • The body parses, and has the shape you expect.
  • Error responses follow the same format as other errors from this API.

Exercise

For each request, write down the status code, the Content-Type and one sentence about the body.

  • /api/status/418. Look up why this code exists.
  • /api/formats/emptysign in to see. How does it differ from /api/formats/no-contentsign in to see? One returns 200 and one returns 204. Which do you think a client handles better?
  • /api/Products?limit=2&page=99. There is no page 99. What status and body did you get, and what do the pagination headers say? Would you have designed it the same way?
  • /api/status/list. Find a code in the list marked "supported": false and try requesting it.

Practise and go further