HTTP call requirements: a route and a name
An HTTP call needs two things from the network: a route, so packets from the client reach the server, and a name, so the client knows which address to connect to. An HTTP client opens a connection to a server, sends a request and reads the response. On the internet, IP routing provides the first and DNS the second.
Machines on one local network, such as the same Wi-Fi access point or the same Ethernet switch, have a route to each other without the internet. They can also have names: multicast DNS (RFC 6762) lets a machine answer to a .local name with no DNS server, by multicasting the query on the local link.
Both stop at the edge of that network. Multicast DNS is link-local, so its queries are not routed to other networks. And two machines can be close together with no route at all:
- each is on a different local network, with no router between them;
- they are joined only through a third machine that does not forward IP traffic, for example two single-board computers each plugged into a laptop;
- the only link between them is Bluetooth LE, which carries no IP traffic between ordinary apps.
In each case the client has nowhere to open a connection to.
HTTP service access with no route
A program can reach an HTTP service on a machine it has no route to in two ways: give the machines a route, or carry the request as a message.
Give the machines a route. Put them on one network, or add a router between the networks, or run a VPN or overlay network that gives each machine an address the other can reach. This keeps HTTP exactly as it is. The cost is setup and dependencies: a static tunnel needs keys and endpoints configured on each machine, managed overlays usually rely on a coordination service, and none of it helps over a link that carries no IP, such as Bluetooth LE between a phone and a box.
Carry the request as a message. Put a small proxy beside each program. The client sends its HTTP request to the proxy on its own machine. The proxy turns the request into a message and hands it to a messaging layer that can deliver it by whatever path exists: a LAN, a chain of devices that relay for each other, Bluetooth LE, or an internet relay. On the far side, the proxy replays the request against the service on its own machine and sends the response back the same way. Neither program changes; they still speak HTTP to something on their own machine.
The second approach works where there is no route, but it changes some assumptions that HTTP programs make without noticing.
HTTP requests carried as messages
When HTTP requests travel as messages, timeouts, body size, caller identity, encryption and naming all work differently.
Timeouts. A message can wait for a path, and a path can be slow. A request that times out at the client may still arrive and run on the service. RFC 9110 defines which HTTP methods are idempotent, meaning that repeating them has the same effect as sending them once, such as GET, PUT and DELETE. A request that is not, such as a POST that starts a job, needs an idempotency key the service checks, or a client that does not retry blindly.
Body size. A message layer has limits that a TCP connection does not, and a slow radio link makes large bodies slow. Keep request and response bodies small, and move large files another way.
Caller identity. The service sees every request arrive from the local proxy, so the source IP address says nothing about the caller. A design whose devices have verifiable identities can tell the service which device called, in a header the proxy sets. The service can trust that header only if nothing but the proxy can reach it.
Encryption. If the message passes through other devices, they should carry it without being able to read it. That means encrypting end to end between the two proxies, not only on each link.
Naming. With no DNS, the device has to be named some other way. One option is to put the device’s identity into the host name, so a request to a name such as <service>.<device>.<domain> tells the proxy where to send it.
Offline Protocol HTTP front for device-to-device calls
Offline Protocol’s SDK added the proxy-and-message pattern in version 0.29.0, as the HTTP front in its Python service. A provider registers a local HTTP endpoint with the front on its own box:
curl -X PUT http://127.0.0.1:8080/services/timeofday \
-H 'content-type: application/json' \
-d '{"callback": "http://127.0.0.1:9000"}'
A program on another box calls it with an ordinary request through its own front. The host name carries the service and the providing device’s address:
curl -H "Host: timeofday.<device address>.offline.protocol.internal" \
http://127.0.0.1:8080/now
The fronts carry the request and the response inside end-to-end encrypted messages, over whatever carrier reaches the other box, including through a box in between. The provider’s endpoint receives the caller’s authenticated address in an X-Offline-Protocol-Sender header. Bodies are limited to 128 KiB each way, and a request waits 30 seconds by default. The specification is explicit about the assumptions above: a 504 does not mean the request did not run, the front does not decide who may call, and the provider should trust the sender header only when nothing but the front can reach its endpoint.