Why the endpoint is validated¶
Config::base_url looks like an ordinary convenience. It is the field that
decides where your API key goes, which makes it the most security-relevant setting
in the crate.
Why the option exists at all¶
There are good reasons to let someone point at their own endpoint. They are running a local model. They are behind a corporate proxy that fronts the real provider. They are on a vendor's regional or enterprise host, which lives at a different name. They have a self-hosted gateway doing rate limiting and audit.
All of those are legitimate, and a client that refuses them is a client people work around. So the option stays, and the risk gets handled instead of avoided.
The base URL is where the credential goes¶
The API key rides on every request, to whatever host the URL resolves to. The moment the endpoint is configurable, the destination of the secret is configurable too — and configuration is a free-text field that reaches the client through config files, environment variables and command-line flags, any of which somebody other than the tool author may control.
People do not have to be malicious for that to go wrong. They paste a URL from a
snippet that turned out to be somebody's collection endpoint. They leave http://
on the front and the key crosses the wire in the clear. They copy
https://user:token@host/v1 without noticing that the userinfo changes who they
are authenticating to. They never edit the https://api.example.com/v1
placeholder out of a sample config, and wonder later why their key was sent to a
domain they do not own.
Three rules, checked before the client is built¶
validate_base_url runs inside AiClient::new, before any client is constructed
and long before a request is sent. It rejects three things, each one matching a
way the destination of a secret goes wrong:
Non-HTTPS schemes. A plaintext endpoint puts the key on the wire in the clear
for anything on the path to read. The one exception is allow_insecure_base_url,
which is a Rust-only field on a struct with no Deserialize impl — so no config
file, environment variable or flag can turn HTTPS enforcement off. That is the
point of it not being deserialisable.
Userinfo in the URL. https://user:token@host/ authenticates to somebody, and
which somebody is easy to miss when reading a URL quickly. Credentials belong in
the credential field, where they are a SecretString rather than a substring of a
loggable value.
Documentation placeholder hosts. example.com and example.org, and any
subdomain of either, are what a half-finished config looks like. Rejecting them
turns "my key went somewhere I did not expect" into an error message before the
first request.
What the rules do not cover¶
They are a check on shape, not a policy on destination. A well-formed HTTPS URL
with no userinfo, pointing at a host nobody vetted, is accepted — because
"which hosts are allowed" is a question only the tool embedding rtb-chat can
answer.
If your tool needs an allowlist, build one at the layer that parses configuration.
validate_base_url is public precisely so you can run the shape check there too,
alongside your own rules, and reject a bad endpoint at config-parse time rather
than at client construction.
example.net is worth knowing about: it is a reserved documentation domain too,
and it is not on the list. See
validate_base_url — what gets rejected
for exactly what is and is not caught.
Why the host is logged and nothing else¶
AiClient::new logs one INFO line carrying the provider and the endpoint host.
The path and the query string are deliberately left out — Gemini's API puts the
key in the query string, and a log line is a place secrets go to be copied into
somewhere less careful.
The host on its own is the part that answers the question worth asking after the fact: where was this key being sent?