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
- Inspect errorDetails.response to see what Xero actually returned.
- Confirm network egress to identity.xero.com is unproxied/unintercepted.
- 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).
- 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
- Ensure egress to identity.xero.com is not intercepted by a captive proxy.
- Log the raw response body when this fires to diagnose the shape.
- Retry once; persistent failures usually indicate infra interception.
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
- Xero token refresh failed
- MISSING_OAUTH_CREDENTIALS
- MISSING_REFRESH_TOKEN
- XERO_TOKEN_EXCHANGE_FAILED
- Error refreshing access token
AI-assisted analysis of ToolJet/ToolJet@20602a8e10 (2026-08-13).
Data as JSON: /api/errors/ef3c70ef6dff7b1d.
Report an issue: GitHub.