---
title: 'HTTP Methods and What They Mean'
source: 'https://academia.sh/en/courses/application-protocols/http-methods-and-what-they-mean'
course: 'Application Layer Protocols'
language: en
updated: '2026-08-17T18:06:59+00:00'
license: 'CC BY-SA 4.0'
---

# HTTP Methods and What They Mean

A method is a declaration: the intermediary in between draws the repeatable and storable decisions from it, and nothing verifies the declaration.

The Network Models and Protocols course closed out the transport layer with a single
promise left in hand: TCP delivers an ordered, lossless, non-duplicated byte stream. A
gap sits inside that promise. The transport layer carries **bytes**; it does not know
what a byte means. The same forty bytes could be a measurement record or a delete
command; TCP does not tell the two apart, nor take on that job.

Meaning is built by the **application layer**, and the thing that builds it is
nothing other than what is written in the message: a method name, a path, a header
field. This is exactly what gets counted throughout this course — how many decisions
what is written in the message lets a third party reading it make. This lesson first
sets up that third party and the measure, then measures the first field: the method.

## The Intermediary: The Layer That Does Not Know the Application

The North Slope measurement station serves its readings to a client. Between the two
stands an **intermediary**: a layer that caches, routes, and retries when needed. A
**proxy** is one kind of intermediary; the general name is intermediary.

The intermediary does not know the application: not what was measured, what a record
means, or which session belongs to whom. All it knows is what is written in the
message. The whole subject of this course fits in one sentence: **whatever is written
in the message, the intermediary knows; whatever is not written, it does not know;
and whatever is written wrong, it knows wrong.**

The request an intermediary sees looks like this. The dump below is for teaching purposes
and is not run:

```text
POST /kayit HTTP/1.1
Host: station.example
Content-Type: application/json
Content-Length: 74
```

On the response side there are two more markers the intermediary reads. The response's
**first line** is next lesson's subject; only header fields are read here:

```text
Cache-Control: private
ETag: "s-0417"
```

These four lines correspond to the following fields in the model:

| Where in the dump | Field in the model | What the intermediary derives |
|---|---|---|
| First word of the request line | `method` | `repeatable`, `storable` |
| Second word of the request line | `path` | the resource's identity |
| `Host` | `host` | `redirectable` |
| `Cache-Control: private` | `private_marker` | `shareable`, `storable` |
| `ETag` | `validator` | `fresh` |

## A Method Is a Declaration

A method declares two separate properties. If a method is **safe**, sending the request
is not meant to change state on the server; if **idempotent**, sending it once or five
times in a row leaves the same result.

| Method | Safe | Idempotent | What the intermediary derives |
|---|---|---|---|
| GET | yes | yes | response storable, request repeatable |
| HEAD | yes | yes | response storable, request repeatable |
| PUT | no | yes | response not storable, request repeatable |
| DELETE | no | yes | response not storable, request repeatable |
| POST | no | no | neither |

Safety implies idempotence: a request that does not change state does not change it on
a repeat either. The reverse does not hold; PUT changes state, but repeating it with
the same body leaves the same state.

The resource design topic of the Web API Design course treats this distinction as
**the designer's commitment** and says which method should be chosen. Here the choice
is already made; what is measured is what that choice **tells the intermediary**. The
idempotence key is established in the same course too, as an application contract;
this lesson measures the protocol's own declaration, not what the application adds
on top.

## The Measure: Decisions Taken Without an Extra Round Trip

A protocol's number is not the fields it carries but **how many decisions it lets the
receiving side make without an extra round trip.** A decision that cannot be made is
a round-trip debt: the intermediary must ask what it cannot know, and asking costs a
round trip.

The model produces forty exchanges, each asking the intermediary to make five
decisions.

| Decision | Question |
|---|---|
| `storable` | May this response be cached |
| `shareable` | May the cached copy be given to another client |
| `fresh` | Is the copy still valid |
| `repeatable` | May the request be resent if it goes unanswered |
| `redirectable` | Can the message be routed to its destination |

Forty exchanges and five decisions make a set of two hundred decisions. This set also
sets the measurement's **resolution**: the smallest difference measurable here is
$1/200 = 0.0050$, none smaller is claimed.

