Skip to content

[Bug][github_graphql] GitHub App installation token not refreshed for GraphQL client, panic and pipeline crash on large repos #8788

@danielemoraschi

Description

@danielemoraschi

Search before asking

  • I had searched in the issues and found no similar issues.

What happened

When collecting data from a large repository using GitHub App authentication, the pipeline crashes after approximately 1 hour with the following panic:

panic: non-200 OK status code: 401 Unauthorized body: "{
  \"message\": \"Bad credentials\",
  \"documentation_url\": \"https://docs.github.com/rest\",
  \"status\": \"401\"
}"
goroutine 11186 [running]:
github.com/apache/incubator-devlake/helpers/pluginhelper/api.(*GraphqlAsyncClient).updateRateRemaining.func1()
	/app/helpers/pluginhelper/api/graphql_async_client.go:129
created by github.com/apache/incubator-devlake/helpers/pluginhelper/api.(*GraphqlAsyncClient).updateRateRemaining
	/app/helpers/pluginhelper/api/graphql_async_client.go:117

There are two independent bugs that combine to cause this:

Bug 1: Process crash on any error in the rate-limit goroutine

graphql_async_client.go:129 calls panic(err) inside the background goroutine that periodically polls the GitHub GraphQL rate limit. Any error, including a transient 401, immediately kills the entire DevLake process.

Bug 2: GraphQL HTTP client holds a frozen, never-refreshed token (root cause)

In backend/plugins/github_graphql/impl/impl.go (lines 183-209), the GraphQL HTTP client is built with oauth2.StaticTokenSource:

tokens := strings.Split(connection.Token, ",")
src := oauth2.StaticTokenSource(
    &oauth2.Token{AccessToken: tokens[0]},
)
httpClient := oauth2.NewClient(oauthContext, src)

This freezes the token to the value at task start time. The client has no connection to the TokenProvider / RefreshRoundTripper machinery that already handles token refresh for the REST client.

By contrast, CreateApiClient (called just above at line 173) correctly wires up NewAppInstallationTokenProvider + RefreshRoundTripper and auto-refreshes on 401. This was added in #8746. The GraphQL client was not updated as part of that PR and still bypasses the refresh infrastructure with its own isolated oauth2 HTTP client.

Since GitHub App installation tokens expire after 1 hour, any pipeline running longer than that will encounter 401s on all GraphQL requests. The rate-limit polling goroutine then hits the 401 and panics, crashing the process.

Note: the documentation page at https://devlake.apache.org/docs/Configuration/GitHub/#github-apps-beta currently states "The GitHub App token expires every hour and does not auto-renew." This was accurate before #8746 but is now stale for the REST path. The GraphQL path is the remaining gap.

What do you expect to happen

  • The GitHub App installation token should be transparently refreshed for GraphQL requests, the same way it already is for REST requests.
  • If token refresh fails or any other error occurs in the background rate-limit goroutine, the error should be logged and retried, not cause a process-wide panic.
  • Pipelines against large repositories should complete successfully without requiring users to fall back to Personal Access Tokens.

How to reproduce

  1. Configure a GitHub connection using GitHub App authentication (not PAT).
  2. Create a blueprint targeting a large repository whose full collection takes more than 1 hour.
  3. Trigger the pipeline.
  4. After ~1 hour, observe the panic in the logs and the pipeline crashing.

Anything else

Affected files:

File Issue
backend/helpers/pluginhelper/api/graphql_async_client.go:129 panic(err) in background goroutine crashes the process on any rate-limit poll error
backend/plugins/github_graphql/impl/impl.go:183-209 oauth2.StaticTokenSource freezes the token at task start; GraphQL client is not wired to TokenProvider / RefreshRoundTripper

Suggested fix:

  1. In graphql_async_client.go, replace panic(err) with graceful error handling (log the error and reschedule the next poll) so a transient 401 does not crash the process.

  2. In github_graphql/impl/impl.go, instead of creating a new oauth2.StaticTokenSource-backed HTTP client, reuse apiClient.GetClient() -- the REST client's underlying *http.Client -- which already carries the RefreshRoundTripper. This is safe: ApiAsyncClient embeds *ApiClient which exposes GetClient(), and proxy is already configured inside NewApiClientFromConnection so the oauthContext proxy block becomes redundant. Token injection is handled by the RefreshRoundTripper, making the oauth2 wrapper unnecessary.

Related: #8746 (added token refresh for REST path)

Version

v1.0.3-beta10

Are you willing to submit PR?

  • Yes I am willing to submit a PR!

Code of Conduct

Metadata

Metadata

Assignees

Labels

component/pluginsThis issue or PR relates to pluginspriority/highThis issue is very importantseverity/p0This bug blocks key user journey and functiontype/bugThis issue is a bug

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions