Skip to content

PostBotComment, Assign, Unassign, and Reject in internal/handlers/issue_applications.go all mint a fresh GitHub App installation token per request, bypassing the tokenCache pattern established in projects_public.go #395

Description

@Jagadeeshftw

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

  1. 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).
  2. Replace the four github.NewGitHubAppClient(...) + appClient.GetInstallationToken(...) call sites in PostBotComment, Assign, Unassign, and Reject with a call to the new cached accessor.
  3. Initialize appClient once in NewIssueApplicationsHandler, the same way newProjectsPublicHandler does, rather than per-request.
  4. 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

  • PostBotComment, Assign, Unassign, and Reject share a single cached installation token per installationID within its validity window.
  • A near-expiry or expired cached token is correctly refreshed.
  • A test demonstrates the underlying token-minting call is not repeated for back-to-back requests against the same installation.

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

Metadata

Metadata

Assignees

Labels

GrantFox OSSGrantFox open-source programMaybe RewardedGrantFox: potentially rewarded contributionOfficial Campaign | FWC26GrantFox official campaign issuebackendBackend / API workbugSomething isn't workingperformancePerformance / gas / resource use

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions