Skip to content
C.W.K.
Lesson 03 of 05 · published

Routes and Their Refusals

~16 min · routing, http-status, error-design, pairing

Level 0Idle Phone
0 XP0/45 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

Thirteen routes, one switch

Router.route is a switch over the method and the path, and there are thirteen pairs in it. I list them in the order a caller meets them:

RouteTokenWhat it answers
GET /hellonowho answers here: app, version, build, paired, and the wire's part versions
POST /pairnosets the token once; 409 after that
GET /statusyesthe body now: device, battery, grants, what it is doing
POST /photo, POST /clipyesa JPEG, or 1 to 15 s of HEVC video, with facts in a header
GET /whereyesone fresh GPS fix
POST /say, POST /hushyesplays a line and says how long it played; stops the line now
POST /faceyesOriel's push of what the face should show
GET /ears, POST /ears, GET /ears/next, POST /listenyesthe ears, their switches, Oriel's long poll, the listening grant

A known path asked with the wrong method gets 405 with the sentence "GET is not how /photo is asked". A path the switch does not know gets 404, "no route /nowhere". Both are in wrongMethodAndUnknownPath.

Two routes before the token

The first block is the top of route. /hello and /pair are matched before anything checks a token, because a fresh install has none. /hello says what Oriel needs to decide whether to talk at all: paired, and the version of each part of the wire, wire 1, ears 1, onair 2, solo 1. The numbers let Oriel tell an older build from a newer one without guessing: the comment beside earsVersion says a body whose /hello lacks it has no ears.

/pair has three answers. The first valid token, 64 hex characters, is saved and answered 200. A token of any other shape is 400, "a token is 64 hex characters". A second pair, whatever its token, is 409: "this body is already paired; reinstall Cordelia to pair it again". Re-pairing is a reinstall on purpose, and the next track explains why.

Then come two guards in a row, both 401 and with different words. An unpaired body answers every other route with "this body is not paired yet; Oriel pairs it first". A paired body answers a missing or wrong token with "the token does not match this body's pairing". Because these guards stand before the route lookup, a stranger without the token gets 401 even for a path that does not exist. The door tells it nothing about which routes are behind it.

The body refuses, the router only carries it

The router decides only the refusals it can see in the request: a camera that is not back or front, a clip outside 1 to 15 s, a /say whose body is not audio or is empty, an /ears change that names nothing. Everything about the device's state is decided by the device, which throws a Refusal carrying a status and a sentence. The bottom of route (second block) turns any Refusal into exactly that status and sentence, any WireError into 400, and anything else into 500 with "the body failed:" and the error's description. The test theBodysRefusalKeepsItsStatusAndReason loads a fake with .notActive("camera") and checks that 409 and the words "not in front" both arrive.

StatusWhat it names on this wire
401no pairing yet, or a token that does not match
403the camera or the location grant is missing; a person allows it once in Settings
409the app is not in front, so iOS has the camera off; already paired; a line is already playing
413a head over 16 KB or a body over 32 MB (from the door, before routing)
423the camera is busy with another capture
503no such camera on this device, a camera that would not open, the body shutting down
504a named deadline ran out (the last lesson of this track)

The order of the guards is part of the meaning. A photo checks the front first (409), then the grant (403), then whether the camera is free (423). So a 423 also tells Oriel that the app is in front and allowed: only the camera's own state is in the way. A fix checks the grant before the front, because a missing grant is the problem a person has to solve first.

409 means three things, and the sentence says which

409 shows up for an app in the background, a second pair, and a second line while one plays. A status code is a category, never a diagnosis. Each of the three carries its own sentence, and that sentence is what Oriel logs and what a brain reads back. The number lets code branch; the words let a person act.

What Oriel does with a refusal

