mastra-ai/mastra · error

Linear ${label} returned no access token.

Error message

Linear ${label} returned no access token.

What it means

`#requestTokens` POSTs to Linear's OAuth token endpoint for either a code exchange or a refresh. If the endpoint returns HTTP 200 but the JSON body has no `access_token`, it throws this error naming the grant type ('token exchange' or 'token refresh'). A 200 without an access token violates the OAuth spec and indicates Linear returned an unexpected payload.

Source

Thrown at mastracode/factory/src/integrations/linear/integration.ts:645

      body: new URLSearchParams({
        ...params,
        client_id: this.#clientId,
        client_secret: this.#clientSecret,
      }),
    });
    if (!res.ok) {
      const err = new Error(`Linear ${label} failed (${res.status})`);
      (err as { status?: number }).status = res.status;
      throw err;
    }
    const body = (await res.json()) as {
      access_token?: string;
      refresh_token?: string;
      expires_in?: number;
      scope?: string;
    };
    if (!body.access_token) {
      throw new Error(`Linear ${label} returned no access token.`);
    }
    return {
      accessToken: body.access_token,
      refreshToken: body.refresh_token ?? null,
      expiresAt: typeof body.expires_in === 'number' ? new Date(Date.now() + body.expires_in * 1000) : null,
      scope: body.scope ?? null,
    };
  }

  // ── GraphQL reads/writes ─────────────────────────────────────────────────

  /** Fetch the workspace (organization) the access token is scoped to. */
  async fetchWorkspace(accessToken: string): Promise<LinearWorkspace> {
    const data = await linearGraphql<{ organization: { name: string; urlKey: string } }>(
      accessToken,
      `query { organization { name urlKey } }`,
    );
    return { name: data.organization.name, urlKey: data.organization.urlKey };

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Retry the grant once — for `refresh_token` grants note Linear rotates tokens, so re-read the stored refresh token first (the integration already reloads before refreshing).
  2. For 'token exchange', restart the OAuth flow: authorization codes are single-use, so a new code via buildAuthorizeUrl.
  3. Log/inspect the raw 200 body to see what was actually returned.
  4. If mocking the token endpoint, make fixtures include `access_token` (plus `refresh_token`, `expires_in`, `scope`).
  5. Check for proxies or middlewares between your server and LINEAR_TOKEN_URL.

Example fix

// test mock before
mockTokenEndpoint = () => ({ status: 200, body: {} });
// after
mockTokenEndpoint = () => ({ status: 200, body: { access_token: 'ltok', refresh_token: 'rtok', expires_in: 7200, scope: 'read comments:create' } });
Defensive patterns

Strategy: retry

Type guard

function isMissingAccessTokenError(err: unknown): boolean {
  return err instanceof Error && err.message.includes('returned no access token.');
}

Try / catch

try {
  const tokens = await integration.exchangeOAuthCode(code, redirectUri);
} catch (err) {
  if (isMissingAccessTokenError(err) && err.message.includes('token exchange')) {
    // Authorization codes are single-use: restart the OAuth flow with a fresh code.
    restartOAuthFlow();
    return;
  }
  throw err;
}

Prevention

When it happens

Trigger: `exchangeOAuthCode` or `refreshAccessToken` receiving a 200 response whose body lacks `access_token` — e.g. an intermediary/proxy returning a 200 with an empty or non-OAuth JSON body, a mocked token endpoint with incomplete fixtures, or a Linear-side anomaly.

Common situations: Test mocks that return 200 with `{}`; gateway/proxy rewrites stripping the body; using an incorrect token URL override pointing at something that answers 200; network appliances returning cached empty responses.

Related errors


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/ed957fa51f174e17. Report an issue: GitHub.