ToolJet/ToolJet · error · QueryError

Access token not found in Xero response

Error message

Access token not found in Xero response

What it means

Thrown by the refresh path (index.ts:180) when the token endpoint returned an HTTP success (got did not throw) but the parsed body lacks access_token. It is a defensive check: Xero answered 2xx but the payload was not a usable token response. errorDetails carries the raw response and statusCode; thrown as QueryError 'XeroTokenError'.

Source

Thrown at marketplace/plugins/xero/lib/index.ts:180

        method: 'post',
        form: data,
        responseType: 'json',
      });

      const result = response.body as { access_token?: string; refresh_token?: string };

      if (result.access_token) {
        return {
          access_token: result.access_token,
          refresh_token: result.refresh_token,
        };
      } else {
        const errorMessage = 'Access token not found in Xero response';
        const errorDetails = {
          response: result,
          status: response.statusCode,
        };
        throw new QueryError('XeroTokenError', errorMessage, errorDetails);
      }
    } catch (error: any) {
      let parsed: any;

      try {
        parsed = error?.response?.body ? JSON.parse(error.response.body) : error;
      } catch {
        parsed = error?.response?.body || error;
      }

      const errorMessage =
        parsed?.Title || parsed?.error_description || parsed?.error || error?.message || 'Xero token refresh failed';

      const errorDetails = {
        status: error?.response?.statusCode || null,
        response: parsed,
      };
      throw new QueryError('XeroTokenRefreshError', errorMessage, errorDetails);

View on GitHub (pinned to 20602a8e10)

Solutions

  1. Inspect errorDetails.response to see what Xero actually returned.
  2. Confirm network egress to identity.xero.com is unproxied/unintercepted.
  3. Retry; if persistent, verify the grant_type/refresh_token sent were valid (a bad refresh_token usually yields non-2xx, but some proxies normalise to 200).
  4. Check for an Xero API status incident.

Example fix

// before: (no caller-side fix; server-side response is malformed)

// after: surface the raw response and surface a clearer error
if (!result.access_token) {
  throw new QueryError('XeroTokenError', 'No access_token in 2xx response', { response: result, status: response.statusCode });
}
Defensive patterns

Strategy: try-catch

Type guard

function hasAccessToken(body: any): boolean {
  return typeof body?.access_token === 'string' && body.access_token.length > 0;
}

Try / catch

try {
  const tokens = await refreshToken(sourceOptions, userId);
  if (!tokens.access_token) throw new Error('Xero returned 2xx with no access_token');
} catch (e) {
  if (e instanceof QueryError && /access token not found/i.test(e.description)) {
    logger.error({ response: e.data?.response }, 'Xero returned a non-token 2xx body — check for proxy interception');
  }
  throw e;
}

Prevention

When it happens

Trigger: Xero identity endpoint returning 200 with an unexpected/empty body, a man-in-the-middle or proxy returning a 200 HTML page, an API contract change where the field was renamed, or a partial/malformed JSON body parsed without access_token.

Common situations: Corporate proxy intercepting identity.xero.com with a captive page, Xero temporarily serving a degraded response, an incorrect endpoint URL returning success for unrelated content.

Related errors


AI-assisted analysis of ToolJet/ToolJet@20602a8e10 (2026-08-13). Data as JSON: /api/errors/ef3c70ef6dff7b1d. Report an issue: GitHub.