larksuite/cli · error

TAT response missing access_token (HTTP %d)

Error message

TAT response missing access_token (HTTP %d)

What it means

The TAT endpoint returned a 2xx response whose parsed JSON contains neither an access_token nor an OAuth error field (code==0 and error=="") — a malformed success. The library treats it as ambiguous/untyped because the HTTP status says success but no token was delivered.

Source

Thrown at internal/credential/tat_fetch.go:126

	if result.Code == 0 && result.AccessToken != "" {
		return result.AccessToken, nil
	}

	// Transient/server-side failures stay untyped so probe callers stay silent and
	// retryers can back off; only deterministic client rejections are typed. Covers
	// 5xx and the OAuth transient error strings (server_error,
	// temporarily_unavailable, slow_down). HTTP 429 was already returned above
	// as a typed rate-limit error with retry guidance and an upstream delay when available.
	if resp.StatusCode >= 500 ||
		result.Error == "server_error" || result.Error == "temporarily_unavailable" ||
		result.Error == "slow_down" {
		return "", fmt.Errorf("TAT endpoint transient failure (HTTP %d, code=%d, error=%q): %s",
			resp.StatusCode, result.Code, result.Error, result.ErrorDescription)
	}

	// A 2xx with neither token nor error is a malformed success — ambiguous, untyped.
	if result.Code == 0 && result.Error == "" {
		return "", fmt.Errorf("TAT response missing access_token (HTTP %d)", resp.StatusCode)
	}

	// Prefer the OAuth error_description; fall back to the legacy Lark `msg` so a
	// gateway-level {code, msg} response (carrying no OAuth fields) still yields a
	// non-empty typed message instead of a bare "API error: [code]".
	desc := result.ErrorDescription
	if desc == "" {
		desc = result.Msg
	}
	return "", classifyTATResponseCode(result.Code, result.Error, desc, string(brand), appID)
}

func tatRetryAfterSeconds(header http.Header) int {
	for _, name := range []string{"X-Ogw-Ratelimit-Reset", "Retry-After"} {
		seconds, err := strconv.Atoi(strings.TrimSpace(header.Get(name)))
		if err == nil && seconds > 0 {
			return seconds
		}

View on GitHub (pinned to 7fd6ef3c07)

Solutions

  1. Log the full 2xx response body and headers to identify what actually came back instead of an access_token.
  2. Verify you are calling the current, documented TAT/OAuth token endpoint path and version.
  3. Retry once in case of an intermittent upstream issue; if reproducible, report the malformed response shape to the API provider.
  4. Check whether a proxy or response-rewriting middleware is stripping or altering the JSON payload.

Example fix

// before (assumes token present whenever err==nil)
// after (fail fast and dump body when token is missing)
if resp.StatusCode == 200 && !strings.Contains(string(body), "access_token") {
    log.Fatalf("unexpected 2xx TAT body: %s", string(body))
}
Defensive patterns

Strategy: fallback

Type guard

func isMalformedSuccess(err error) bool {
    return strings.Contains(err.Error(), "TAT response missing access_token")
}

Try / catch

token, err := credential.FetchTAT(ctx, cfg)
if err != nil {
    if strings.Contains(err.Error(), "TAT response missing access_token") {
        // HTTP said 2xx but no token: capture body, retry once, then escalate
        log.Printf("malformed TAT success, retrying once")
        token, err = credential.FetchTAT(ctx, cfg)
    }
    if err != nil { return err }
}

Prevention

When it happens

Trigger: A 2xx response with {code:0} but no access_token field, or an empty/other-shaped JSON body that unmarshals into zero fields, from the token endpoint.

Common situations: Upstream schema change on the token endpoint (fields renamed/removed); a misconfigured endpoint (e.g. wrong path) returning a benign empty JSON success; an intermediary caching or rewriting the response body.

Related errors


AI-assisted analysis of larksuite/cli@7fd6ef3c07 (2026-09-04). Data as JSON: /api/errors/d31ea22a7be91629. Report an issue: GitHub.