MHSanaei/3x-ui · error

GitHub API returned status %d: %s

Error message

GitHub API returned status %d: %s

What it means

Returned by fetchPanelRelease when the GitHub releases API responds with any status other than 200. The message includes both the numeric code and resp.Status. By far the most common cause is GitHub's unauthenticated rate limit (403 with 'API rate limit exceeded'); 404 means the requested tag does not exist (e.g. a dev-latest tag not yet published), 401 means a bad token.

Source

Thrown at internal/web/service/panel/panel.go:431

// fetchPanelRelease fetches a release from GitHub. An empty tag resolves the
// latest stable release; a non-empty tag (e.g. dev-latest) resolves that tag.
func fetchPanelRelease(tag string) (*service.Release, error) {
	url := "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest"
	if tag != "" {
		url = "https://api.github.com/repos/MHSanaei/3x-ui/releases/tags/" + tag
	}
	client := (&service.SettingService{}).NewProxiedHTTPClient(10 * time.Second)
	req, reqErr := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
	if reqErr != nil {
		return nil, reqErr
	}
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("GitHub API returned status %d: %s", resp.StatusCode, resp.Status)
	}

	var release service.Release
	if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
		return nil, err
	}
	return &release, nil
}

// extractReleaseCommit reads the build commit recorded in the dev release: first
// the `commit=<sha>` marker the CI writes into the body, falling back to the
// tag's target commit.
func extractReleaseCommit(release *service.Release) string {
	if m := releaseCommitRegex.FindStringSubmatch(release.Body); m != nil {
		return strings.ToLower(m[1])
	}
	if isCommitSHA(release.TargetCommitish) {
		return strings.ToLower(release.TargetCommitish)

View on GitHub (pinned to ad32144c42)

Solutions

  1. Wait for the rate-limit window to reset (up to 1 hour) or set GITHUB_TOKEN to raise the limit to 5000/h
  2. For 404 on a specific tag, verify the release exists: curl -s https://api.github.com/repos/MHSanaei/3x-ui/releases/tags/<tag>
  3. Check https://www.githubstatus.com for ongoing incidents and retry later
  4. Confirm the configured HTTP proxy allows api.github.com
Defensive patterns

Strategy: retry

Validate before calling

if resp.StatusCode == http.StatusForbidden {
    if reset := resp.Header.Get("X-RateLimit-Reset"); reset != "" {
        // compute wait time before retrying
    }
}

Try / catch

var apiErr error
for attempt := 0; attempt < 3; attempt++ {
    release, apiErr = fetchPanelRelease(tag)
    if apiErr == nil || !strings.Contains(apiErr.Error(), "status 403") {
        break
    }
    time.Sleep(backoff(attempt)) // honor X-RateLimit-Reset when present
}

Prevention

When it happens

Trigger: More than 60 unauthenticated requests/hour to api.github.com from the same IP (403); fetchPanelRelease("dev-latest") when that tag has no release yet (404); GitHub incident (5xx); proxy blocking api.github.com.

Common situations: Panels on shared IPs (NAT cloud ranges) that exhaust the anonymous quota; checking for a dev release before CI has published it; automated update checks running too frequently.

Related errors


AI-assisted analysis of MHSanaei/3x-ui@ad32144c42 (2026-08-15). Data as JSON: /api/errors/5154c34f8bbda121. Report an issue: GitHub.