fetch(): HTTP in a Trench Coat
Part of: Dev Docs
await fetch("http://api.ipify.org") in a .mx file looks like the browser. It is not the browser. It is one POSIX/WinSock socket shim, a detached worker thread per request, and an awaitable that resumes your coroutine when the bytes arrive. This page explains the trench coat — what the lowering produces, how the request actually travels, and why the code is shaped the way it is.
The user-facing behavior (async/await, Response API) is documented in the main docs' async guide; the coroutine machinery it rides on is in The Reactivity Engine.
The shape (runtime/cpp/net/net.h, net.cpp)
| Piece | What it is |
|---|---|
morph::net::Headers |
Case-insensitive map; append joins with ", " like the Fetch spec |
Response |
A Fetch API mirror: status / statusText / headers / body / url / redirected / type / bodyUsed / ok() + text() / json() / arrayBuffer() / clone() + a Node-like std::formatter |
detail::SharedState, detail::HttpAwaitable |
The coroutine plumbing between the request and your co_await |
http_request |
The one HTTP stack for all OSes: getaddrinfo → connect → HTTP/1.1 Connection: close request → recv_all → header parse + redirect detection |
One stack for all OSes, behind a WinSock/POSIX socket shim. HTTP/1.1 with Connection: close — no keep-alive pool, no HTTP/2, no regrets. This client does exactly one thing (fetch a URL, return a Response) and does it portably.
The journey of a fetch()
your .mx: const r = await fetch(url)
│ lowers to fetch() → morph::Result<JsString>-ish / fetch_response() awaitable
▼
HttpAwaitable::await_suspend — spawns a DETACHED worker thread
│ worker does the blocking request (main thread never blocks)
│ reports to DevTools via devNetBegin/devNetEnd (MORPH_FEATURE_DEV)
│ status == 0 → thrown JsValue Error (network failure speaks JS)
▼
h.resume() — your coroutine continues on the main-thread heartbeat
│ (and h.destroy() if done — the fire-and-forget case)Three design decisions worth understanding:
- The worker thread is detached and blocking. No async sockets, no reactor, no event loop inside the event loop. A thread per request is gloriously unfashionable and completely adequate at UI-app scale — and every line of it is debuggable with a normal debugger.
fetch_response()returns the awaitable directly. The comment in the code explains the alternative that was rejected: wrapping it in an eagerResultlambda would race and yield a default-constructed Response. Returning the awaitable keeps exactly one owner of the completion. If you ever refactor this path, re-read that comment first — it is guarding a real race, not a hypothetical one.- Failures arrive as thrown
JsValueErrors.try/catcharoundfetch()in your.mxfile catches a genuine JS-flavored error value, which is why theipcheckerexample's loading/error-state dance works with ordinary syntax.
Worked example: the ipchecker pattern
const [ip, setIp] = morphState("…")
const [err, setErr] = morphState("")
morphEffect(() => {
(async () => {
try {
const r = await fetch("http://api.ipify.org")
if (r.ok()) setIp(r.text())
else setErr("status " + r.status())
} catch (e) {
setErr("network failed")
}
})()
}, [])Native-side, that await parks the coroutine (see next_frame/scheduler semantics in The Reactivity Engine), the worker thread blocks on sockets, and resume() continues the coroutine on the main heartbeat — where setIp notifies subscribers and the window re-renders. The .mx author wrote five lines; four threads' worth of coordination happened underneath.
DevTools sees everything
Every request is logged through devNetBegin/devNetEnd (gated by MORPH_FEATURE_DEV), which feeds the DevTools Network tab: status, timing, headers, body previews. If a request misbehaves, open the Network tab before reaching for packet captures — the trench coat has glass pockets.
Where to cut
| "I want to…" | Touch |
|---|---|
| Change request building or response parsing | net.cpp (http_request, build_request, header parse) |
| Change await/resume semantics | detail::HttpAwaitable — re-read the fetch_response() race comment first |
Change the Response API surface |
net.h — and mirror the Fetch spec deliberately, not accidentally |
| Add redirect/HTTPS policy | http_request redirect detection; note TLS posture is a security decision, see docs/future/ security notes |
Verify by
<binary> --morph-self-test
./tests/runtime/run-selftests.sh
# Plus a manual run of examples/ipchecker under morph dev with the Network tab openNetworking changes deserve a live run: build ipchecker, watch the request in DevTools, then kill the network and confirm the catch path speaks a proper JsValue Error.