Skip to content
17 changes: 10 additions & 7 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ This project uses [oxlint](https://oxc.rs/docs/guide/usage/linter) and [oxfmt](h
- Use single quotes for strings
- Add trailing commas in multiline structures
- Export types and interfaces alongside implementations
- Declare private members as `#private` identifiers, and use `protected` only for helpers that subclasses call
- Avoid `any` types where possible (though the oxlint rule is disabled)

### Before Committing
Expand Down Expand Up @@ -306,6 +307,7 @@ When adding support for a new API resource:
// src/resource_clients/my_resource.ts
import { ApiClientSubResourceOptions } from '../base/api_client';
import { ResourceClient } from '../base/resource_client';
import * as schemas from '../schemas';

export class MyResourceClient extends ResourceClient {
constructor(options: ApiClientSubResourceOptions) {
Expand All @@ -316,15 +318,15 @@ export class MyResourceClient extends ResourceClient {
}

async get(): Promise<MyResource | undefined> {
return this._get();
return this.getResource(schemas.MyResource());
}

async update(newFields: MyResourceUpdate): Promise<MyResource> {
return this._update(newFields);
return this.updateResource(schemas.MyResource(), newFields);
}

async delete(): Promise<void> {
return this._delete();
return this.deleteResource();
}
}
```
Expand All @@ -335,6 +337,7 @@ export class MyResourceClient extends ResourceClient {
// src/resource_clients/my_resource_collection.ts
import { ApiClientSubResourceOptions } from '../base/api_client';
import { ResourceCollectionClient } from '../base/resource_collection_client';
import * as schemas from '../schemas';
import { PaginatedList } from '../utils';

export class MyResourceCollectionClient extends ResourceCollectionClient {
Expand All @@ -346,11 +349,11 @@ export class MyResourceCollectionClient extends ResourceCollectionClient {
}

async list(options?: MyResourceListOptions): Promise<PaginatedList<MyResource>> {
return this._list(options);
return this.listResources(schemas.ListOfMyResources(), options);
}

async create(resource: MyResourceCreate): Promise<MyResource> {
return this._create(resource);
return this.createResource(schemas.MyResource(), resource);
}
}
```
Expand All @@ -360,11 +363,11 @@ export class MyResourceCollectionClient extends ResourceCollectionClient {
```typescript
// In src/apify_client.ts
myResource(id: string): MyResourceClient {
return new MyResourceClient(this._subResourceOptions({ id }));
return new MyResourceClient({ id, ...this.subClientOptions() });
}

myResources(): MyResourceCollectionClient {
return new MyResourceCollectionClient(this._options());
return new MyResourceCollectionClient(this.subClientOptions());
}
```

Expand Down
33 changes: 33 additions & 0 deletions docs/04_upgrading/upgrading_v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -368,3 +368,36 @@ export HTTPS_PROXY=http://proxy.example.com:3128
```

The same `proxy-agent` upgrade removes the `[DEP0169] DeprecationWarning` about `url.parse()` that Node.js 24 and newer printed on the client's first request.

## Non-public members no longer carry an underscore

Members declared `private` or `protected` had a leading underscore on top of the keyword in v2, and the underscore is gone in v3. Code that calls only the public methods of a client is unaffected. A class that extends one of the client classes and calls a protected helper has to switch to the new name. The base classes below are not exported, so you reach these members by extending a concrete client such as `ActorClient`.

| Class | v2 | v3 |
| --- | --- | --- |
| `ApiClient` | `_url()` | `buildUrl()` |
| `ApiClient` | `_publicUrl()` | `buildPublicUrl()` |
| `ApiClient` | `_params()` | `buildParams()` |
| `ApiClient` | `_subResourceOptions()` | `subResourceOptions()` |
| `ApiClient` | `_toSafeId()` | `toSafeId()` |
| `ApiClient` | `_listPaginatedFromCallback()` | `listPaginatedFromCallback()` |
| `ResourceClient` | `_get()` | `getResource()` |
| `ResourceClient` | `_update()` | `updateResource()` |
| `ResourceClient` | `_delete()` | `deleteResource()` |
| `ResourceClient` | `_waitForFinish()` | `waitForJobFinish()` |
| `ResourceCollectionClient` | `_list()` | `listResources()` |
| `ResourceCollectionClient` | `_listPaginated()` | `listResourcesPaginated()` |
| `ResourceCollectionClient` | `_create()` | `createResource()` |
| `ResourceCollectionClient` | `_getOrCreate()` | `getOrCreateResource()` |
| `RequestQueueClient` | `_batchAddRequests()` | `addRequestBatch()` |
| `RequestQueueClient` | `_batchAddRequestsWithRetries()` | `addRequestBatchWithRetries()` |

A helper got a suffix wherever the bare name would collide with a public method of the same class, which is why `_get()` is now `getResource()` and not `get()`.

<ApiLink to="class/LoggerActorRedirect">`LoggerActorRedirect`</ApiLink> keeps `_log()`, since it overrides the method of that name on the `Logger` base class in `@apify/log`.

The `clientMethod` field on <ApiLink to="class/ApifyApiError">`ApifyApiError`</ApiLink> is parsed from the stack trace, and a public method that delegates to one of these helpers is reported under the helper's name. An error from `client.actor(id).get()` says `ActorClient.getResource`, where v2 said `ActorClient.get`. Adjust anything that matches on these values in your logs.

### Private members are private at runtime

Members that v2 declared `private` are declared with a `#` in v3, so the JavaScript runtime enforces the boundary, where v2 relied on the type checker. Code that reached one of them through a cast, such as `(client.httpClient as any).nodeInitPromise`, throws a `TypeError` in v3. They also don't appear when you spread an instance, iterate `Object.keys()` on it, or pass it to `JSON.stringify()`. The `protected` helpers in the table keep the `protected` keyword, so a subclass can call them.
36 changes: 10 additions & 26 deletions src/apify_api_error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,6 @@ export type { ApifyApiErrorType } from './models.js';
*/
const CLIENT_METHOD_REGEX = /at( async)? ([A-Za-z]+(Collection)?Client)\.([A-Za-z]+) \(/;

/**
* A public method that returns the promise of a shared helper without awaiting it leaves only the helper on
* the stack, so the helper is reported under the method the caller invoked.
* @private
*/
const PUBLIC_METHOD_BY_HELPER: Record<string, string> = {
getResource: 'get',
updateResource: 'update',
deleteResource: 'delete',
waitForJobFinish: 'waitForFinish',
listResources: 'list',
createResource: 'create',
getOrCreateResource: 'getOrCreate',
addRequestBatch: 'batchAddRequests',
addRequestBatchWithRetries: 'batchAddRequests',
};

/**
* An `ApifyApiError` is thrown for successful HTTP requests that reach the API,
* but the API responds with an error response. Typically, those are rate limit
Expand All @@ -53,8 +36,9 @@ export class ApifyApiError extends Error {
override name: string;

/**
* The invoked resource client and the method. Known issue: Sometimes it displays
* as `unknown` because it can't be parsed from a stack trace.
* The resource client and the method parsed from the stack trace, such as `ActorClient.getResource`. A public
* method that delegates to a shared helper of its class is reported under the helper. The value is `unknown`
* when the stack trace cannot be parsed.
*/
clientMethod: string;

Expand Down Expand Up @@ -131,17 +115,17 @@ export class ApifyApiError extends Error {
super(message);

this.name = this.constructor.name;
this.clientMethod = this._extractClientAndMethodFromStack();
this.clientMethod = this.#extractClientAndMethodFromStack();
this.statusCode = response.status;
this.type = type;
this.attempt = attempt;
this.httpMethod = response.config?.method;
this.path = this._safelyParsePathFromResponse(response);
this.path = this.#safelyParsePathFromResponse(response);

const stack = this.stack!;

this.originalStack = stack.slice(stack.indexOf('\n'));
this.stack = this._createApiStack();
this.stack = this.#createApiStack();

this.data = errorData;
}
Expand All @@ -156,7 +140,7 @@ export class ApifyApiError extends Error {
return new ErrorClass(response, attempt);
}

private _safelyParsePathFromResponse(response: AxiosResponse) {
#safelyParsePathFromResponse(response: AxiosResponse) {
const urlString = response.config?.url;
let url;
try {
Expand All @@ -167,10 +151,10 @@ export class ApifyApiError extends Error {
return url.pathname + url.search;
}

private _extractClientAndMethodFromStack() {
#extractClientAndMethodFromStack() {
const match = this.stack!.match(CLIENT_METHOD_REGEX);
if (!match) return 'unknown';
return `${match[2]}.${PUBLIC_METHOD_BY_HELPER[match[4]] ?? match[4]}`;
return `${match[2]}.${match[4]}`;
}

/**
Expand All @@ -187,7 +171,7 @@ export class ApifyApiError extends Error {
* httpMethod: post
* path: /v2/actor-tasks/user~my-task/runs
*/
private _createApiStack() {
#createApiStack() {
const { name, ...props } = this;

const stack = Object.entries(props)
Expand Down
Loading
Loading