The truth itself is represented by the **oracle**: because we produced the setup,
whether each exchange is actually personalized and whether the copy actually went
stale is known. The intermediary's decision is counted against this truth as
correct, wrong, or unavailable.

Assumptions of the measurement:

- **HA1** — Forty exchanges are generated from a single seed; method and path are drawn
  from fixed lists, and the measurement gives the same set every run.
- **HA2** — The oracle's truth is known because we produced the setup: whether
  the response is personalized and whether the copy went stale are set at generation
  time, not read from the message.
- **HA3** — A single regime is measured in this lesson: the message is **open** and all
  five fields are visible to the intermediary. Regimes where a field is hidden are the
  subject of later lessons.
- **HA4** — The intermediary does not know the application and builds every decision only
  from a field present in the message. If a field is not in the message, the decision is
  never made — not a wrong decision, an **unavailable** one.
- **HA5** — The private flag is the server's own declaration, and the server sometimes
  skips setting it on a personalized response; the validator tag also has a resolution
  and sometimes misses a change. **Nothing verifies what is written in the message.**
- **HA6** — The byte count is computed from field name, value and separator length; the
  measurement is not a network measurement, it is a count of the message's content.

## The Core

The setup, the oracle and the intermediary are built in the core below. The `OPEN` tuple
gives the fields the intermediary can read: in this lesson the message is open, and all
five fields are visible.

```python
SEED = 20260809
SAFE = {"GET", "HEAD"}
IDEMPOTENT = {"GET", "HEAD", "PUT", "DELETE"}
METHOD_POOL = ["GET", "GET", "GET", "HEAD", "POST", "PUT", "DELETE"]
PATH_POOL = ["/olcum/kuzey", "/olcum/yamac", "/ozet", "/kayit", "/oturum"]
DECISIONS = ("storable", "shareable", "fresh", "repeatable",
             "redirectable")
OPEN = ("method", "path", "host", "private_marker", "validator")


def generator(seed: int):
    d = seed % 2147483646 + 1

    def r(n: int) -> int:
        nonlocal d
        d = (d * 48271) % 2147483647
        return d % n
    return r


def exchanges(count: int = 40, seed: int = SEED) -> list[dict]:
    r, out = generator(seed), []
    for i in range(count):
        method, path = METHOD_POOL[r(7)], PATH_POOL[r(5)]
        private = path == "/oturum" or r(5) == 0
        stale = r(3) == 0
        out.append({
            "no": i + 1, "method": method, "path": path,
            "private": private,                     # truth: is the response personalized
            "stale": stale,                         # truth: has the copy gone stale
            "private_marker": private and r(4) != 0,       # declaration: did the server set the flag
            "tag_missed": stale and r(6) == 0,     # validator tag missed the change
        })
    return out


def oracle(a: dict) -> dict:
    return {
        "storable": a["method"] in SAFE and not a["private"],
        "shareable": not a["private"],
        "fresh": not a["stale"],
        "repeatable": a["method"] in IDEMPOTENT,
        "redirectable": True,
    }


def message(a: dict, visible: tuple) -> dict:
    full = {"method": a["method"], "path": a["path"], "host": "station.example",
            "private_marker": a["private_marker"],
            "validator": a["stale"] and not a["tag_missed"]}
    return {k: v for k, v in full.items() if k in visible}


def byte_count(i: dict) -> int:
    return sum(len(k) + len(str(v)) + 4 for k, v in i.items())


def infer(i: dict) -> dict:
    k = {}
    if "method" in i:
        k["repeatable"] = i["method"] in IDEMPOTENT
    if "private_marker" in i:
        k["shareable"] = not i["private_marker"]
        if "method" in i:
            k["storable"] = i["method"] in SAFE and not i["private_marker"]
    if "validator" in i:
        k["fresh"] = not i["validator"]
    if "host" in i:
        k["redirectable"] = True
    return k
```

The structure of the `infer` function deserves attention: every decision sits behind an
`in` check. If a field is not in the message, the decision never enters the dictionary
— the intermediary does not make a wrong decision, it **cannot make one.** This
distinction is what gets counted for the rest of the course.

## Measuring the Open Regime

