Fetch Decisions
Why every call of Link's own goes out on one client, link-fetch, and
why that client reads answers as it does (Fetch).
Why does every call of Link's own go out on one client?
link-fetch applies one set of rules to Link's HTTP calls: HTTPS except
for permitted local calls, no redirects by default, bounded answers,
timeouts, a common user agent and consistent errors. Each caller states
any required exception explicitly. The relay and plugins share this library.
What it leaves out
Agent web requests and model-provider requests have their own rules and are listed in the HTTP-client audit.
Why do calls to others' APIs go through it too?
A credential is as sacred sent to Slack as to Link. A bot's token,
a cloud's key and an account's access token all ride the same rules:
https:// alone, no redirect, an error that never prints them. What
differs is said where the client is made, a named rule each
(outside, metadata_server, plain_http, public_only,
follow_redirects), never a
client built by hand. A service that isn't Link's adds fields without a
new version, so its answers are read open; a value of the wrong type is
still refused.
Why does a call that reads part of an answer say so?
Closed is the default, and a part is a choice a review sees. Some
routes answer more than one caller needs: a balance's level, of all a
console shows. A caller reading two fields of thirty says so where it
calls (answer_part), so the rest isn't refused, and every other
answer stays closed.
Why does every folder run an audit of its HTTP clients?
So a client built by hand is found, as a secret exposed by hand is.
link_fetch::audit finds reqwest's client built in a folder's code
outside its tests; one is either made a link-fetch call or listed
with why it isn't one of Link's own (a model, what an agent fetches, a
test's stand-in). The list is in the folder, beside its secrets', and a
new one fails until a review sees it. It reads the text, not Rust's
names: a client made in a macro, through a crate that re-exports
reqwest, or through a type alias, it can't see, and a review does.
Why is a stream given up when it stalls, not when it's long?
An agent's files take what they take; a dead connection doesn't
move. A client streaming has no timer on the whole call. A body it
sends is watched as it goes, and given up once none of it has gone for
the stall's length, however long it all takes; its answer is then
waited for while the service works on it. An answer it reads is given
up when none of it comes for that long. reqwest's own read timeout
isn't used: it counts from the send to the answer's start, not reset
while a body goes, so it would stop a long upload that's moving.
Why is every answer closed?
An answer that isn't its type is refused, never guessed at. A
field an answer's type doesn't name is refused as deny_unknown_fields
would, for every type read, whether it says so or not: link-fetch
reads through serde_ignored, which finds every field a type passed
over, at any depth, so a type that forgot the attribute is closed all
the same.
A flattened type
One thing hides a field from it: a type that flattens another
(flatten) reads both from one map and drops what neither names. So
an answer's type names each field itself, as the account service's
types, generated from its spec, all do (link-api). An added field in an answer is
a new version of its API (Decisions),
or of the relay's route (Changing the protocol):
an lnk already installed would refuse it.
Why doesn't an error quote the answer?
What's wrong with an answer is said by where, never by what. serde's own message quotes the value it couldn't read, and that value may be a secret, so an answer of the wrong type is named by the call, the kind of fault and its line and column. A refusal's body is shown, since it's the service's words to the person, in printable text and at most 512 bytes of it.