โ† Over the wire ยท all materials

Over the wire on one page

Everything from Lecture 8, condensed. Print it, stick it next to your screen.

1 An HTTP exchange

POST /api/stalls/taco-bike/reviews HTTP/1.1   // method ยท target
Content-Type: application/json                // headers
                                              // empty line
{"author":"Ana","rating":5,"text":"โ€ฆ"}        // body

HTTP/1.1 201 Created                          // status
Location: /api/reviews/r17                    // headers
{"id":"r17", โ€ฆ}                               // body

Origin = scheme + host + port. The fragment (#โ€ฆ) is never sent.

1 Methods & statuses

GET readsafe, idempotent
POST create / actionneither: don't retry blindly
PUT set at a URL ยท DELETEidempotent
PATCH change some fieldsnot guaranteed
200 ยท 201 ยท 204OK ยท Created ยท no body
400 ยท 401 ยท 403 ยท 404malformed ยท no or invalid credentials ยท valid but not allowed ยท not found
409 ยท 415 ยท 422 ยท 429conflict ยท not JSON ยท invalid values ยท slow down
500 ยท 503server bug ยท try again later

2 The event loop

One thread. Code on the stack runs to the end; then every microtask (promise callbacks, resuming after await); then one task (timer, event); maybe a render; repeat.

console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("D");                 // A D C B

2 Two styles, same promise

getStalls()
  .then((stalls) => render(stalls))
  .catch((error) => showError(error))
  .finally(() => hideSpinner());

try { render(await getStalls()); }
catch (error) { showError(error); }
finally { hideSpinner(); }

.then returns a new promise; return a value or a promise from it, throw to reject. await only in async functions and modules. Independent: await Promise.all([a(), b()]).

2 fetch

const response = await fetch("/api/stalls?tag=food");
response.status;        // 200
response.ok;            // 200โ€“299
response.headers.get("X-Total-Count");
const data = await response.json();   // second await: the body

A 404 or a 500 resolves. fetch rejects only without a response: network, CORS, abort. Body readers: json() ยท text() ยท blob(), once each response.

2 One helper

async function getJSON(url, options) {
  const response = await fetch(url, options);
  if (!response.ok) {
    const body = await response.json().catch((e) => {
      if (e.name === "AbortError") throw e;
      return null;          // an error page that isn't JSON
    });
    throw new HttpError(response, body); // .status, .body
  }
  return response.status === 204 ? null : response.json();
}

2 URLs

const params = new URLSearchParams();
if (tag !== "all") params.set("tag", tag);
params.set("q", "rock & roll");      // encoded for you
fetch(`/api/stalls?${params}`);

3 Async UI state

state.status = "loading";   // "loading" | "ready" | "error"
render();
try {
  state.stalls = await getStalls();
  state.status = "ready";
} catch (error) {
  console.error(error);
  state.error = describe(error);   // a sentence, not "Failed to fetch"
  state.status = "error";
}
render();                   // empty = ready && stalls.length === 0

role="status" for messages ยท role="alert" for the error, always in the page, only its text changes ยท Try again = loadStalls(), focus moved first.

3 How it fails

no network, CORSTypeError (message varies by browser)
4xx / 5xxresolves, ok === false
body isn't JSONjson() โ†’ SyntaxError (on a 2xx: wrong URL)
aborted ยท timed outAbortError ยท TimeoutError

4 POST JSON

const data = Object.fromEntries(new FormData(form));
const review = { ...data, rating: Number(data.rating) };

const response = await fetch(url, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(review),
});

Forget JSON.stringify โ†’ [object Object] (400). Forget the header โ†’ text/plain (415). A string number โ†’ 422.

4 After the response

  • 201: render the server's copy (it has the id), reset the form.
  • 422: body.fields[name] under each field, aria-invalid, keep the input.
  • Other: a sentence, keep the input.
  • button.disabled = true before, false in finally.
  • 204: no body. Don't call json().
  • Project helper paths are relative to /api: postJSON("/stalls/taco-bike/reviews", review).
  • Through the helper, a 422 is thrown: catch (e) { if (e.status === 422) showErrors(e.body.fields); }

5 CORS

A page may read a cross-origin response only if the server sends Access-Control-Allow-Origin. JSON POSTs and Authorization trigger a preflight (OPTIONS). The fix is on the server; mode: "no-cors" gives an unreadable response.

5 Abort what's obsolete

let controller = null;
input.addEventListener("input", async () => {
  controller?.abort();
  controller = new AbortController();
  try {
    render(await getJSON(url, { signal: controller.signal }));
  } catch (error) {
    if (error.name !== "AbortError") showError(error);
  }
});
fetch(url, { signal: AbortSignal.timeout(8000) });  // a timeout

โœ“ When a request misbehaves

  1. Network panel: is it sent? Method, URL, status?
  2. Payload: what did you really send? Types?
  3. Response: what does the server say?
  4. Console: CORS? Uncaught (in promise)?
  5. Then the API's docs. Then the code.

Keys and tokens in browser code are public. Server data is user input: textContent.