Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions docs/02_concepts/04_proxy_management.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -142,20 +142,18 @@ The difference is easy to remember. <ApiLink to="apify/interface/ProxyConfigurat

## Apify Proxy Configuration

With Apify Proxy, you can select specific proxy groups to use, or countries to connect from.
This allows you to get better proxy performance after some initial research.
With Apify Proxy, you can select specific proxy groups to use, or countries to connect from. For even finer control, you can also target a specific subdivision (e.g. a US state) using the `subdivisionCode` option alongside `countryCode`. This allows you to get better proxy performance after some initial research.

```javascript
const proxyConfiguration = await Actor.createProxyConfiguration({
groups: ['RESIDENTIAL'],
countryCode: 'US',
subdivisionCode: 'CA',
});
const proxyUrl = proxyConfiguration.newUrl();
```

Now your crawlers will use only Residential proxies from the US. Note that you must first get access
to a proxy group before you are able to use it. You can find your available proxy groups
in the [proxy dashboard](https://console.apify.com/proxy).
Now your crawlers will use only Residential proxies from California, US. The `subdivisionCode` accepts a 1–3 character [ISO 3166-2](https://en.wikipedia.org/wiki/ISO_3166-2) code (e.g. `CA` for California) and currently only works for the United States (`countryCode: 'US'`). Note that you must first get access to a proxy group before you are able to use it. You can find your available proxy groups in the [proxy dashboard](https://console.apify.com/proxy).

## Inspecting current proxy in Crawlers

Expand Down
65 changes: 57 additions & 8 deletions src/proxy_configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const MAX_SESSION_ID_LENGTH = 50;
const CHECK_ACCESS_REQUEST_TIMEOUT_MILLIS = 4_000;
const CHECK_ACCESS_MAX_ATTEMPTS = 2;
const COUNTRY_CODE_REGEX = /^[A-Z]{2}$/;
// ISO 3166-2 subdivision codes are 1–3 uppercase alphanumeric characters, e.g. 'CA' (California), 'NSW' (New South Wales), '9' (Wien, AT-9)
const SUBDIVISION_CODE_REGEX = /^[A-Z0-9]{1,3}$/;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would highly recommend add there a comment that explains that 1-3 range for an ISO standard that expects only 2 chars 😄

This was something that confused me.


export interface ProxyConfigurationOptions extends CoreProxyConfigurationOptions {
/**
Expand Down Expand Up @@ -46,16 +48,29 @@ export interface ProxyConfigurationOptions extends CoreProxyConfigurationOptions

/**
* Same option as `groups` which can be used to
* configurate the proxy by UI input schema. You should use the `groups` option in your crawler code.
* configure the proxy by UI input schema. You should use the `groups` option in your crawler code.
*/
apifyProxyGroups?: string[];

/**
* Same option as `countryCode` which can be used to
* configurate the proxy by UI input schema. You should use the `countryCode` option in your crawler code.
* configure the proxy by UI input schema. You should use the `countryCode` option in your crawler code.
*/
apifyProxyCountry?: string;

/**
* If set, all proxied requests will use IP addresses geolocated to the specified subdivision (e.g. US state).
* Requires `countryCode` to be set. The value must follow the ISO 3166-2 subdivision code format,
* e.g. `'CA'` for California when `countryCode` is `'US'`.
*/
subdivisionCode?: string;

/**
* Same option as `subdivisionCode` which can be used to
* configure the proxy by UI input schema. You should use the `subdivisionCode` option in your crawler code.
*/
apifyProxySubdivision?: string;

/**
* Multiple different ProxyConfigurationOptions stratified into tiers. Crawlee crawlers will switch between those tiers
* based on the blocked request statistics.
Expand Down Expand Up @@ -123,6 +138,12 @@ export interface ProxyInfo extends CoreProxyInfo {
*/
countryCode?: string;

/**
* If set, all proxied requests use IP addresses geolocated to the specified subdivision (e.g. US state).
* ISO 3166-2 subdivision code, e.g. `'CA'` when `countryCode` is `'US'`.
*/
subdivisionCode?: string;

/**
* User's password for the proxy. By default, it is taken from the `APIFY_PROXY_PASSWORD`
* environment variable, which is automatically set by the system when running the Actors
Expand Down Expand Up @@ -167,6 +188,7 @@ export interface ProxyInfo extends CoreProxyInfo {
export class ProxyConfiguration extends CoreProxyConfiguration {
private groups: string[];
private countryCode?: string;
private subdivisionCode?: string;
private password?: string;
private hostname: string;
private port?: number;
Expand All @@ -187,7 +209,7 @@ export class ProxyConfiguration extends CoreProxyConfiguration {
});
ow(
rest,
ow.object.exactShape({
ow.object.partialShape({
groups: ow.optional.array.ofType(
ow.string.matches(APIFY_PROXY_VALUE_REGEX),
),
Expand All @@ -197,6 +219,12 @@ export class ProxyConfiguration extends CoreProxyConfiguration {
countryCode: ow.optional.string.matches(COUNTRY_CODE_REGEX),
apifyProxyCountry:
ow.optional.string.matches(COUNTRY_CODE_REGEX),
subdivisionCode: ow.optional.string.matches(
SUBDIVISION_CODE_REGEX,
),
apifyProxySubdivision: ow.optional.string.matches(
SUBDIVISION_CODE_REGEX,
),
password: ow.optional.string,
tieredProxyUrls: ow.optional.array.ofType(
ow.array.ofType(ow.string),
Expand All @@ -210,6 +238,8 @@ export class ProxyConfiguration extends CoreProxyConfiguration {
apifyProxyGroups = [],
countryCode,
apifyProxyCountry,
subdivisionCode,
apifyProxySubdivision,
password = config.get('proxyPassword'),
tieredProxyConfig,
tieredProxyUrls,
Expand All @@ -226,13 +256,20 @@ export class ProxyConfiguration extends CoreProxyConfiguration {

const groupsToUse = groups.length ? groups : apifyProxyGroups;
const countryCodeToUse = countryCode || apifyProxyCountry;
const subdivisionCodeToUse = subdivisionCode || apifyProxySubdivision;
const hostname = config.get('proxyHostname');
const port = config.get('proxyPort');

if (subdivisionCodeToUse && !countryCodeToUse) {
throw new Error(
'ProxyConfiguration: "subdivisionCode" requires "countryCode" to be set.',
);
}

// Validation
if (
(proxyUrls || newUrlFunction) &&
(groupsToUse.length || countryCodeToUse)
(groupsToUse.length || countryCodeToUse || subdivisionCodeToUse)
) {
this._throwCannotCombineCustomWithApify();
}
Expand All @@ -241,6 +278,7 @@ export class ProxyConfiguration extends CoreProxyConfiguration {

this.groups = groupsToUse;
this.countryCode = countryCodeToUse;
this.subdivisionCode = subdivisionCodeToUse;
this.password = password;
this.hostname = hostname!;
this.port = port;
Expand Down Expand Up @@ -328,7 +366,14 @@ export class ProxyConfiguration extends CoreProxyConfiguration {
const proxyInfo = await super.newProxyInfo(sessionId, options);
if (!proxyInfo) return proxyInfo;

const { groups, countryCode, password, port, hostname } = (
const {
groups,
countryCode,
subdivisionCode,
password,
port,
hostname,
} = (
this.usesApifyProxy ? this : new URL(proxyInfo.url)
) as ProxyConfiguration;

Expand All @@ -337,6 +382,7 @@ export class ProxyConfiguration extends CoreProxyConfiguration {
sessionId,
groups,
countryCode,
subdivisionCode,
// this.password is not encoded, but the password from the URL will be, we need to normalize
password: this.usesApifyProxy
? (password ?? '')
Expand Down Expand Up @@ -413,7 +459,7 @@ export class ProxyConfiguration extends CoreProxyConfiguration {
*/
protected _getUsername(sessionId?: string): string {
let username;
const { groups, countryCode } = this;
const { groups, countryCode, subdivisionCode } = this;
const parts: string[] = [];

if (groups && groups.length) {
Expand All @@ -422,7 +468,9 @@ export class ProxyConfiguration extends CoreProxyConfiguration {
if (sessionId) {
parts.push(`session-${sessionId}`);
}
if (countryCode) {
if (subdivisionCode) {
parts.push(`country-${countryCode}_${subdivisionCode}`);
} else if (countryCode) {
parts.push(`country-${countryCode}`);
}

Expand Down Expand Up @@ -545,7 +593,8 @@ export class ProxyConfiguration extends CoreProxyConfiguration {
throw new Error(
'Cannot combine custom proxies with Apify Proxy! ' +
'It is not allowed to set "options.proxyUrls" or "options.newUrlFunction" combined with ' +
'"options.groups" or "options.apifyProxyGroups" and "options.countryCode" or "options.apifyProxyCountry".',
'"options.groups", "options.apifyProxyGroups", "options.countryCode", "options.apifyProxyCountry", ' +
'"options.subdivisionCode" or "options.apifyProxySubdivision".',
);
}
}
77 changes: 77 additions & 0 deletions test/apify/proxy_configuration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,83 @@ describe('ProxyConfiguration', () => {
expect(proxyConfiguration.countryCode).toStrictEqual(apifyProxyCountry);
});

test('subdivisionCode should produce country-US_XX in proxy URL', async () => {
const proxyConfiguration = new ProxyConfiguration({
groups,
countryCode: 'US',
subdivisionCode: 'CA',
password,
});

expect(await proxyConfiguration.newUrl(sessionId)).toBe(
'http://groups-GROUP1+GROUP2,session-538909250932,country-US_CA:test12345@proxy.apify.com:8000',
);
});

test('subdivisionCode without session should produce correct URL', async () => {
const proxyConfiguration = new ProxyConfiguration({
groups,
countryCode: 'US',
subdivisionCode: 'NY',
password,
});

expect(await proxyConfiguration.newUrl()).toBe(
'http://groups-GROUP1+GROUP2,country-US_NY:test12345@proxy.apify.com:8000',
);
});

test('apifyProxySubdivision UI input schema should work', async () => {
const proxyConfiguration = new ProxyConfiguration({
apifyProxyGroups: groups,
apifyProxyCountry: 'US',
apifyProxySubdivision: 'TX',
password,
});

// @ts-expect-error private property
expect(proxyConfiguration.subdivisionCode).toBe('TX');
expect(await proxyConfiguration.newUrl()).toBe(
'http://groups-GROUP1+GROUP2,country-US_TX:test12345@proxy.apify.com:8000',
);
});

test('subdivisionCode without countryCode should throw', () => {
expect(
() => new ProxyConfiguration({ subdivisionCode: 'CA', password }),
).toThrow(
'ProxyConfiguration: "subdivisionCode" requires "countryCode" to be set.',
);
expect(
() =>
new ProxyConfiguration({
apifyProxySubdivision: 'CA',
password,
}),
).toThrow(
'ProxyConfiguration: "subdivisionCode" requires "countryCode" to be set.',
);
});

test('should throw on invalid subdivisionCode', () => {
expect(
() =>
new ProxyConfiguration({
countryCode: 'US',
subdivisionCode: 'ca',
password,
}),
).toThrow();
expect(
() =>
new ProxyConfiguration({
countryCode: 'US',
subdivisionCode: 'California',
password,
}),
).toThrow();
});

test('should throw on invalid arguments structure', () => {
// Group value
const invalidGroups = ['GROUP1*'];
Expand Down