Description
ProjectsPublicHandler maintains a mutex-guarded, per-installation token cache specifically to avoid re-minting GitHub App installation tokens (which themselves require signing a fresh RS256 App JWT and making a network call to GitHub's /app/installations/{id}/access_tokens endpoint) on every request:
// projects_public.go
type ProjectsPublicHandler struct {
...
appClient *github.GitHubAppClient
tokenMu sync.Mutex
tokenCache map[string]struct {
token string
expiresAt time.Time
}
}
IssueApplicationsHandler, however, has four handlers — PostBotComment, Assign, Unassign, and Reject — that each independently construct a brand-new GitHubAppClient and call GetInstallationToken with no caching at all, on every single invocation:
// repeated near-identically in PostBotComment / Assign / Unassign / Reject
appClient, err := github.NewGitHubAppClient(h.cfg.GitHubAppID, h.cfg.GitHubAppPrivateKey)
if err != nil {
slog.Error("failed to create GitHub App client for assign", "error", err)
return httpx.RespondError(c, fiber.StatusInternalServerError, "github_app_client_failed", "")
}
token, err := appClient.GetInstallationToken(c.Context(), installationID)
Installation access tokens are valid for roughly an hour per GitHub's own API contract, but GetInstallationToken (internal/github/app.go) unconditionally requests a new one from GitHub every time it's called — there is no expiry check, no cache lookup. Every application accepted, every assignment made or removed, and every rejection comment sent by a maintainer through these four endpoints costs an extra GitHub App JWT signature plus a full round-trip to GitHub's token-minting endpoint that a cache hit would have avoided.
Requirements
IssueApplicationsHandler must reuse a cached, non-expired installation token for a given installationID across its four call sites instead of minting a new one per request.
- The cache must respect the token's real
expires_at (returned by GetInstallationToken's underlying response) and refresh only once expired, matching projects_public.go's existing behavior.
IssueApplicationsHandler must remain safe for concurrent requests (the cache needs the same kind of mutex protection ProjectsPublicHandler.tokenMu already provides).
Suggested execution
- Add the same
appClient *github.GitHubAppClient, tokenMu sync.Mutex, tokenCache map[string]struct{ token string; expiresAt time.Time } fields to IssueApplicationsHandler (or extract a small shared installationTokenCache helper type used by both handlers to avoid duplicating the caching logic twice).
- Replace the four
github.NewGitHubAppClient(...) + appClient.GetInstallationToken(...) call sites in PostBotComment, Assign, Unassign, and Reject with a call to the new cached accessor.
- Initialize
appClient once in NewIssueApplicationsHandler, the same way newProjectsPublicHandler does, rather than per-request.
- Add a test proving a second call for the same
installationID within the token's validity window does not invoke the underlying GetInstallationToken HTTP call again (mock/fake installationTokenGetter, following the pattern already used in internal/handlers/github_app_cleanup_test.go).
Acceptance criteria
Security notes
No new exposure — installation tokens are still scoped per-installation and still expire — but this needlessly increases GitHub API call volume and JWT signing operations for every maintainer action on an issue, which matters for GitHub App rate-limit budgets shared across the whole deployment.
Guidelines
- Minimum 95% test coverage
- Timeframe: 96 hours
Description
ProjectsPublicHandlermaintains a mutex-guarded, per-installation token cache specifically to avoid re-minting GitHub App installation tokens (which themselves require signing a fresh RS256 App JWT and making a network call to GitHub's/app/installations/{id}/access_tokensendpoint) on every request:IssueApplicationsHandler, however, has four handlers —PostBotComment,Assign,Unassign, andReject— that each independently construct a brand-newGitHubAppClientand callGetInstallationTokenwith no caching at all, on every single invocation:Installation access tokens are valid for roughly an hour per GitHub's own API contract, but
GetInstallationToken(internal/github/app.go) unconditionally requests a new one from GitHub every time it's called — there is no expiry check, no cache lookup. Every application accepted, every assignment made or removed, and every rejection comment sent by a maintainer through these four endpoints costs an extra GitHub App JWT signature plus a full round-trip to GitHub's token-minting endpoint that a cache hit would have avoided.Requirements
IssueApplicationsHandlermust reuse a cached, non-expired installation token for a giveninstallationIDacross its four call sites instead of minting a new one per request.expires_at(returned byGetInstallationToken's underlying response) and refresh only once expired, matchingprojects_public.go's existing behavior.IssueApplicationsHandlermust remain safe for concurrent requests (the cache needs the same kind of mutex protectionProjectsPublicHandler.tokenMualready provides).Suggested execution
appClient *github.GitHubAppClient,tokenMu sync.Mutex,tokenCache map[string]struct{ token string; expiresAt time.Time }fields toIssueApplicationsHandler(or extract a small sharedinstallationTokenCachehelper type used by both handlers to avoid duplicating the caching logic twice).github.NewGitHubAppClient(...)+appClient.GetInstallationToken(...)call sites inPostBotComment,Assign,Unassign, andRejectwith a call to the new cached accessor.appClientonce inNewIssueApplicationsHandler, the same waynewProjectsPublicHandlerdoes, rather than per-request.installationIDwithin the token's validity window does not invoke the underlyingGetInstallationTokenHTTP call again (mock/fakeinstallationTokenGetter, following the pattern already used ininternal/handlers/github_app_cleanup_test.go).Acceptance criteria
PostBotComment,Assign,Unassign, andRejectshare a single cached installation token perinstallationIDwithin its validity window.Security notes
No new exposure — installation tokens are still scoped per-installation and still expire — but this needlessly increases GitHub API call volume and JWT signing operations for every maintainer action on an issue, which matters for GitHub App rate-limit budgets shared across the whole deployment.
Guidelines