Skip to content
academia.sh

Lesson 01 / 16

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.

Contents

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:

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:

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.00501/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.

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.

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:

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.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close