The measurement runs right after the core above. In each exchange the oracle's truth is
compared against the intermediary's decision; if a decision could not be made,
that exchange owes one round trip.

```python
batch = exchanges()
counter = {k: [0, 0, 0] for k in DECISIONS}    # correct, wrong, unavailable
extra_round = total_bytes = 0
for x in batch:
    truth, given = oracle(x), infer(message(x, OPEN))
    missing = False
    for decision in DECISIONS:
        if decision not in given:
            counter[decision][2] += 1
            missing = True
        elif given[decision] == truth[decision]:
            counter[decision][0] += 1
        else:
            counter[decision][1] += 1
    extra_round += 1 if missing else 0
    total_bytes += byte_count(message(x, OPEN))

print(f"exchanges {len(batch)}, decisions {len(batch) * len(DECISIONS)}, "
      f"declaring idempotent {sum(x['method'] in IDEMPOTENT for x in batch)}, "
      f"not declaring idempotent {sum(x['method'] not in IDEMPOTENT for x in batch)}, "
      f"declaring safe {sum(x['method'] in SAFE for x in batch)}")
print(f"personalized {sum(x['private'] for x in batch)}, "
      f"left unflagged {sum(x['private'] and not x['private_marker'] for x in batch)}, "
      f"stale {sum(x['stale'] for x in batch)}")
print(f"open regime: correct {sum(v[0] for v in counter.values())}, "
      f"wrong {sum(v[1] for v in counter.values())}, "
      f"unavailable {sum(v[2] for v in counter.values())}, "
      f"extra round {extra_round}, bytes {total_bytes}")
print()
print(f"{'decision':<18s} {'correct':>7s} {'wrong':>7s} {'unavailable':>12s}")
for decision, (c, w, u) in counter.items():
    print(f"{decision:<18s} {c:7d} {w:7d} {u:12d}")
```

```
exchanges 40, decisions 200, declaring idempotent 31, not declaring idempotent 9, declaring safe 18
personalized 17, left unflagged 2, stale 12
open regime: correct 195, wrong 5, unavailable 0, extra round 0, bytes 3724

decision           correct   wrong  unavailable
storable                40       0            0
shareable               38       2            0
fresh                   37       3            0
repeatable              40       0            0
redirectable            40       0            0
```

The first line to read is **unavailable 0** and **extra round 0**. While the message is
open, the intermediary gets two hundred of two hundred decisions and never goes
back to the server even once. That is what the three thousand seven hundred and
twenty-four bytes buy.

The second line is the method's share. **Thirty-one** of the forty exchanges declare an
idempotent method, **eighteen** declare a safe one. `repeatable` is correct in all
forty decisions, because the oracle and the intermediary both look at the same
set: the method name is written in the message and depends on nothing else. In a request
left unanswered, this single word alone decides whether the
intermediary resends it.

`storable` is also correct in all forty, but not for the same reason. This
decision depends on two fields at once: the method must be safe **and** the response
must not be personalized. **Seventeen** of the forty exchanges are actually
personalized. The intermediary cannot infer this from the method; it looks at the flag
the server set.

## The Cost of Retrying

`repeatable` coming out correct in all forty does not mean it is free; in the case it
covers, it is expensive. Say a request was sent and no response came back. All the
intermediary knows is that the response did not arrive. It cannot tell whether the
request never reached the server, or reached it, was processed, and the response was
lost on the way back. The transport layer gives no such distinction: a dropped
connection looks the same in both cases.

If the method is idempotent, no distinction is needed. Whether the request arrived or
not, sending it a second time leaves the same state, so the intermediary resends it
without asking anyone. This is the case in **thirty-one** of the forty exchanges, and
the decision is made without an extra round trip.

The remaining **nine** exchanges use POST. The intermediary cannot resend here, because
a second send could double the result of the first. It can only push the decision to
the endpoints: report the state to the client, or ask the server. Both are a round
trip. A single word zeroes out the round trip in thirty-one exchanges and charges it
in nine.

This is the measure's first concrete payoff. The same field sits in the same place;
what it carries either closes a decision inside the message or converts it into a
round-trip debt. A protocol's number is how these two outcomes distribute across
forty exchanges.

