ToolJet/ToolJet · error · OAuthUnauthorizedClientError

Query could not be completed

Error message

Query could not be completed

What it means

In the Google Sheets run() catch block, when authentication_type is NOT 'service_account' and the HTTP response status is 401 or 403, the plugin throws OAuthUnauthorizedClientError (a distinct Error subclass signaling the OAuth token is invalid/expired so ToolJet can prompt re-auth) instead of the generic QueryError. This branch exists specifically to tell the platform the user must re-authorize.

Source

Thrown at plugins/packages/googlesheets/lib/index.ts:182

        const errorRespose = JSON.parse(error.response.body);
        errorDetails = {
          message: errorRespose?.error?.message,
          status: errorRespose?.error?.status,
        };
      }

      if (error.message.replace(/\s+/g, ' ').trim().includes('Unexpected token in JSON')) {
        errorDetails = {
          message: 'Invalid JSON',
        };
      }
      // For OAuth if token is expired or invalid it returns 401 or 403
      // For other authentication types just throw generic error so that user can re-authenticate
      if (
        sourceOptions['authentication_type'] !== 'service_account' &&
        (error?.response?.statusCode === 401 || error?.response?.statusCode === 403)
      ) {
        throw new OAuthUnauthorizedClientError('Query could not be completed', error.message, {
          ...error,
          ...errorDetails,
        });
      }
      throw new QueryError('Query could not be completed', error.message, errorDetails);
    }

    return {
      status: 'ok',
      data: result,
    };
  }

  async refreshToken(sourceOptions) {
    if (!sourceOptions['refresh_token']) {
      throw new QueryError('Query could not be completed', 'Refresh token empty', {});
    }
    const accessTokenUrl = 'https://oauth2.googleapis.com/token';

View on GitHub (pinned to 20602a8e10)

Solutions

  1. Re-authorize the Google Sheets datasource through the ToolJet OAuth flow to obtain a fresh access+refresh token.
  2. Ensure GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET env vars on the ToolJet server match the Google Cloud OAuth app used for the original consent.
  3. Confirm the OAuth consent screen includes the spreadsheets scope; re-consent if scopes changed.
  4. If the issue is only an expired access token, verify refreshToken() (lines 196+) can exchange the refresh_token successfully.

Example fix

// no code fix — user must re-run the OAuth consent flow in the datasource settings
Defensive patterns

Strategy: try-catch

Validate before calling

// No pre-call validation can prevent an expired access token; detect at runtime.
// Proactively refresh before the 1h window: schedule refreshToken() calls.

Type guard

function isOAuthUnauthorized(e: unknown): e is OAuthUnauthorizedClientError { return e instanceof Error && e.name === 'OAuthUnauthorizedClientError'; }

Try / catch

try { await plugin.run(sourceOptions, queryOptions); }
catch (e) {
  if (e instanceof OAuthUnauthorizedClientError) { /* trigger re-consent / refresh flow, then retry once */ }
  else throw e;
}

Prevention

When it happens

Trigger: OAuth access token expired (Google access tokens last ~1h) and a read/append/update/delete/info/list_all call returned 401. The user revoked ToolJet access in their Google account, producing 401/403. The refresh token is invalid or was never persisted. The OAuth app scopes were changed and the existing grant no longer covers spreadsheets.

Common situations: Long-running app sessions past the 1h access-token window where the refresh path failed. User manually revoked the consent. GOOGLE_CLIENT_ID/SECRET env vars changed so refresh fails. App restored from an export where the OAuth grant did not carry over. Migrating between ToolJet instances without re-consent.

Related errors


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