Skip to content

Commit ba170e6

Browse files
l2yshoclaude
andcommitted
fix(auth): only fail the identity lookup when the token itself is refused
Resolving the identity from the APIFY_TOKEN account put a network call inside getLocalUserInfo(), which threw on any failure. `apify run` and `mcp install` call it and need no network, so `APIFY_TOKEN=<token> apify run` broke offline or on any API hiccup — the command from the original report. HTTP 401/403/409 mean the token itself was refused and still fail loudly, with the status code and a mention of permissions in the message (403 is a valid token without the required rights, which the old "is it still valid?" wording got wrong). Everything else degrades to no identity, leaving the existing "not logged in" handling to deal with it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 5b764bf commit ba170e6

2 files changed

Lines changed: 45 additions & 6 deletions

File tree

src/lib/utils.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,15 @@ async function fetchActiveUserInfo(token: string): Promise<AuthJSON> {
191191
activeUserInfo = { token, info: await client.user('me').get() };
192192
} catch (err) {
193193
cliDebugPrint('[fetchActiveUserInfo] error getting user info', { error: err });
194-
throw new Error(`The token in ${APIFY_ENV_VARS.TOKEN} was rejected by the Apify API. Is it still valid?`);
194+
// The token itself being refused is worth failing on. Anything else (offline, API hiccup) must not
195+
// break commands that work locally — `apify run` needs no network, so it degrades to no identity.
196+
const { statusCode } = err as { statusCode?: number };
197+
if (statusCode && [401, 403, 409].includes(statusCode)) {
198+
throw new Error(
199+
`The token in ${APIFY_ENV_VARS.TOKEN} was refused by the Apify API (HTTP ${statusCode}). Check that it is valid and has the required permissions.`,
200+
);
201+
}
202+
return {};
195203
}
196204
}
197205

test/local/lib/credentials.test.ts

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import {
1616
} from '../../../src/lib/credentials.js';
1717
import { getApifyClientOptions, getLocalUserInfo, getLoggedClient } from '../../../src/lib/utils.js';
1818

19+
/** `user('me').get()` failures for the mock below to simulate, keyed by the token used. */
20+
const userFetchFailures = new Map<string, { statusCode?: number }>();
21+
1922
// Stubs out the `user('me').get()` round-trip so getLoggedClient() can be tested without the API.
2023
vi.mock('apify-client', () => {
2124
class ApifyClient {
@@ -25,11 +28,16 @@ vi.mock('apify-client', () => {
2528
}
2629
user() {
2730
return {
28-
get: async () => ({
29-
id: `id_for_${this.token}`,
30-
username: `user_for_${this.token}`,
31-
proxy: { password: `pw_for_${this.token}` },
32-
}),
31+
get: async () => {
32+
const failure = userFetchFailures.get(this.token!);
33+
if (failure) throw Object.assign(new Error('simulated API failure'), failure);
34+
35+
return {
36+
id: `id_for_${this.token}`,
37+
username: `user_for_${this.token}`,
38+
proxy: { password: `pw_for_${this.token}` },
39+
};
40+
},
3341
};
3442
}
3543
}
@@ -72,6 +80,7 @@ describe('credentials', () => {
7280
vitest.stubEnv('APIFY_TOKEN', undefined);
7381
keyringStore.clear();
7482
keyringFailures.clear();
83+
userFetchFailures.clear();
7584
__resetCredentialsForTests();
7685
});
7786

@@ -319,6 +328,28 @@ describe('credentials', () => {
319328

320329
expect(await getLocalUserInfo()).toMatchObject({ username: 'user_for_env_tok_b', id: 'id_for_env_tok_b' });
321330
});
331+
332+
// `apify run` needs no network, so an unreachable API must not stop it — only a refused token is fatal.
333+
it.each([[undefined], [500], [502]])(
334+
'degrades to no identity when the identity lookup fails with statusCode %s',
335+
async (statusCode) => {
336+
const token = `env_tok_soft_${statusCode}`;
337+
vitest.stubEnv('APIFY_DISABLE_KEYRING', '1');
338+
vitest.stubEnv('APIFY_TOKEN', token);
339+
userFetchFailures.set(token, { statusCode });
340+
341+
expect(await getLocalUserInfo()).toEqual({ token });
342+
},
343+
);
344+
345+
it.each([[401], [403], [409]])('throws when the API refuses the APIFY_TOKEN with %i', async (statusCode) => {
346+
const token = `env_tok_refused_${statusCode}`;
347+
vitest.stubEnv('APIFY_DISABLE_KEYRING', '1');
348+
vitest.stubEnv('APIFY_TOKEN', token);
349+
userFetchFailures.set(token, { statusCode });
350+
351+
await expect(getLocalUserInfo()).rejects.toThrow(`refused by the Apify API (HTTP ${statusCode})`);
352+
});
322353
});
323354

324355
// Precedence: explicit token arg (e.g. --token) > APIFY_TOKEN env var > stored login token.

0 commit comments

Comments
 (0)