Skip to main content

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
Prerequisites

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.

TopicSee
Encryption of the channel and certificate validationTLS and certificates
Cookie attributes and scopesCookies
The browser's origin boundary and CORSOrigins and CORS
The design of retries (count, interval, idempotency key)Retries and idempotency
Whether and how long to cacheHTTP caching and CDNs
URL design and versioning, and defining routes in a frameworkLaravel API Development Guide — Routing and the first endpoint
The design of response output formatLaravel 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}
PartContent
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"}
PartContent
Line 1 (status line)Protocol version + status code + reason phrase
Line 2+Headers
After the blank lineBody

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​

MethodWhat it doesSafeIdempotent
GETFetch a resource○○
HEADSame as GET but without the body○○
POSTSend data. State changes and side effects allowed××
PUTReplace the resource with the sent content×○
DELETEDelete the resource×○
PATCHPartially modify the resource××
OPTIONSAsk about communication options○○
CONNECTEstablish a tunnel××
TRACELoop-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.

IntentSQLHTTP
CreateINSERTPOST
ReadSELECTGET
Full updateUPDATEPUT
Partial updateUPDATEPATCH
DeleteDELETEDELETE

Choosing between PUT, PATCH, and POST​

All three change state, but they differ in what the body means.

MethodWhat you sendWhat the server does
PUTThe whole thing, as it should end upReplaces the resource with exactly what you sent
PATCHInstructions for what to changeApplies the instructions you sent to the current content
POSTSomething you want processedReceives 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.

  1. PUT if you can write that URL's content as the whole desired state
  2. PATCH if you can write just the part you want changed, at the same URL
  3. POST if 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.

ClassMeaningRepresentative
1xxInformational101 Switching Protocols
2xxSuccess200 OK / 201 Created / 204 No Content
3xxRedirection301 Moved Permanently / 302 Found / 304 Not Modified
4xxClient-side error400 / 401 / 403 / 404 / 409 / 422 / 429
5xxServer-side error500 / 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 UnauthorizedAuthentication is missing. Who they are is unknown
403 ForbiddenAuthenticated, but authorization doesn't allow it. Who they are is known but they lack the right
404 Not FoundDoesn't exist. Sometimes used in place of 403 to hide existence
400 Bad RequestRequest format is invalid
422 Unprocessable ContentFormat is valid but the content violates a business rule
409 ConflictConflicts with current state (duplicate registration, version mismatch)
301 / 302Moved elsewhere. When received in response to a POST, some implementations switch the method to GET when following
307 / 308Also redirects, but used when the method and body must be preserved

Headers​

Headers carry "everything besides the body". Common ones grouped by role:

RoleExample
Content descriptionContent-Type / Content-Length / Content-Encoding
AuthenticationAuthorization / WWW-Authenticate
CachingCache-Control / ETag / Last-Modified
NegotiationAccept / Accept-Language / Accept-Encoding
State keepingCookie / Set-Cookie
Cross-origin controlOrigin / Access-Control-Allow-Origin / Access-Control-Allow-Headers
SecurityContent-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​

DebutedTransportMain changes
HTTP/1.11997TCPConnection reuse (keep-alive). One request at a time per connection
HTTP/22015TCPMultiplexing. Multiple exchanges concurrent on one connection. Header compression. From here the wire format is binary frames, not text
HTTP/32022UDP (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:

IntentSQLHTTP
Full replaceUPDATEPUT
Change partUPDATEPATCH
DeleteDELETEDELETE

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.

SituationCodeMeaning
No token, expired, or invalid401Unknown who — please authenticate
Authenticated but insufficient rights403Known 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. UPDATE and REMOVE are not among them
  • PUT sends the whole thing as it should end up, PATCH the parts you want changed, POST something you want processed. If the desired state can't be written as a value, use POST
  • Safe = no state change; idempotent = same final state regardless of send count. Safe implies idempotent, not vice versa
  • DELETE isn't safe, but it is idempotent
  • Status: 4xx is client-side, 5xx is server-side. 401 is authentication, 403 is 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
Related references