Skip to content

Add ETag conditional requests to the REST transport - #3026

Open
joshfree wants to merge 4 commits into
github:mainfrom
joshfree:feat/etag-conditional-requests
Open

Add ETag conditional requests to the REST transport#3026
joshfree wants to merge 4 commits into
github:mainfrom
joshfree:feat/etag-conditional-requests

Conversation

@joshfree

@joshfree joshfree commented Aug 5, 2026

Copy link
Copy Markdown

Summary

Adds HTTP conditional-request (ETag / If-None-Match) support to the REST transport so unchanged resources are revalidated with a 304 Not Modified instead of being re-downloaded in full.

Why

Every REST request is currently issued unconditionally: the ETag returned by the GitHub API is never stored or replayed, so repeated tool calls that read the same resource (for example pull request reads, file and commit listings, and reviews) download the full response each time.

Fixes #3025

What changed

  • Added ETagTransport, a bounded (LRU), concurrency-safe http.RoundTripper that caches the ETag and body of cacheable GET responses and sends If-None-Match on the next identical request; a 304 Not Modified is served from the cached body.
  • Wired ETagTransport into createGitHubClients() below the user-agent and auth layers, so cached entries are scoped by the request's Authorization header and never shared across tokens.
  • Rate-limit headers are surfaced from the live 304 response so downstream rate-limit accounting stays correct.
  • Added ETag / If-None-Match header constants.

Every request is still sent to the server, so responses are always revalidated and never served stale. Per the GitHub REST API docs, a 304 Not Modified does not count against the token's primary rate limit, so repeated reads conserve rate-limit budget and bandwidth while returning identical data.

MCP impact

  • No tool or API changes

Security / limits

  • Auth / permissions considered — cache keys include a hash of the Authorization header, so cached bodies are never served across different tokens.
  • Data exposure, filtering, or token/size limits considered — the cache is bounded (default 512 entries, LRU eviction) and in-memory only.

Tool renaming

  • I am not renaming tools as part of this PR

Lint & tests

  • Tested locally with go test ./pkg/http/transport/... (new etag_test.go covers the 304-serves-cached-body flow, per-token scoping, rate-limit header pass-through, GET-only caching, and caller-supplied conditional headers). go build ./... and go vet ./... pass.

Docs

  • Not needed

Every REST request was issued unconditionally: the ETag returned by the
GitHub API was never stored or replayed, so repeated tool calls that read
the same resource (for example pull request reads, file and commit
listings, and reviews) re-downloaded the full response each time.

This adds an ETagTransport round tripper that caches the ETag and body of
cacheable GET responses and sends If-None-Match on the next identical
request. When the API answers 304 Not Modified, the cached body is served
instead of re-downloading it. The transport is inserted below the
user-agent and auth layers in createGitHubClients(), so cached entries are
scoped by the request's Authorization header and never shared across
tokens. The cache is bounded (LRU) and safe for concurrent use.

Every request is still sent to the server, so responses are always
revalidated and never served stale. Per the GitHub REST API docs, a 304
Not Modified response does not count against the token's primary rate
limit, so repeated reads conserve rate-limit budget and bandwidth while
returning identical data. Rate-limit headers are surfaced from the live
304 response so downstream rate-limit accounting stays correct.

Closes github#3025

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4959e1f9-f8e6-4e97-a487-f395a0123c79
@joshfree
joshfree requested a review from a team as a code owner August 5, 2026 23:40
Copilot AI balanced review requested due to automatic review settings August 5, 2026 23:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds ETag-based conditional revalidation to GitHub REST requests to reduce bandwidth and rate-limit usage.

Changes:

  • Adds a concurrency-safe LRU ETag transport.
  • Integrates it into REST client construction.
  • Adds header constants and transport tests.
Show a summary per file
File Description
pkg/http/transport/etag.go Implements ETag caching and revalidation.
pkg/http/transport/etag_test.go Tests conditional-request behavior.
pkg/http/headers/headers.go Adds ETag header constants.
internal/ghmcp/server.go Enables the transport for REST clients.

Review details

  • Files reviewed: 4/4 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread pkg/http/transport/etag.go Outdated
Comment thread pkg/http/transport/etag.go Outdated
The do/helper closures returned *http.Response, which the bodyclose
linter flags at each call site even though the body is closed inside
the closure. Return only the asserted values (status code, body, and
headers) so no response escapes the helper.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4959e1f9-f8e6-4e97-a487-f395a0123c79
@SamMorrowDrums

Copy link
Copy Markdown
Collaborator

Thanks for this, the aims make sense, however most of our usage is through the hosted remote server, which is horizontally scaled and so would need to think about this because it's pretty expensive to add a distributed store there, and additional Redis usage, extra network request per request that would normally be false adding latency, in-memory store would likely be much less useful and we also handle hundreds of RPS so really would need to think about this in that context.

joshfree and others added 2 commits August 5, 2026 17:56
revive's redefines-builtin-id flags the local variable named max in
the LRU eviction loop, which shadows the Go 1.21 builtin. Rename it to
limit; no behavior change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4959e1f9-f8e6-4e97-a487-f395a0123c79
Restrict the conditional-request cache to the long-lived local (stdio)
REST client and give the raw-content client a separate transport without
it, so large file bodies are streamed rather than buffered into memory.

Bound the cache by a per-entry and total-byte budget in addition to the
entry count, and never retain responses marked non-storable by HTTP
cache directives (request or response Cache-Control: no-store, or
Vary: *).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4959e1f9-f8e6-4e97-a487-f395a0123c79
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PR tools re-fetch data without conditional requests (ETag / If-None-Match)

3 participants