grpc/grpc-go · error

json.Unmarshal(%v): %v

Error message

json.Unmarshal(%v): %v

What it means

Returned by tokenInfoFromResponse in sts/sts.go:326 when json.Unmarshal of the STS response body into responseParameters fails. The STS server returned HTTP 200 but a body that is not valid JSON for the expected fields (it logs/returns the raw bytes via %v).

Source

Thrown at credentials/sts/sts.go:326

	// responsibility of the caller to read the response body till an EOF is
	// encountered and to close it.
	body, err := io.ReadAll(resp.Body)
	resp.Body.Close()
	if err != nil {
		return nil, err
	}

	if resp.StatusCode == http.StatusOK {
		return body, nil
	}
	logger.Warningf("http status %d, body: %s", resp.StatusCode, string(body))
	return nil, fmt.Errorf("http status %d, body: %s", resp.StatusCode, string(body))
}

func tokenInfoFromResponse(respBody []byte) (*tokenInfo, error) {
	respData := &responseParameters{}
	if err := json.Unmarshal(respBody, respData); err != nil {
		return nil, fmt.Errorf("json.Unmarshal(%v): %v", respBody, err)
	}
	if respData.AccessToken == "" {
		return nil, fmt.Errorf("empty accessToken in response (%v)", string(respBody))
	}
	return &tokenInfo{
		tokenType:  respData.TokenType,
		token:      respData.AccessToken,
		expiryTime: time.Now().Add(time.Duration(respData.ExpiresIn) * time.Second),
	}, nil
}

// requestParameters stores all STS request attributes defined in
// https://tools.ietf.org/html/rfc8693#section-2.1.
type requestParameters struct {
	// REQUIRED. The value "urn:ietf:params:oauth:grant-type:token-exchange"
	// indicates that a token exchange is being performed.
	GrantType string `json:"grant_type"`
	// OPTIONAL. Indicates the location of the target service or resource where

View on GitHub (pinned to 03255a9237)

Solutions

  1. Inspect the raw body in the error message to identify what was actually returned.
  2. Confirm TokenExchangeServiceURI points at the real STS token endpoint, not a generic web frontend.
  3. Bypass any intercepting proxy for the token-exchange host.
  4. If the server legitimately returns a different JSON shape, switch to a custom PerRPCCredentials implementation.

Example fix

// before
opts := sts.Options{TokenExchangeServiceURI: "https://gateway.example.com/"} // returns HTML

// after
opts := sts.Options{TokenExchangeServiceURI: "https://sts.example.com/v1/token"} // returns JSON
Defensive patterns

Strategy: validation

Validate before calling

// Sanity-check the STS endpoint returns JSON before relying on it.
resp, err := http.Post(opts.TokenExchangeServiceURI, "application/json", bytes.NewReader(sampleBody))
if err != nil { return err }
defer resp.Body.Close()
if ct := resp.Header.Get("Content-Type"); !strings.Contains(ct, "application/json") {
    return fmt.Errorf("STS endpoint returned non-JSON (%s)", ct)
}

Try / catch

if strings.Contains(err.Error(), "json.Unmarshal") {
    // STS endpoint returned a non-JSON body (HTML page, wrong service, proxy)
    // inspect the raw bytes in the error and point the URI at the real token endpoint
}

Prevention

When it happens

Trigger: The token-exchange endpoint returned an HTML error page, a plain-text message, or a JSON shape that does not deserialize into responseParameters, despite a 200 status; a proxy/CDN intercepting the response.

Common situations: Corporate proxy returns a 200 + HTML captive-portal page; STS endpoint behind an API gateway returning a generic success page; misconfigured endpoint URL that hits a different service; version skew where the server returns a newer incompatible schema.

Related errors


AI-assisted analysis of grpc/grpc-go@03255a9237 (2026-08-07). Data as JSON: /api/errors/07745b9aaf79f5e9. Report an issue: GitHub.