Conditional Requests¶
The etagcache package provides an HTTP transport that makes conditional
requests against the GitHub API. GitHub returns an ETag with most GET
responses and answers a repeat request carrying If-None-Match with
304 Not Modified when nothing changed. A 304 does not count against the
rate limit. For anything that polls — a dashboard refreshing repository
health every few minutes, a tool that re-reads the same repositories each
run — most responses are unchanged, so most requests become free.
import (
"github.com/grokify/gogithub/clientv1"
"github.com/grokify/gogithub/etagcache"
)
cache := etagcache.NewTransport(nil)
client, err := clientv1.NewClientWithOptions(ctx, clientv1.ClientOptions{
Token: token,
Transport: cache,
})
// ... use the client as usual, then:
fmt.Println(cache.Stats()) // "412 requests, 397 served from cache, 15 fetched, 0 uncacheable"
How it works¶
The transport sits below the token: clientv1 attaches Authorization, then
the transport sees the request.
- On a
GETorHEAD, it looks up the URL in its store. With an entry, it addsIf-None-Match(andIf-Modified-Sincewhen known) to the request. - On
304 Not Modified, it returns the cached body and headers as a200 OK, so go-github and every gogithub package see an ordinary success. Rate limit headers,Date, and the request ID come from the 304, soGetRateLimitand error messages stay accurate. - On
200 OKwith anETag, it stores the body and headers (including theLinkpagination header) and returns the response.
Requests with other methods, non-200 responses, and responses without an
ETag pass through untouched. A request that already carries its own
If-None-Match is sent as is.
Entries are keyed by method, URL, and a hash of the Authorization header,
so a store shared by clients with different tokens never serves one token's
response to another. The token itself is never stored.
Stores¶
| Store | Use |
|---|---|
NewMemoryStore(maxEntries) |
Long-running processes. Least-recently-used eviction when maxEntries is exceeded; 0 means unlimited. NewTransport uses an unlimited one. |
NewFileStore(dir) |
Command-line tools, so the cache survives between runs. One JSON file per entry, directory 0700, files 0600, written atomically. No eviction; remove the directory to clear it. |
store, err := etagcache.NewFileStore(filepath.Join(os.UserCacheDir(), "myapp", "github"))
cache := etagcache.NewTransportWithStore(nil, store)
Implement Store for anything else (Redis, a database). Store failures never
fail a request: the transport proceeds without the cache and calls
Transport.OnStoreError, if set.
Cached bodies contain whatever the API returned, including private repository data. Treat the store accordingly.
What to expect¶
For repository health, the four requests per repository all
return ETags, so a refresh in which nothing changed costs zero rate limit.
The requests are still made — a 304 is a round trip — so wall-clock time is
similar; what the cache buys is rate limit headroom and, for large bodies,
bandwidth.
GitHub's documentation on conditional requests describes the behavior the transport relies on.
Command line¶
gogithub health --cache-dir <dir> uses a FileStore in that directory and
prints the hit statistics to stderr after the tables. See the
CLI guide.