Oriel's side reads the wire the same way. ensure_paired (third block) calls /hello first and refuses a body that speaks another wire version. A body that says it is paired while Oriel holds no token for it was paired by someone else, and Oriel stops with "reinstall Cordelia there to pair it again". Only an unpaired body gets a fresh token. When a body answers an error, Oriel's _json keeps the body's sentence ("refused a front photo: …") and folds the number for its own callers: anything under 500 becomes 409, anything from 500 up becomes 503. The device's fine distinctions survive in the words, and Oriel's callers branch on two numbers.

Code

cwkCordelia, CordeliaKit/Sources/CordeliaKit/Router.swift: /hello and /pair, then the two token guards (verbatim, lines 221-242)·swift
  private func route(_ request: HTTPRequest) async -> HTTPResponse {
    switch (request.method, request.path) {
    case ("GET", "/hello"):
      return .json(200, ["app": body.identity.app, "version": body.identity.version, "build": body.identity.build,
                         "paired": pairing.paired, "wire": wireVersion, "ears": earsVersion, "onair": onairVersion,
                         "solo": soloVersion])
    case ("POST", "/pair"):
      let token = (try? request.json())?["token"] as? String ?? ""
      switch pairing.pair(token) {
      case .paired: return .json(200, ["ok": true, "paired": true])
      case .alreadyPaired: return .refuse(409, "this body is already paired; reinstall Cordelia to pair it again")
      case .badToken: return .refuse(400, "a token is \(Pairing.tokenLength) hex characters")
      }
    default:
      break
    }
    guard pairing.paired else {
      return .refuse(401, "this body is not paired yet; Oriel pairs it first")
    }
    guard pairing.admits(request.header("authorization")) else {
      return .refuse(401, "the token does not match this body's pairing")
    }
cwkCordelia, CordeliaKit/Sources/CordeliaKit/Router.swift: 405, 404, and every thrown refusal kept as its status and words (verbatim, lines 312-325)·swift
      case (_, "/status"), (_, "/photo"), (_, "/clip"), (_, "/where"), (_, "/say"), (_, "/face"), (_, "/hello"),
           (_, "/pair"), (_, "/ears"), (_, "/ears/next"), (_, "/hush"), (_, "/listen"):
        throw Refusal(405, "\(request.method) is not how \(request.path) is asked")
      default:
        throw Refusal(404, "no route \(request.path)")
      }
    } catch let refusal as Refusal {
      return .refuse(refusal.status, refusal.reason)
    } catch let wire as WireError {
      return .refuse(400, wire.reason)
    } catch {
      return .refuse(500, "the body failed: \(error.localizedDescription)")
    }
  }
cwkOriel, backend/cordelia.py: Oriel reads /hello before it pairs, and stops at a body paired elsewhere (verbatim, lines 280-293)·python
    def ensure_paired(self) -> dict[str, Any]:
        """Pair on first contact; a body paired to someone else stops here."""
        hello = self.hello()
        if int(hello.get("wire") or 0) != WIRE:
            raise BodyError(f"{self.body.name} speaks wire {hello.get('wire')}, Oriel speaks {WIRE}", 409)
        if hello.get("paired") and not self._token():
            raise BodyError(f"{self.body.name} is paired with something else (Oriel holds no token for it); "
                            "reinstall Cordelia there to pair it again", 409)
        if not hello.get("paired"):
            token = self._token(create=True)
            status, _, raw = self.request("POST", "/pair", payload={"token": token}, timeout=TIMEOUTS["pair"],
                                          what="pairing", auth=False)
            self._json(status, raw, "pairing")
        return hello

External links

Exercise

List every refusal your own service can give, as a table of status, cause and the sentence the caller sees. Then restructure your handler so that the device or domain layer throws a typed refusal carrying a status and a sentence, and the router only carries it out unchanged. Add one test that loads a fake with a refusal and checks that both the status and a key phrase of the sentence reach the response.
Hint
Put the authentication guard before the route lookup, so an unauthenticated caller gets the same 401 for a real path and a made-up one. Then check the order of your state guards: which refusal should a caller see first when two things are wrong at once?

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.