Skip to content

Commit

Permalink
feat: ignore proxy configuration locally if no valid token or passwor…
Browse files Browse the repository at this point in the history
…d is found

We use the proxy configuration in all the templates, as we want users to run behind a proxy all the times.
This PR changes the internal mechanism to gracefully ignore the proxy configuration locally when we don't
see a valid token or proxy password. This way, new users can run their actors locally without being logged in.

Closes #262
  • Loading branch information
B4nan committed Jan 8, 2024
1 parent 3ec01d5 commit b849f07
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 15 deletions.
7 changes: 5 additions & 2 deletions packages/apify/src/actor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -873,9 +873,12 @@ export class Actor<Data extends Dictionary = Dictionary> {
}

const proxyConfiguration = new ProxyConfiguration(options, this.config);
await proxyConfiguration.initialize();

return proxyConfiguration;
if (await proxyConfiguration.initialize()) {
return proxyConfiguration;
}

return undefined;
}

/**
Expand Down
59 changes: 48 additions & 11 deletions packages/apify/src/proxy_configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
ProxyConfiguration as CoreProxyConfiguration,
} from '@crawlee/core';
import { gotScraping } from '@crawlee/utils';
import type { UserProxy } from 'apify-client';
import ow from 'ow';

import { Actor } from './actor';
Expand Down Expand Up @@ -213,11 +214,13 @@ export class ProxyConfiguration extends CoreProxyConfiguration {
* You should use the {@apilink createProxyConfiguration} function to create a pre-initialized
* `ProxyConfiguration` instance instead of calling this manually.
*/
async initialize(): Promise<void> {
async initialize(): Promise<boolean> {
if (this.usesApifyProxy) {
await this._setPasswordIfToken();
await this._checkAccess();
return this._checkAccess();
}

return true;
}

/**
Expand Down Expand Up @@ -315,7 +318,19 @@ export class ProxyConfiguration extends CoreProxyConfiguration {
const token = this.config.get('token');

if (token) {
const { proxy } = await Actor.apifyClient.user().get();
let proxy: UserProxy;

try {
const user = await Actor.apifyClient.user().get();
proxy = user.proxy!;
} catch (error) {
if (Actor.isAtHome()) {
throw error;
} else {
this.log.warning(`Failed to fetch user data based on token, disabling proxy.`, { error });
return;
}
}
const { password } = proxy!;

if (this.password) {
Expand All @@ -329,8 +344,14 @@ export class ProxyConfiguration extends CoreProxyConfiguration {
}

if (!this.password) {
throw new Error(`Apify Proxy password must be provided using options.password or the "${APIFY_ENV_VARS.PROXY_PASSWORD}" environment variable. `
+ `If you add the "${APIFY_ENV_VARS.TOKEN}" environment variable, the password will be automatically inferred.`);
if (Actor.isAtHome()) {
throw new Error(`Apify Proxy password must be provided using options.password or the "${APIFY_ENV_VARS.PROXY_PASSWORD}" environment variable. `
+ `If you add the "${APIFY_ENV_VARS.TOKEN}" environment variable, the password will be automatically inferred.`);
} else {
this.log.warning(`No proxy password or token detected, running without proxy. To use Apify Proxy locally, `
+ `provide options.password or "${APIFY_ENV_VARS.PROXY_PASSWORD}" environment variable. `
+ `If you add the "${APIFY_ENV_VARS.TOKEN}" environment variable, the password will be automatically inferred.`);
}
}
}

Expand All @@ -339,17 +360,33 @@ export class ProxyConfiguration extends CoreProxyConfiguration {
* If the check can not be made, it only prints a warning and allows the program to continue. This is to
* prevent program crashes caused by short downtimes of Proxy.
*/
protected async _checkAccess(): Promise<void> {
protected async _checkAccess(): Promise<boolean> {
const status = await this._fetchStatus();
if (status) {
const { connected, connectionError, isManInTheMiddle } = status;
this.isManInTheMiddle = isManInTheMiddle;

if (!connected) this._throwApifyProxyConnectionError(connectionError);
} else {
if (!status) {
this.log.warning('Apify Proxy access check timed out. Watch out for errors with status code 407. '
+ 'If you see some, it most likely means you don\'t have access to either all or some of the proxies you\'re trying to use.');
return true;
}

const { connected, connectionError, isManInTheMiddle } = status;
this.isManInTheMiddle = isManInTheMiddle;

if (connected) {
return true;
}

// Throw only on the platform, locally we just print a warning and run requests without the proxy.
// This is because the user might not have set up things correctly yet.
// It still fails on the platform, where we don't want to allow this behavior.
if (Actor.isAtHome()) {
this._throwApifyProxyConnectionError(connectionError);
} else {
this.log.warning(connectionError);
return false;
}

return false;
}

/**
Expand Down
4 changes: 2 additions & 2 deletions test/apify/actor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -532,15 +532,15 @@ describe('Actor', () => {
};

const initializeSpy = vitest.spyOn(ProxyConfiguration.prototype, 'initialize');
initializeSpy.mockImplementationOnce(async () => {});
initializeSpy.mockImplementationOnce(async () => true);
await expect(Actor.createProxyConfiguration(proxyConfiguration)).resolves.toBeInstanceOf(ProxyConfiguration);
expect(initializeSpy).toBeCalledTimes(1);
});

test('createProxyConfiguration should create ProxyConfiguration', async () => {
const sdk = new Actor();
const initializeSpy = vitest.spyOn(ProxyConfiguration.prototype, 'initialize');
initializeSpy.mockImplementationOnce(async () => {});
initializeSpy.mockImplementationOnce(async () => true);
await sdk.createProxyConfiguration();
expect(initializeSpy).toBeCalledTimes(1);
});
Expand Down
10 changes: 10 additions & 0 deletions test/apify/proxy_configuration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ afterEach(() => {
delete process.env[APIFY_ENV_VARS.PROXY_PASSWORD];
delete process.env[APIFY_ENV_VARS.PROXY_HOSTNAME];
delete process.env[APIFY_ENV_VARS.PROXY_STATUS_URL];
delete process.env[APIFY_ENV_VARS.IS_AT_HOME];
});

describe('ProxyConfiguration', () => {
Expand Down Expand Up @@ -406,9 +407,11 @@ describe('Actor.createProxyConfiguration()', () => {
gotScrapingSpy.mockRestore();
});

// TODO: test that on platform we throw but locally we only print warning
test('should throw missing password', async () => {
delete process.env[APIFY_ENV_VARS.PROXY_PASSWORD];
delete process.env[APIFY_ENV_VARS.TOKEN];
process.env[APIFY_ENV_VARS.IS_AT_HOME] = '1';

const status = { connected: true };

Expand All @@ -421,11 +424,15 @@ describe('Actor.createProxyConfiguration()', () => {
await expect(Actor.createProxyConfiguration()).rejects.toThrow('Apify Proxy password must be provided');

gotScrapingSpy.mockRestore();

delete process.env[APIFY_ENV_VARS.IS_AT_HOME];
await expect(Actor.createProxyConfiguration()).resolves.toBeInstanceOf(ProxyConfiguration);
});

test('should throw when group is not available', async () => {
delete process.env[APIFY_ENV_VARS.PROXY_PASSWORD];
process.env.APIFY_TOKEN = '123456789';
process.env[APIFY_ENV_VARS.IS_AT_HOME] = '1';
const connectionError = 'Invalid username: proxy group "GROUP2"; not found or not accessible.';
const status = { connected: false, connectionError };
const getUserSpy = vitest.spyOn(UserClient.prototype, 'get');
Expand All @@ -436,6 +443,9 @@ describe('Actor.createProxyConfiguration()', () => {

gotScrapingSpy.mockRestore();
getUserSpy.mockRestore();

delete process.env[APIFY_ENV_VARS.IS_AT_HOME];
await expect(Actor.createProxyConfiguration({ groups })).resolves.toBeInstanceOf(ProxyConfiguration);
});

test('should not throw when access check is unresponsive', async () => {
Expand Down

0 comments on commit b849f07

Please sign in to comment.