ToolJet/ToolJet · error · QueryError

QuickBooksTokenError

Error message

QuickBooksTokenError

What it means

Thrown inside QuickBooks.refreshToken when the POST to TOKEN_URL succeeded (HTTP 200) but the parsed body has no access_token field. Because it is thrown inside the try block, the catch rethrows it verbatim via the `instanceof QueryError` guard. It signals Intuit returned a structurally unexpected success response.

Source

Thrown at marketplace/plugins/quickbooks/lib/index.ts:130

        method: 'post',
        headers: {
          Authorization: this.getBasicAuthHeader(clientId, clientSecret),
          'Content-Type': 'application/x-www-form-urlencoded',
        },
        body: data.toString(),
        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 || refreshTokenValue, // preserve existing if not reissued
        };
      }

      throw new QueryError('QuickBooksTokenError', 'Access token not found in response', { response: result });
    } catch (error: any) {
      if (error instanceof QueryError) throw error;
      const parsed = error?.response?.body || error;
      const errorMessage = typeof parsed === 'object' ? (parsed?.error_description || parsed?.error || JSON.stringify(parsed)) : error?.message;
      console.error('[QuickBooks] Token refresh failed:', errorMessage);
      throw new QueryError('QuickBooksTokenRefreshError', errorMessage, { status: error?.response?.statusCode, response: parsed });
    }
  }

  async run(sourceOptions: any, queryOptions: any, dataSourceId: string): Promise<QueryResult> {
    const accessToken = sourceOptions['access_token'];
    const companyId = sourceOptions['company_id'];

    if (!accessToken) {
      throw new QueryError(
        'Authentication required',
        'No access token found. Please connect to QuickBooks first.',
        { code: 'MISSING_ACCESS_TOKEN' }

View on GitHub (pinned to 20602a8e10)

Solutions

  1. Log the full result object (data.response) to see what Intuit actually returned — if it is HTML, a proxy is intercepting.
  2. Verify the got call uses responseType:'json' and that the Content-Type from Intuit is application/json.
  3. Retry once; transient maintenance pages resolve quickly.
  4. If the schema genuinely changed, update the access_token extraction path in refreshToken.

Example fix

// before
const result = response.body as { access_token?: string; refresh_token?: string };
if (result.access_token) { return {...}; }
throw new QueryError('QuickBooksTokenError', 'Access token not found in response', { response: result });

// after: include raw body + content-type for diagnosis
if (!result.access_token) {
  throw new QueryError('QuickBooksTokenError', 'Access token not found in response', {
    response: result,
    rawBody: response.rawBody?.toString()?.slice(0, 500),
    contentType: response.headers['content-type'],
  });
}
Defensive patterns

Strategy: validation

Validate before calling

function looksLikeIntuitTokenResponse(body: any): boolean {
  return body && typeof body === 'object'
    && (typeof body.access_token === 'string' || typeof body.refresh_token === 'string');
}

Type guard

function isHtmlInterstitial(body: any, headers: any): boolean {
  const ct = headers?.['content-type'] || '';
  return typeof body === 'string' && (ct.includes('text/html') || body.slice(0, 50).toLowerCase().includes('<!doctype'));
}

Try / catch

try {
  return await plugin.refreshToken(sourceOptions, dataSourceId, userId, isAppPublic);
} catch (e) {
  if (e instanceof QueryError && e.message === 'QuickBooksTokenError') {
    // inspect e.data.response; if HTML -> proxy; if schema changed -> patch extraction
  }
  throw e;
}

Prevention

When it happens

Trigger: Intuit changes the token response schema; a proxy returns a 200 with an HTML login/interstitial page that got parses as JSON; responseType mis-parses the body into an object without access_token; the endpoint returned an error-shaped body with a 2xx status.

Common situations: A corporate proxy or captive portal intercepts the HTTPS response with a 200 HTML page. Misconfigured responseType where the body is a string but the code treats it as an object. Intuit maintenance returning a non-standard body.

Related errors


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