## Why a Missing Flag Produces Two Wrong Decisions, Not Four

The server did not set the flag on **two** of the seventeen personalized responses. In
these two exchanges the intermediary trusts the only declaration it has and counts
the response as shareable. But the wrong-decision count is two, not four; the reason
is in the dump below, which runs after the core and measurement blocks:

```python
print(f"{'left unflagged':<21s} {'shareable':>21s} {'storable':>21s}")
print(f"{'no':>3s} {'method':<7s} {'path':<9s} {'given':>10s} {'truth':>10s} "
      f"{'given':>10s} {'truth':>10s}")
for x in batch:
    if x["private"] and not x["private_marker"]:
        g, v = oracle(x), infer(message(x, OPEN))
        print(f"{x['no']:3d} {x['method']:<7s} {x['path']:<9s} "
              f"{str(v['shareable']):>10s} {str(g['shareable']):>10s} "
              f"{str(v['storable']):>10s} {str(g['storable']):>10s}")
```

```
left unflagged                    shareable              storable
 no method  path           given      truth      given      truth
  1 POST    /kayit          True      False      False      False
 34 POST    /kayit          True      False      False      False
```

Both exchanges use **POST**, which is not safe; the intermediary would not have
cached its response with or without the flag, so `storable` comes out **correct** in
both — not for the correct reason, but correct. `shareable`, on the other hand,
depends only on the flag and comes out wrong in both.

The rule that follows is as important as the measure itself: **a missing declaration
only costs something where a different decision could otherwise have been made.** If
these same two exchanges had used GET, `storable` would also have gone wrong, making
the total seven wrong decisions, not five. Decisions at the intersection of two
fields are more resilient than decisions tied to one field, because the second field
can cover for the first.

The remaining three wrong decisions come from the `fresh` decision. Twelve of the
forty copies have actually gone stale; in **three** the validator tag failed to catch
the change, and the intermediary called a stale copy fresh. The validator's
resolution is a later lesson's subject; here only its contribution to the wrong count
is counted: two plus three, **five**.

## Nothing Verifies the Declaration

Two of the five wrong decisions come from a flag the server did not set, three from a
tag's resolution. What they share is this: **neither is the intermediary's fault.**
The intermediary read what was written and processed it correctly; the wrong decision
comes from what is written parting ways with the truth.

There is no mechanism in the protocol that closes this gap. The intermediary cannot
test whether a method is actually safe, or see that a request declaring POST changed
nothing on the server, or that a request declaring GET deleted a record. All it has
is the declaration, and **the intermediary believes the declaration.**

There is also a narrowing side to this: when the server does not put a field in the
message, the intermediary **cannot** make the decision tied to that field; when it
does, the intermediary uses it without question. This choice between the two ends is
what gets measured one field at a time in the lessons that follow — what the
intermediary gains if a field stays visible, and what it loses if the field
disappears.

## Summary

- The transport layer carries bytes and does not know their meaning; meaning is built by
  the application layer, and the only thing that builds that meaning is what is written
  in the message.
- A protocol's measure is not the number of fields it carries but the number of decisions
  it lets be made without an extra round trip; an unavailable decision is a round-trip
  debt.
- A method declares two things: safety and idempotence. From these the intermediary
  derives the `repeatable` and `storable` decisions.
- On the set of forty exchanges and five decisions, the open regime gives 195 correct, 5
  wrong, 0 unavailable decisions and 0 extra round trips; the measurement reads 3724
  bytes.
- Two of the five wrong decisions come from the two responses left unflagged, and it is
  two, not four, because those two exchanges' method was already not safe; the remaining
  three come from changes the validator tag missed.
- There is no mechanism that verifies what is written in the message; the intermediary
  believes the declaration, and the wrong decisions come from that trust.

## Next Step

Every field measured in this lesson sat on the request side: method, path, host. The
response's headers were looked at, but its **first line** was deliberately left out.
Yet the first decision the intermediary makes about a response sits on that line, and
what it carries is a three-digit number. The next lesson measures that number: can
the intermediary, without opening the body, decide whether to retry or cache just by
reading the first digit — and when an intermediary that does open the body reaches
the same decision, how many extra bytes does it read to get there.
