Skip to content

Commit 53dc481

Browse files
authored
feat: surface approval URL when calling unapproved Actor (#1111)
tldr; the run Actor API returns an `approvalUrl` when an Actor needs full account access — we were swallowing it and only printing the bare error message. ## Before Run: Calling Actor apify/puppeteer-scraper (YJCnS9qogi9XxDgLB) Error: This Actor requires full access to your account. You must approve its permissions before running it. ## After <img width="912" height="113" alt="image" src="https://github.com/user-attachments/assets/c9d4376f-a0dd-4746-8842-7f72eabcb2ae" /> Handled in the same `catch` block as `record-not-found` in `run-on-cloud.ts`. Affects `apify call`, `apify actors call`, `apify actors start`, `apify task run` and `apify tasks run`. ## Test plan - [x] Manually triggered against `apify/puppeteer-scraper`, approval URL surfaces correctly - [ ] Add a vitest case for the `full-permission-actor-not-approved` branch
1 parent f606fe5 commit 53dc481

2 files changed

Lines changed: 68 additions & 0 deletions

File tree

src/lib/commands/run-on-cloud.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,17 @@ export async function* runActorOrTaskOnCloud(apifyClient: ApifyClient, options:
8888
throw new Error(`${type} ${actorOrTaskData.userFriendlyId} (${actorOrTaskData.id}) not found!`);
8989
}
9090

91+
if (err.type === 'full-permission-actor-not-approved') {
92+
const approvalUrl: string | undefined = err.data?.approvalUrl;
93+
const lines = [
94+
`${type} ${actorOrTaskData.userFriendlyId} requires full access to your Apify account and has not been approved yet.`,
95+
];
96+
if (approvalUrl) {
97+
lines.push('', `Approve here: ${chalk.blue(approvalUrl)}`);
98+
}
99+
throw new Error(lines.join('\n'));
100+
}
101+
91102
throw err;
92103
}
93104

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type { ApifyClient } from 'apify-client';
2+
3+
import { runActorOrTaskOnCloud } from '../../../src/lib/commands/run-on-cloud.js';
4+
5+
const fakeClientThatThrows = (error: unknown) =>
6+
({
7+
actor: () => ({
8+
start: async () => {
9+
throw error;
10+
},
11+
}),
12+
}) as unknown as ApifyClient;
13+
14+
const callAndCatch = async (apiError: unknown) => {
15+
const iterator = runActorOrTaskOnCloud(fakeClientThatThrows(apiError), {
16+
actorOrTaskData: { id: 'abc', userFriendlyId: 'apify/test-actor' },
17+
runOptions: {},
18+
type: 'Actor',
19+
silent: true,
20+
});
21+
22+
try {
23+
for await (const _ of iterator) {
24+
// drain
25+
}
26+
} catch (err) {
27+
return err as Error;
28+
}
29+
30+
throw new Error('Expected runActorOrTaskOnCloud to throw');
31+
};
32+
33+
describe('runActorOrTaskOnCloud', () => {
34+
it('surfaces approval URL when Actor requires full account access', async () => {
35+
const approvalUrl = 'https://console.apify.com/actors/abc?approvePermissions=true';
36+
const apiError = Object.assign(new Error('This Actor requires full access to your account.'), {
37+
type: 'full-permission-actor-not-approved',
38+
data: { approvalUrl },
39+
});
40+
41+
const err = await callAndCatch(apiError);
42+
43+
expect(err.message).toMatch(/has not been approved yet/);
44+
expect(err.message).toContain(approvalUrl);
45+
});
46+
47+
it('falls back to bare message when API response has no approvalUrl', async () => {
48+
const apiError = Object.assign(new Error('This Actor requires full access to your account.'), {
49+
type: 'full-permission-actor-not-approved',
50+
});
51+
52+
const err = await callAndCatch(apiError);
53+
54+
expect(err.message).toMatch(/has not been approved yet/);
55+
expect(err.message).not.toMatch(/Approve here/);
56+
});
57+
});

0 commit comments

Comments
 (0)