router-for-me/CLIProxyAPI · error

decode release: %w

Error message

decode release: %w

What it means

Client.FetchLatestRelease GETs https://api.github.com/repos/{owner}/{repo}/releases/latest (with redirect handling and auth headers via the plugin store's `get`) and json.Unmarshals the body into a Release struct. 'decode release: %w' wraps the json error when the 2xx response body is not valid JSON or does not fit the expected shape. It is a protocol-shape error, distinct from HTTP status errors which are raised earlier by readPluginStoreResponse.

Source

Thrown at internal/pluginstore/github.go:78

// FetchLatestRelease returns the latest published release of the plugin's
// GitHub repository, mirroring the WebUI panel update check.
func (c Client) FetchLatestRelease(ctx context.Context, plugin Plugin) (Release, error) {
	owner, repo, errRepository := GitHubRepositoryParts(plugin.Repository)
	if errRepository != nil {
		return Release{}, errRepository
	}
	releaseURL := fmt.Sprintf(
		"https://api.github.com/repos/%s/%s/releases/latest",
		url.PathEscape(owner),
		url.PathEscape(repo),
	)
	data, errDownload := c.get(ctx, releaseURL, "application/vnd.github+json", RequestKindMetadata, 0)
	if errDownload != nil {
		return Release{}, errDownload
	}
	var release Release
	if errDecode := json.Unmarshal(data, &release); errDecode != nil {
		return Release{}, fmt.Errorf("decode release: %w", errDecode)
	}
	return release, nil
}

// FetchReleaseByTag returns a published release by its exact GitHub tag.
func (c Client) FetchReleaseByTag(ctx context.Context, plugin Plugin, tag string) (Release, error) {
	owner, repo, errRepository := GitHubRepositoryParts(plugin.Repository)
	if errRepository != nil {
		return Release{}, errRepository
	}
	tag = strings.TrimSpace(tag)
	if tag == "" {
		return Release{}, fmt.Errorf("release tag is required")
	}
	releaseURL := fmt.Sprintf(
		"https://api.github.com/repos/%s/%s/releases/tags/%s",
		url.PathEscape(owner),
		url.PathEscape(repo),

View on GitHub (pinned to 78f0c4079e)

Solutions

  1. Inspect the wrapped error (errors.Unwrap) — 'invalid character \'<\'' means HTML was served, i.e. a proxy/portal problem, not GitHub.
  2. Reproduce with curl -H 'Accept: application/vnd.github+json' against the same releases/latest URL from the same network.
  3. Bypass or configure the intercepting proxy / captive portal for api.github.com.
  4. If it is a genuine schema mismatch on a field, pin to a GitHub API version header or report the field/shape change to this project.

Example fix

// before
release, err := client.FetchLatestRelease(ctx, plugin)
if err != nil { return err } // 'decode release: invalid character ...'

// after
release, err := client.FetchLatestRelease(ctx, plugin)
if err != nil {
    var syntaxErr *json.SyntaxError
    if errors.As(err, &syntaxErr) {
        log.WithError(err).Error("non-JSON body from GitHub — check proxy/network interception")
    }
    return err
}
Defensive patterns

Strategy: retry

Try / catch

release, err := client.FetchLatestRelease(ctx, plugin)
if err != nil {
    var jsonErr *json.SyntaxError
    if errors.As(err, &jsonErr) {
        log.WithError(err).Warn("non-JSON GitHub response — likely proxy interference; retrying once")
        release, err = client.FetchLatestRelease(ctx, plugin)
    }
    if err != nil { return err }
}

Prevention

When it happens

Trigger: api.github.com returns 200 with a non-JSON body — a captive portal/transparent proxy, a rate-limit page that still returns 200, or an API change in the response schema that breaks unmarshaling (type mismatch on a field). The wrapping preserves the underlying json error via %w.

Common situations: Corporate proxies or hotspots intercepting HTTPS and serving HTML; GitHub Enterprise endpoints with a different response shape; a body truncated by an intermediary so JSON is malformed.

Related errors


AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15). Data as JSON: /api/errors/952387f3cb3fd709. Report an issue: GitHub.