HTTP — methods, statuses, and headers
HTTP/1.1 is a simple text-based protocol. Write "what you want to do" on the first line, put related information in headers, and follow with a body. Grasp this structure and both API design and debugging become clearer.
What you'll learn in this chapter
- The structure of requests and responses, and where the method appears
- The nine standard methods, and the two properties of safety and idempotency
- Choosing between
PUT/PATCH/POST, and handling requests whose state can't be written as a value - The five categories of status codes, and how to choose one
- What changed from HTTP/1.1 through HTTP/3
This chapter builds on the layered model of TCP/IP. HTTP is an application-layer protocol.
What this chapter does not cover
Topics adjacent to HTTP are covered by their own chapters and guides.
| Topic | See |
|---|---|
| Encryption of the channel and certificate validation | TLS and certificates |
| Cookie attributes and scopes | Cookies |
| The browser's origin boundary and CORS | Origins and CORS |
| The design of retries (count, interval, idempotency key) | Retries and idempotency |
| Whether and how long to cache | HTTP caching and CDNs |
| URL design and versioning, and defining routes in a framework | Laravel API Development Guide — Routing and the first endpoint |
| The design of response output format | Laravel API Development Guide — API resources |
Message structure
An HTTP exchange consists of two kinds of messages: request and response.
Request
POST /api/orders HTTP/1.1
Host: example.com
Content-Type: application/json
Authorization: Bearer eyJhbGci...
{"itemId": 42, "quantity": 2}
| Part | Content |
|---|---|
| Line 1 (request line) | Method + path + protocol version |
| Line 2+ (headers) | Related information. Continues until a blank line |
| After the blank line (body) | Data to send. Absent for some methods |
The method appears only on the request line. It isn't specified in a header, and it isn't placed in the body.
Response
HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/orders/1001
{"id": 1001, "status": "accepted"}
| Part | Content |
|---|---|
| Line 1 (status line) | Protocol version + status code + reason phrase |
| Line 2+ | Headers |
| After the blank line | Body |
A response has no method in it. What a response carries is a status code. "What was done" is request-side information; the responder returns "what happened".
Nine standard methods
| Method | What it does | Safe | Idempotent |
|---|---|---|---|
GET | Fetch a resource | ○ | ○ |
HEAD | Same as GET but without the body | ○ | ○ |
POST | Send data. State changes and side effects allowed | × | × |
PUT | Replace the resource with the sent content | × | ○ |
DELETE | Delete the resource | × | ○ |
PATCH | Partially modify the resource | × | × |
OPTIONS | Ask about communication options | ○ | ○ |
CONNECT | Establish a tunnel | × | × |
TRACE | Loop-back test on the path | ○ | ○ |
UPDATE and REMOVE are not among the nine above. Use PUT or PATCH for updates and DELETE for deletions. It's a common confusion coming from SQL's UPDATE / DELETE and CRUD vocabulary; the mapping is worth pinning down.
| Intent | SQL | HTTP |
|---|---|---|
| Create | INSERT | POST |
| Read | SELECT | GET |
| Full update | UPDATE | PUT |
| Partial update | UPDATE | PATCH |
| Delete | DELETE | DELETE |
Choosing between PUT, PATCH, and POST
All three change state, but they differ in what the body means.
| Method | What you send | What the server does |
|---|---|---|
PUT | The whole thing, as it should end up | Replaces the resource with exactly what you sent |
PATCH | Instructions for what to change | Applies the instructions you sent to the current content |
POST | Something you want processed | Receives it and processes it. What that means is up to the server |
PUT and PATCH assume a shape where the URL points at one "thing" and you can send its content. /users/1 points at a single user, so PUT /users/1 means "make this user look like this" and PATCH /users/1 means "change just this part of this user". That shape works when the state you want can be written as a value: set the name to "Tanaka", swap the email, raise the published flag. Each is a "this is how it should be" you can put into a value.
Sometimes what you want to send is an action, not a state. Writing "subtract 500 from the balance" as a PUT means the client reads the current balance and sends the value after subtracting. If another update lands between the read and the send, that update is overwritten and lost. Send the act of subtracting itself and the read and the arithmetic happen server-side in one go. POST is what carries that. Neither what you send nor what the server does with it is specified, so it can express a request for an action.
You can put a diff like {"delta": -500} into a PATCH. The PATCH RFC doesn't specify the diff format — it states that no single default format is required of implementations — so this is not a spec violation on its own. But JSON Patch, the format in wide use, has six operations (add, remove, replace, move, copy, test) and no arithmetic. How delta is read becomes a convention private to that API, and nobody reading it can tell what will happen.
The order to choose in. It assumes there is a URL for the "thing" you want to update; when that doesn't hold, you fall through to 3.
PUTif you can write that URL's content as the whole desired statePATCHif you can write just the part you want changed, at the same URLPOSTif the "thing" doesn't exist yet (creation), or you want to request an action rather than a state
Creation is POST because of the first half of 3. Before it exists there is no URL for it yet, so you send "please make this" to a collection URL such as /orders. The server decides what the URL will be and returns it in the Location header.
Safety and idempotency
These look similar but are separate properties.
Safe — does not change server state. Read-only.
Idempotent — sending the same request many times leaves the state the same as sending it once.
safe ⊂ idempotent
Safe methods are always idempotent. If they change nothing, doing them n times leaves state unchanged. The converse doesn't hold.
DELETE is the clearest example. It changes state so it isn't safe, but it is idempotent. Deleting the same resource twice leaves the final state as "the resource doesn't exist", unchanged. A second call may return 404, but "the response is the same" is a different concern from "state is idempotent".
POST is not idempotent because each send creates a new resource. Two calls to an order API create two orders. That's why the double-tap on a submit button matters, and one countermeasure is an idempotency key (the same key is processed only once).
The PUT / PATCH split shows this too. PUT is "replace with this content", so any number of calls converges to the same state and it's idempotent. PATCH is "apply these instructions", so depending on them it may not be idempotent (the "decrement by 1" kind of diff from the earlier section).
This property matters when deciding on retries. If communication is cut mid-flight and the response is unclear, an idempotent method can be safely retried; a non-idempotent one risks duplicate processing. How many times and at what interval to retry is covered by Retries and idempotency.
Status codes
The leading digit divides them into five classes.
| Class | Meaning | Representative |
|---|---|---|
1xx | Informational | 101 Switching Protocols |
2xx | Success | 200 OK / 201 Created / 204 No Content |
3xx | Redirection | 301 Moved Permanently / 302 Found / 304 Not Modified |
4xx | Client-side error | 400 / 401 / 403 / 404 / 409 / 422 / 429 |
5xx | Server-side error | 500 / 502 / 503 |
The 4xx / 5xx boundary matters in practice. If the request content is at fault it's 4xx; if the server caused it it's 5xx. Getting this wrong misfires monitoring alerts. Returning 500 for validation errors turns ordinary input mistakes into server-fault notifications.
A quick disambiguation:
| When to use | |
|---|---|
401 Unauthorized | Authentication is missing. Who they are is unknown |
403 Forbidden | Authenticated, but authorization doesn't allow it. Who they are is known but they lack the right |
404 Not Found | Doesn't exist. Sometimes used in place of 403 to hide existence |
400 Bad Request | Request format is invalid |
422 Unprocessable Content | Format is valid but the content violates a business rule |
409 Conflict | Conflicts with current state (duplicate registration, version mismatch) |
301 / 302 | Moved elsewhere. When received in response to a POST, some implementations switch the method to GET when following |
307 / 308 | Also redirects, but used when the method and body must be preserved |
Headers
Headers carry "everything besides the body". Common ones grouped by role:
| Role | Example |
|---|---|
| Content description | Content-Type / Content-Length / Content-Encoding |
| Authentication | Authorization / WWW-Authenticate |
| Caching | Cache-Control / ETag / Last-Modified |
| Negotiation | Accept / Accept-Language / Accept-Encoding |
| State keeping | Cookie / Set-Cookie |
| Cross-origin control | Origin / Access-Control-Allow-Origin / Access-Control-Allow-Headers |
| Security | Content-Security-Policy / Strict-Transport-Security |
Content-Type carries not only the body format but which character encoding to read it as. Sender and receiver diverging here produces mojibake (Character encoding).
HTTP is a stateless protocol. The server doesn't remember state between requests. To keep a login, place a cookie or a token on every request, and match on the server. "Don't hold state" is the principle; if state is needed, carry it explicitly.
Differences between versions
| Debuted | Transport | Main changes | |
|---|---|---|---|
| HTTP/1.1 | 1997 | TCP | Connection reuse (keep-alive). One request at a time per connection |
| HTTP/2 | 2015 | TCP | Multiplexing. Multiple exchanges concurrent on one connection. Header compression. From here the wire format is binary frames, not text |
| HTTP/3 | 2022 | UDP (QUIC) | Drop TCP to shorten connection setup. Localize the impact of packet loss |
Connection setup is short because QUIC folds the TLS 1.3 handshake into its own connection setup. Setting up TCP and setting up a secure connection don't become separate round trips; they fit in one.
In HTTP/1.1, one connection couldn't send the next request until the previous response returned (head-of-line blocking). To work around this, browsers opened several connections to the same host.
HTTP/2 dissolved that by letting one connection carry many streams — but at the TCP layer, one lost packet still stalls everything behind it. HTTP/3 moved to UDP to avoid this TCP-derived stall.
The meanings of methods and status codes don't change across versions. Only the transport changed.
Common misconceptions
"It's fine to change state as a side effect of GET." — GET is defined as safe. Designing "fetch the list and mark them read" lets crawlers and prefetches change state without asking. Put side effects on POST.
"POST is only for creating." — It's used for creation often, but that isn't specified. It's a general-purpose method that need not be idempotent and can also represent operations that can't be written as the content of the "thing" the URL points at. "Decrement stock by 1" as POST is because of this property.
"DELETE is not idempotent." — It is. Even if the second call returns 404, the resulting state is unchanged. Idempotency is a property of state, not of whether the response is identical.
"Safe means idempotent, and idempotent means safe." — Safe implies idempotent; the converse fails. PUT and DELETE are idempotent but not safe.
Exercises
Q1. In an internal API design review, someone proposes "use UPDATE for updates and REMOVE for deletes". How do you push back?
Answer and explanation
Answer: Neither is among the standard nine. Use PUT or PATCH for update and DELETE for delete.
The standard methods are these nine:
GET / HEAD / POST / PUT / DELETE / PATCH / OPTIONS / CONNECT / TRACE
UPDATE and REMOVE mix in because of SQL and CRUD vocabulary. The mapping:
| Intent | SQL | HTTP |
|---|---|---|
| Full replace | UPDATE | PUT |
| Change part | UPDATE | PATCH |
| Delete | DELETE | DELETE |
Custom method names aren't parsed by proxies, caches, or API clients. HTTP methods are shared vocabulary, so adding your own means intermediaries can't handle them.
If you have an operation that doesn't fit any method, using POST with the operation expressed in the path or body is the pragmatic landing point.
Q2. You are building an API "decrement stock by 1". Which of POST, PUT, or PATCH is appropriate?
Answer and explanation
Answer: POST
Judge by idempotency. "Decrement by 1" changes the result every time, so it isn't idempotent. Assigning an idempotent method (PUT / DELETE) to a non-idempotent operation lets clients and proxies think "safe to retry" and double-decrement.
PATCH is also non-idempotent and thus a candidate, but JSON Patch, the format in wide use, has no arithmetic operation (add, remove, replace, move, copy, test). You would be inventing your own reading of the diff, so requesting an action with POST communicates better.
To make it idempotent, change the design to send the resulting value.
PUT /api/items/42/stock HTTP/1.1
Content-Type: application/json
{"quantity": 9}
Any number of calls converges to 9. Idempotent. But this opens concurrent-update overwrites from other clients, so you need version-based conflict detection separately.
Q3. A user hits a management-panel API without being logged in. What status code should you return? What if they are logged in but lack permission?
Answer and explanation
Answer: Not logged in → 401 Unauthorized. Insufficient permission → 403 Forbidden.
The axis is authentication vs. authorization.
| Situation | Code | Meaning |
|---|---|---|
| No token, expired, or invalid | 401 | Unknown who — please authenticate |
| Authenticated but insufficient rights | 403 | Known who, but not allowed — reauthenticating won't help |
401 reads as "authentication might get you through"; 403 reads as "reauthenticating won't". Client behavior differs too — 401 prompts a login flow, 403 shows an error. The background of this distinction is in Authentication and authorization.
Some designs return 404 in place of 403 to hide the existence of a resource, because saying "not allowed" reveals that the resource is there.
Summary
- HTTP messages have a request line (or status line) + headers + body
- The method appears only on the request line. Not in headers or body, and not in the response
- Nine standard methods.
UPDATEandREMOVEare not among them PUTsends the whole thing as it should end up,PATCHthe parts you want changed,POSTsomething you want processed. If the desired state can't be written as a value, usePOST- Safe = no state change; idempotent = same final state regardless of send count. Safe implies idempotent, not vice versa
DELETEisn't safe, but it is idempotent- Status:
4xxis client-side,5xxis server-side.401is authentication,403is authorization - HTTP is stateless. State is carried explicitly by cookies or tokens
- What changed across versions is the transport, not the meaning of methods or status codes
- Laravel API Development Guide — Routing and the first endpoint — Choosing methods and status codes in API design
- MDN — HTTP request methods
- RFC 9110 — HTTP Semantics — Definition of methods and the rules of safety and idempotency
- RFC 5789 — PATCH Method for HTTP —
PATCHalone is defined in its own RFC. It does not specify the diff format - RFC 6902 — JavaScript Object Notation (JSON) Patch — the diff format in wide use. Six operations, no arithmetic
What to read next
- Authentication and authorization — On to Part 6. Digging into the thinking behind
401vs.403 - Cookies — Carrying state on a stateless protocol
- TLS and certificates — The layer that encrypts this channel