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).

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.