Back to Gh Cli

`api_host`

docs/api-host.md

2.100.05.0 KB
Original Source

api_host

api_host sends a host's API traffic somewhere other than that host's usual API endpoint, so that an organisation can put a gateway in front of GitHub without every user reconfiguring or re-authenticating.

yml
# hosts.yml
github.com:
    api_host: gh-gateway.example.com
    users:
        octocat:
            oauth_token: gho_...

With that set, gh api repos/cli/cli asks gh-gateway.example.com rather than api.github.com. Everything else about the host is unchanged: it is still github.com as far as login, git remotes and web URLs are concerned.

The shape of the problem

api_host is a claim about all API traffic, so it can only be honoured in one place. Any code that builds an absolute https://api.github.com/... URL and hands it to an *http.Client has already decided where the request goes, and no amount of configuration downstream can redirect it.

That makes this feature unusual to work on in two ways.

Firstly, it is a property of the whole codebase rather than of any one command, so it is only as good as its worst call site. Secondly, a bypass is invisible to ordinary tests: a request that ignores api_host and goes to api.github.com succeeds, because api.github.com is reachable. Tests pass and the feature is quietly broken.

The second point is why this feature comes with a purpose-built test that makes api.github.com unreachable, so a bypass fails loudly instead of silently working. See api-host-test-harness.md.

How it resolves

Request routing lives in go-gh, which reads api_host for the host a request is aimed at and sends the request there instead.

That leaves gh with a problem of its own: credentials. gh resolves tokens from the hostname in the request URL, and once a request has been redirected that hostname is the gateway, which gh has never logged in to and holds no token for. So api/http_client.go maps the gateway back to the host it stands in for, via HostForAPIHost, and sends that host's token.

The fallback only ever adds a token where there would have been none. A host gh is genuinely logged in to keeps resolving to its own token even if some other host names it as an api_host, so the mapping cannot hijack real credentials.

The reverse lookup is deliberately narrow. It answers only "which host does this hostname stand in for", and says nothing about whether a given piece of code should honour api_host. That decision belongs to the caller, because api_host covers API traffic and not, for instance, git operations against the same host.

Scope

api_host applies to API traffic only. Git operations, browser URLs and OAuth device flow continue to use the host itself.

It is a per-host setting under hosts.yml, not a global one, so a user can route one host through a gateway and reach another directly.

Configuring the same api_host on two hosts is a misconfiguration rather than a supported topology, since a request arriving at the gateway could belong to either. The reverse lookup resolves it to the first matching host and does not report an error.

Known gaps

api_host is not yet honoured centrally. It works for requests that go through go-gh's client, and for gh api, but some call sites still build absolute api.github.com URLs and never reach the gateway at all.

Those call sites are being migrated a capability at a time, because each one built its own request for a reason: a header, an endpoint's scopes, or a redirect policy. All three are now expressible on a shared client request, so a call site no longer has to own its destination in order to say what it needs.

gh api is a wart worth naming. It does not use go-gh's client, so it resolves api_host itself with a second implementation of the same rule. Two implementations of "where does this request go" is exactly the shape of problem this feature exists to remove, and it survives here only because gh api takes its path verbatim from the user and cannot route it through the normal client.

Two call sites still send a request the client did not build, through DoRequest: uploading and downloading a release asset. Both use an absolute URL that the API itself supplied, so they reach the right place today, but they resolve their own destination rather than asking the client to.

Whole commands remain unmigrated, and none of them are covered by the harness: gh codespace, gh agent-task and gh copilot each build their own absolute URLs, and the update checker does too. They are not oversights. Codespaces and agent-task talk to services that are not the REST API and have clients of their own, so whether api_host should apply to them is a question about what the setting means rather than about how to implement it.

So the harness proves something narrower than "gh honours api_host": it proves that the twelve journeys it exercises do, and that the shared client can express what a call site needs in order to stop resolving its own destination. What is left is mechanical for the REST commands, and a design question for the rest.