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 whereView on GitHub (pinned to 03255a9237)
Solutions
- Inspect the raw body in the error message to identify what was actually returned.
- Confirm TokenExchangeServiceURI points at the real STS token endpoint, not a generic web frontend.
- Bypass any intercepting proxy for the token-exchange host.
- 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
- Confirm the STS URI points at the real token endpoint, not a gateway frontend.
- Bypass intercepting proxies for the token-exchange host.
- Add a startup connectivity probe that asserts a JSON content-type response.
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
- nil receiver passed to UnmarshalJSON
- failed to create http request: %v
- http status %d, body: %s
- empty accessToken in response (%v)
- empty token_exchange_service_uri in options
AI-assisted analysis of grpc/grpc-go@03255a9237 (2026-08-07).
Data as JSON: /api/errors/07745b9aaf79f5e9.
Report an issue: GitHub.