Skip to content

Commit 07b36fb

Browse files
authored
fix: presigned resource urls shouldn't follow baseUrl (#745)
Closes #744
1 parent ec6e638 commit 07b36fb

File tree

7 files changed

+86
-4
lines changed

7 files changed

+86
-4
lines changed

src/apify_client.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ const DEFAULT_TIMEOUT_SECS = 360;
4242
export class ApifyClient {
4343
baseUrl: string;
4444

45+
publicBaseUrl: string;
46+
4547
token?: string;
4648

4749
stats: Statistics;
@@ -55,6 +57,7 @@ export class ApifyClient {
5557
options,
5658
ow.object.exactShape({
5759
baseUrl: ow.optional.string,
60+
publicBaseUrl: ow.optional.string,
5861
maxRetries: ow.optional.number,
5962
minDelayBetweenRetriesMillis: ow.optional.number,
6063
requestInterceptors: ow.optional.array,
@@ -66,6 +69,7 @@ export class ApifyClient {
6669

6770
const {
6871
baseUrl = 'https://api.apify.com',
72+
publicBaseUrl = 'https://api.apify.com',
6973
maxRetries = 8,
7074
minDelayBetweenRetriesMillis = 500,
7175
requestInterceptors = [],
@@ -75,6 +79,10 @@ export class ApifyClient {
7579

7680
const tempBaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, baseUrl.length - 1) : baseUrl;
7781
this.baseUrl = `${tempBaseUrl}/v2`;
82+
const tempPublicBaseUrl = publicBaseUrl.endsWith('/')
83+
? publicBaseUrl.slice(0, publicBaseUrl.length - 1)
84+
: publicBaseUrl;
85+
this.publicBaseUrl = `${tempPublicBaseUrl}/v2`;
7886
this.token = token;
7987
this.stats = new Statistics();
8088
this.logger = logger.child({ prefix: 'ApifyClient' });
@@ -93,6 +101,7 @@ export class ApifyClient {
93101
private _options() {
94102
return {
95103
baseUrl: this.baseUrl,
104+
publicBaseUrl: this.publicBaseUrl,
96105
apifyClient: this,
97106
httpClient: this.httpClient,
98107
};
@@ -347,6 +356,8 @@ export class ApifyClient {
347356
export interface ApifyClientOptions {
348357
/** @default https://api.apify.com */
349358
baseUrl?: string;
359+
/** @default https://api.apify.com */
360+
publicBaseUrl?: string;
350361
/** @default 8 */
351362
maxRetries?: number;
352363
/** @default 500 */

src/base/api_client.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { HttpClient } from '../http_client';
44
/** @private */
55
export interface ApiClientOptions {
66
baseUrl: string;
7+
publicBaseUrl: string;
78
resourcePath: string;
89
apifyClient: ApifyClient;
910
httpClient: HttpClient;
@@ -25,6 +26,8 @@ export abstract class ApiClient {
2526

2627
baseUrl: string;
2728

29+
publicBaseUrl: string;
30+
2831
resourcePath: string;
2932

3033
url: string;
@@ -36,11 +39,12 @@ export abstract class ApiClient {
3639
params?: Record<string, unknown>;
3740

3841
constructor(options: ApiClientOptions) {
39-
const { baseUrl, apifyClient, httpClient, resourcePath, id, params = {} } = options;
42+
const { baseUrl, publicBaseUrl, apifyClient, httpClient, resourcePath, id, params = {} } = options;
4043

4144
this.id = id;
4245
this.safeId = id && this._toSafeId(id);
4346
this.baseUrl = baseUrl;
47+
this.publicBaseUrl = publicBaseUrl;
4448
this.resourcePath = resourcePath;
4549
this.url = id ? `${baseUrl}/${resourcePath}/${this.safeId}` : `${baseUrl}/${resourcePath}`;
4650
this.apifyClient = apifyClient;
@@ -51,6 +55,7 @@ export abstract class ApiClient {
5155
protected _subResourceOptions<T>(moreOptions?: T): BaseOptions & T {
5256
const baseOptions: BaseOptions = {
5357
baseUrl: this._url(),
58+
publicBaseUrl: this.publicBaseUrl,
5459
apifyClient: this.apifyClient,
5560
httpClient: this.httpClient,
5661
params: this._params(),
@@ -62,6 +67,13 @@ export abstract class ApiClient {
6267
return path ? `${this.url}/${path}` : this.url;
6368
}
6469

70+
protected _publicUrl(path?: string): string {
71+
const url = this.id
72+
? `${this.publicBaseUrl}/${this.resourcePath}/${this.safeId}`
73+
: `${this.publicBaseUrl}/${this.resourcePath}`;
74+
return path ? `${url}/${path}` : url;
75+
}
76+
6577
protected _params<T>(endpointParams?: T): Record<string, unknown> {
6678
return { ...this.params, ...endpointParams };
6779
}
@@ -74,6 +86,7 @@ export abstract class ApiClient {
7486

7587
export interface BaseOptions {
7688
baseUrl: string;
89+
publicBaseUrl: string;
7790
apifyClient: ApifyClient;
7891
httpClient: HttpClient;
7992
params: Record<string, unknown>;

src/resource_clients/actor.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ export class ActorClient extends ResourceClient {
180180

181181
return new BuildClient({
182182
baseUrl: this.apifyClient.baseUrl,
183+
publicBaseUrl: this.apifyClient.publicBaseUrl,
183184
httpClient: this.httpClient,
184185
apifyClient: this.apifyClient,
185186
id,

src/resource_clients/dataset.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ export class DatasetClient<
199199

200200
const { expiresInSecs, ...queryOptions } = options;
201201

202-
let createdItemsPublicUrl = new URL(this._url('items'));
202+
let createdItemsPublicUrl = new URL(this._publicUrl('items'));
203203

204204
if (dataset?.urlSigningSecretKey) {
205205
const signature = createStorageContentSignature({

src/resource_clients/key_value_store.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ export class KeyValueStoreClient extends ResourceClient {
9696

9797
const store = await this.get();
9898

99-
const recordPublicUrl = new URL(this._url(`records/${key}`));
99+
const recordPublicUrl = new URL(this._publicUrl(`records/${key}`));
100100

101101
if (store?.urlSigningSecretKey) {
102102
const signature = createHmacSignature(store.urlSigningSecretKey, key);
@@ -134,7 +134,7 @@ export class KeyValueStoreClient extends ResourceClient {
134134

135135
const { expiresInSecs, ...queryOptions } = options;
136136

137-
let createdPublicKeysUrl = new URL(this._url('keys'));
137+
let createdPublicKeysUrl = new URL(this._publicUrl('keys'));
138138

139139
if (store?.urlSigningSecretKey) {
140140
const signature = createStorageContentSignature({

test/datasets.test.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,24 @@ describe('Dataset methods', () => {
329329
});
330330

331331
describe('createItemsPublicUrl()', () => {
332+
it.each([
333+
['https://custom.public.url/', 'custom.public.url'],
334+
[undefined, 'api.apify.com'],
335+
])('respects publicBaseUrl client option (%s)', async (publicBaseUrl, expectedHostname) => {
336+
const datasetId = 'dataset-id';
337+
client = new ApifyClient({
338+
baseUrl,
339+
publicBaseUrl,
340+
...DEFAULT_OPTIONS,
341+
});
342+
343+
const res = await client.dataset(datasetId).createItemsPublicUrl();
344+
345+
const url = new URL(res);
346+
expect(url.hostname).toBe(expectedHostname);
347+
expect(url.pathname).toBe(`/v2/datasets/${datasetId}/items`);
348+
});
349+
332350
it('should include a signature in the URL when the caller has permission to access the signing secret key', async () => {
333351
const datasetId = 'id-with-secret-key';
334352
const res = await client.dataset(datasetId).createItemsPublicUrl();

test/key_value_stores.test.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,26 @@ describe('Key-Value Store methods', () => {
555555
});
556556

557557
describe('getRecordPublicUrl()', () => {
558+
it.each([
559+
['https://custom.public.url/', 'custom.public.url'],
560+
[undefined, 'api.apify.com'],
561+
])('respects publicBaseUrl client option (%s)', async (publicBaseUrl, expectedHostname) => {
562+
const storeId = 'some-id';
563+
const key = 'some-key';
564+
565+
client = new ApifyClient({
566+
baseUrl,
567+
publicBaseUrl,
568+
...DEFAULT_OPTIONS,
569+
});
570+
571+
const res = await client.keyValueStore(storeId).getRecordPublicUrl(key);
572+
573+
const url = new URL(res);
574+
expect(url.hostname).toBe(expectedHostname);
575+
expect(url.pathname).toBe(`/v2/key-value-stores/${storeId}/records/${key}`);
576+
});
577+
558578
it('should include a signature in the URL when the caller has permission to access the signing secret key', async () => {
559579
const storeId = 'id-with-secret-key';
560580
const key = 'some-key';
@@ -577,6 +597,25 @@ describe('Key-Value Store methods', () => {
577597
});
578598

579599
describe('createKeysPublicUrl()', () => {
600+
it.each([
601+
['https://custom.public.url/', 'custom.public.url'],
602+
[undefined, 'api.apify.com'],
603+
])('respects publicBaseUrl client option (%s)', async (publicBaseUrl, expectedHostname) => {
604+
const storeId = 'some-id';
605+
606+
client = new ApifyClient({
607+
baseUrl,
608+
publicBaseUrl,
609+
...DEFAULT_OPTIONS,
610+
});
611+
612+
const res = await client.keyValueStore(storeId).createKeysPublicUrl();
613+
614+
const url = new URL(res);
615+
expect(url.hostname).toBe(expectedHostname);
616+
expect(url.pathname).toBe(`/v2/key-value-stores/${storeId}/keys`);
617+
});
618+
580619
it('should include a signature in the URL when the caller has permission to access the signing secret key', async () => {
581620
const storeId = 'id-with-secret-key';
582621
const res = await client.keyValueStore(storeId).createKeysPublicUrl();

0 commit comments

Comments
 (0)