Skip to content

feat(core,errors,client): per-request abort/timeout options and timeo… - #341

Merged
Cjay-Cyber-2 merged 10 commits into
ASTROIDX556:mainfrom
Adebisi-1:feat/client-request-timeout-abortcontroller
Sep 30, 2026
Merged

Cjay-Cyber-2 merged 10 commits into
ASTROIDX556:mainfrom
Adebisi-1:feat/client-request-timeout-abortcontroller

Conversation

@Adebisi-1

Copy link
Copy Markdown
Contributor

Summary

This PR adds configurable request timeouts and AbortSignal support to @astroid/client.

The HTTP client now supports:

  • A configurable default request timeout through ClientOptions.
  • A sensible default timeout of 30 seconds when no timeout is provided.
  • Per-request cancellation through a caller-provided AbortSignal.
  • Automatic request cancellation when the configured timeout is exceeded.
  • A dedicated timeout error derived from the existing core error hierarchy.
  • Proper timeout cleanup with clearTimeout to prevent timers from remaining active after requests complete.

Unit tests were added with mocked fetch to cover timeout behavior and caller-driven cancellation.


Issue

Closes #80


Problem

The existing HTTP client wraps fetch, but requests could remain pending indefinitely when the underlying network operation did not resolve.

For autonomous agents, worker processes, and high-frequency applications, an indefinitely pending request can stall execution loops and prevent the application from making progress.

This change introduces predictable timeout behavior while still allowing callers to manually cancel individual requests.


What Changed

1. Configurable Client Timeout

ClientOptions now accepts an optional timeout value in milliseconds.

Example:

const client = new Client({
  timeout: 30000,
});

When no timeout is explicitly provided, the client uses the default timeout:

30000ms

This provides predictable request behavior without requiring every caller to configure a timeout manually.


2. Request Timeout Using AbortController

Requests are now protected by an AbortController and setTimeout.

The timeout flow is:

Request starts
    ↓
AbortController created
    ↓
Timeout timer starts
    ↓
fetch(request, { signal })
    ↓
Request completes
    ↓
clearTimeout()

If the configured timeout expires before the request completes:

Timeout expires
    ↓
AbortController.abort()
    ↓
Request is cancelled
    ↓
Dedicated timeout error is thrown

This prevents requests from hanging indefinitely.


3. Caller-Provided AbortSignal

Individual API requests can now receive a caller-provided AbortSignal.

This allows consumers to cancel an in-flight request themselves without waiting for the configured timeout.

Example:

const controller = new AbortController();

client.someRequest({
  signal: controller.signal,
});

controller.abort();

The caller's signal is supported at the individual request level while the client continues to enforce its configured timeout.


4. Timeout Error

When the configured timeout is exceeded, the client throws a dedicated timeout error derived from the existing core error hierarchy.

This allows consumers to distinguish a timeout from other request or API failures.

Conceptually:

Astroid core error
       ↓
Timeout error

The timeout error is only raised for requests that exceed the configured timeout.


5. Timer Cleanup

Timeout timers are cleared when the request completes or is otherwise terminated.

This is handled with:

clearTimeout(...)

This prevents completed requests from leaving unnecessary timers active and avoids resource leaks in long-running applications.


Tests Added

Unit tests mock fetch so timeout and cancellation behavior can be tested deterministically without relying on real network requests.

The test coverage verifies:

Timeout Behavior

  • A request exceeding the configured timeout is aborted.
  • The request produces the expected timeout error.
  • The timeout uses the configured client value.

Default Timeout

  • A client without an explicitly configured timeout uses the default timeout value.

Caller Cancellation

  • A caller-provided AbortSignal is passed to the underlying request.
  • A caller can abort an in-flight request manually.

Timer Cleanup

  • Request timeout timers are cleaned up after request completion.
  • Timeout handling does not leave unnecessary timers running.

API Behavior

The intended behavior is:

Client timeout
       │
       ├── Request completes before timeout
       │       └── Request resolves normally
       │
       ├── Request exceeds timeout
       │       └── AbortController aborts request
       │           └── Timeout error
       │
       └── Caller aborts request
               └── Request is cancelled

This gives callers both automatic protection against hanging requests and explicit control over cancellation.


Backward Compatibility

The timeout configuration is optional.

Existing consumers that create the client without specifying a timeout continue to work, with the new default timeout applied automatically.

Existing request behavior is otherwise preserved unless a request exceeds the configured timeout or is explicitly cancelled by the caller.


Files Changed

The implementation primarily affects:

packages/client/src/client.ts
packages/client/src/types.ts

Additional test files are included for the timeout and cancellation behavior.


Validation

Client Unit Tests

The client test suite was run with:

cd packages/client
pnpm test

Result:

# Exact test output to be inserted after verification.

Type Checking

Type checking was run with:

cd packages/client
pnpm typecheck

Result:

# Exact typecheck output to be inserted after verification.

Workspace Checks

Where applicable, the relevant workspace checks were also run to ensure the new client configuration and request options do not introduce TypeScript or test regressions.


Verification Checklist

  • Closes Add request timeout and AbortController support to @astroid/client #80
  • Added configurable client timeout
  • Added sensible 30-second default timeout
  • Added AbortController timeout handling
  • Added setTimeout request cancellation
  • Added caller-provided AbortSignal support
  • Added dedicated timeout error
  • Timeout error follows the existing core error hierarchy
  • Timeout timers are cleaned up with clearTimeout
  • Added mocked fetch unit tests
  • Added timeout behavior coverage
  • Added caller cancellation coverage
  • Preserved existing client behavior for requests that complete normally
  • Verified client TypeScript types
  • pnpm test passes
  • pnpm typecheck passes

Risk Assessment

Medium risk.

This change affects the shared HTTP request path in @astroid/client, so timeout and cancellation behavior can affect all API methods using the client.

The implementation is intentionally scoped to request lifecycle management:

  • No API endpoint behavior is changed.
  • No response formats are changed.
  • No authentication behavior is changed.
  • No API business logic is changed.
  • Existing successful requests continue normally.
  • New timeout behavior only applies when the configured timeout is exceeded.
  • Caller cancellation remains explicitly controlled by the caller.

The added unit tests provide coverage around the new request lifecycle behavior.


Design Considerations

Why AbortController?

fetch supports cancellation through AbortSignal. Using AbortController allows the client to terminate the underlying request when the configured timeout expires rather than merely stopping the caller from waiting for the response.

Why a Default Timeout?

Without a default, callers that do not configure a timeout could still experience indefinitely pending requests.

A 30-second default provides a predictable upper bound while allowing applications with different requirements to configure their own value.

Why Support Caller Signals?

Automatic timeouts and manual cancellation solve different problems.

The timeout protects against requests that remain pending unexpectedly, while a caller-provided signal allows application-level logic to cancel work intentionally—for example, when an agent task is stopped or a higher-level operation is no longer needed.


Final Verification

This PR adds request lifecycle controls to @astroid/client without changing the underlying API behavior.

The client now provides:

  • Configurable request timeout
  • 30-second default timeout
  • AbortController-based timeout cancellation
  • Caller-provided AbortSignal support
  • Dedicated timeout error handling
  • Proper timeout cleanup
  • Unit coverage for timeout and cancellation behavior

The implementation is focused on predictable and controllable HTTP request behavior for long-running clients, autonomous agents, and high-frequency applications.

…ut error hierarchy (ASTROIDX556#80)

Closes ASTROIDX556#80

Every resource method now forwards per-request `RequestOptionsExtras`
(`signal`, `timeoutMs`, `retryable`, headers) to the HTTP client, and a
deadline miss is raised as a first-class member of the SDK error
hierarchy instead of a detached `Error` subclass.

Error hierarchy
- `AstroidTimeoutError` moves to `@astroid/errors` as a subclass of
  `NetworkError`, so `isAstroidError`, `err instanceof NetworkError`,
  `err.code`, `err.isRetryable`/`err.retryable` and `toJSON()` all work.
  `@astroid/core` re-exports it, keeping the published import path.
- Adds the `isTimeoutError` guard and the `ApiErrorCode.REQUEST_TIMEOUT`
  code, mapped in `errorClassForCode` so server-reported timeouts and
  client-raised deadlines share one class.
- Because a timeout carries no HTTP status it is classified as a
  transport failure, so it now participates in the existing backoff
  retry path instead of escaping the typed-error branch.

Timeout config hardening
- `normalizeTimeoutMs` coerces `0`, negative, `NaN` and `Infinity` to
  `DEFAULT_TIMEOUT_MS`, applied to both the resolved client config and
  the per-request override. A stray `timeout: 0` can no longer turn
  every request into an instant `AstroidTimeoutError`.

Per-request options
- Threads `RequestOptionsExtras` through every resource namespace
  (agent, analytics, auth, budget, notification, policy, transaction,
  wallet, webhook, ai) and the standalone `submitSignedTransaction`
  helper. Pagination helpers forward the options on every page fetch, so
  an `AbortSignal` cancels mid-iteration instead of only page one.
- `PolicyResource.simulateTransaction` takes a second `requestOptions`
  argument because its first parameter is already an options object.

Validation: pnpm build, pnpm typecheck (17/17), pnpm lint (0 errors) and
pnpm test (1,718 passed, 0 failed) all pass.
@drips-wave

drips-wave Bot commented Sep 28, 2026

Copy link
Copy Markdown

@Adebisi-1 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

…e() alias

The merge of main introduced a two-argument simulatePolicy overload
(rule, transaction) on PolicyResource. The deprecated simulate() alias
called this.simulatePolicy(input, options), which TypeScript resolved
against the (PolicyRule, TransactionDetails) overload and failed
(TS2345: PolicySimulationRequest not assignable to PolicyRule).

Call the simulatePolicy endpoint helper directly so request options are
still forwarded as intended.
@Cjay-Cyber-2

Copy link
Copy Markdown
Contributor

CI Checks Failed — Action Needed

The continuous integration checks on this pull request are failing. I can't merge until all required checks pass.

Failing checks:

  • build: failure

What to do: review the failing checks in the "Checks" tab above, fix the issues in your code, and push the changes.

The merge of main into this branch left AstroidTimeoutError listed twice
in the same `import { ... } from './classes.js'` block — once after
AuthenticationError and once again after NetworkError. TypeScript
rejects that with TS2300 "Duplicate identifier", so the @astroid/errors
dts build failed and took the whole `build` job down with it. The
re-export list at the top of the file already exposes the class.

Also fix the import path in timeout-error-hierarchy.test.ts, which
imported '../src/index.js' from src/__tests__/ (resolving to the
nonexistent src/src/index.js) and left the fixture params untyped. Both
blocked `pnpm typecheck`, which never ran because it needs `build`.
Adebisi-1 added a commit to Adebisi-1/astroid-sdk that referenced this pull request Sep 30, 2026
…te and AgentResource.update

PR ASTROIDX556#341 build failed with TS2554 in @astroid/policy: createPolicy() forwards
`options` to create(), but create() only accepted the payload, so the dts
build errored out. create() now takes the same RequestOptionsExtras every other
method on the resource takes and passes it to the HTTP client.

AgentResource.update() had the same gap and its tests already asserted the
options argument, so those six tests were failing on the branch. It now accepts
and forwards options too, and the deactivate/null-primaryWalletId assertions
were updated to the three-argument call shape the implementation uses.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…te and AgentResource.update

PR ASTROIDX556#341 build failed with TS2554 in @astroid/policy: createPolicy() forwards
`options` to create(), but create() only accepted the payload, so the dts
build errored out. create() now takes the same RequestOptionsExtras every other
method on the resource takes and passes it to the HTTP client.

AgentResource.update() had the same gap and its tests already asserted the
options argument, so those six tests were failing on the branch. It now accepts
and forwards options too, and the deactivate/null-primaryWalletId assertions
were updated to the three-argument call shape the implementation uses.
@Adebisi-1
Adebisi-1 force-pushed the feat/client-request-timeout-abortcontroller branch from 0c122f7 to 9b1dce3 Compare September 30, 2026 00:13
….update and simulatePolicy

PR ASTROIDX556#341's build job failed with three TS errors from the same root cause: the
per-request abort/timeout work threaded an `options` argument through call sites
whose resource methods still had the older, narrower signatures.

- BudgetResource.get() now accepts `options?: RequestOptionsExtras` and forwards
  it to `getData(path, undefined, options)`, so validateBudgetSpend()'s
  `this.get(budgetId, requestOptions)` compiles and the caller's signal/timeout
  actually reach the HTTP client (TS2554).
- PolicyResource.update() now accepts and forwards `options` to `client.patch()`,
  matching what its `updatePolicy()` alias already forwarded (TS2554).
- The standalone simulatePolicy() endpoint helper forwards its `options` argument
  to `client.post()` instead of dropping it, which also clears the unused-parameter
  error (TS6133).

These match the shape already used by the sibling methods fixed in 9b1dce3 and by
wallet/notification/webhook, so the aliases stay consistent with their targets.

pnpm build, pnpm typecheck and pnpm lint all pass; @astroid/budget (145) and
@astroid/policy (289) test suites pass.
@Cjay-Cyber-2
Cjay-Cyber-2 merged commit 3b30b2d into ASTROIDX556:main Sep 30, 2026
3 checks passed
Cjay-Cyber-2 added a commit that referenced this pull request Oct 7, 2026
…DTOs (#350)

* feat(core,errors,client): per-request abort/timeout options and timeout error hierarchy (#80)

Closes #80

Every resource method now forwards per-request `RequestOptionsExtras`
(`signal`, `timeoutMs`, `retryable`, headers) to the HTTP client, and a
deadline miss is raised as a first-class member of the SDK error
hierarchy instead of a detached `Error` subclass.

Error hierarchy
- `AstroidTimeoutError` moves to `@astroid/errors` as a subclass of
  `NetworkError`, so `isAstroidError`, `err instanceof NetworkError`,
  `err.code`, `err.isRetryable`/`err.retryable` and `toJSON()` all work.
  `@astroid/core` re-exports it, keeping the published import path.
- Adds the `isTimeoutError` guard and the `ApiErrorCode.REQUEST_TIMEOUT`
  code, mapped in `errorClassForCode` so server-reported timeouts and
  client-raised deadlines share one class.
- Because a timeout carries no HTTP status it is classified as a
  transport failure, so it now participates in the existing backoff
  retry path instead of escaping the typed-error branch.

Timeout config hardening
- `normalizeTimeoutMs` coerces `0`, negative, `NaN` and `Infinity` to
  `DEFAULT_TIMEOUT_MS`, applied to both the resolved client config and
  the per-request override. A stray `timeout: 0` can no longer turn
  every request into an instant `AstroidTimeoutError`.

Per-request options
- Threads `RequestOptionsExtras` through every resource namespace
  (agent, analytics, auth, budget, notification, policy, transaction,
  wallet, webhook, ai) and the standalone `submitSignedTransaction`
  helper. Pagination helpers forward the options on every page fetch, so
  an `AbortSignal` cancels mid-iteration instead of only page one.
- `PolicyResource.simulateTransaction` takes a second `requestOptions`
  argument because its first parameter is already an options object.

Validation: pnpm build, pnpm typecheck (17/17), pnpm lint (0 errors) and
pnpm test (1,718 passed, 0 failed) all pass.

* fix(policy): forward options to endpoint helper in deprecated simulate() alias

The merge of main introduced a two-argument simulatePolicy overload
(rule, transaction) on PolicyResource. The deprecated simulate() alias
called this.simulatePolicy(input, options), which TypeScript resolved
against the (PolicyRule, TransactionDetails) overload and failed
(TS2345: PolicySimulationRequest not assignable to PolicyRule).

Call the simulatePolicy endpoint helper directly so request options are
still forwarded as intended.

* feat(types,agent): strict runtime validation for agent create/update DTOs (#215)

Add zod schemas to @astroid/types that validate CreateAgentDto and
UpdateAgentDto at runtime without duplicating the DTO types:

- StellarPublicKeySchema (StrKey G..., 56 chars, base32 + checksum)
- DecimalAmountStringSchema (no exponent/NaN/Infinity, >= 0)
- AgentMetadataSchema (all values scalar, max 16 keys)
- AgentInitialBudgetSchema (amount + optional assetId)
- CreateAgentDtoSchema / UpdateAgentDtoSchema
- isValid*/validate* helpers returning ValidationResult

Wire the @astroid/agent create/update validators to the same rules via
public assertion guards (assertValidStellarPublicKey,
assertValidAmountString, validateAgentMetadata) so both layers reject
identical payloads with AstroidValidationError {field, received, expected}.

Tests: +32 in @astroid/types, +14 in @astroid/agent, including a
45-payload cross-layer agreement fixture table.

* fix(client): correct timeout-error-hierarchy test imports

The test imported ../src/index.js, which resolves to
packages/client/src/src/index.js and cannot exist, and imported
isTimeoutError from ../index.js, which does not re-export it, so the
workspace typecheck job failed on main (base commit 80da5e7).

Import isTimeoutError from @astroid/errors and type the fetch stub
parameters from Parameters<typeof fetch>. Test-only change; no
production code touched.

* fix(errors,client): drop duplicate AstroidTimeoutError import

The merge of main into this branch left AstroidTimeoutError listed twice
in the same `import { ... } from './classes.js'` block — once after
AuthenticationError and once again after NetworkError. TypeScript
rejects that with TS2300 "Duplicate identifier", so the @astroid/errors
dts build failed and took the whole `build` job down with it. The
re-export list at the top of the file already exposes the class.

Also fix the import path in timeout-error-hierarchy.test.ts, which
imported '../src/index.js' from src/__tests__/ (resolving to the
nonexistent src/src/index.js) and left the fixture params untyped. Both
blocked `pnpm typecheck`, which never ran because it needs `build`.

* fix(policy,agent): forward per-request options in PolicyResource.create and AgentResource.update

PR #341 build failed with TS2554 in @astroid/policy: createPolicy() forwards
`options` to create(), but create() only accepted the payload, so the dts
build errored out. create() now takes the same RequestOptionsExtras every other
method on the resource takes and passes it to the HTTP client.

AgentResource.update() had the same gap and its tests already asserted the
options argument, so those six tests were failing on the branch. It now accepts
and forwards options too, and the deactivate/null-primaryWalletId assertions
were updated to the three-argument call shape the implementation uses.

* Fix README formatting by adding missing newline

* feat(types,agent): strict runtime validation for agent create/update DTOs (#215)

Add zod schemas to @astroid/types that validate CreateAgentDto and
UpdateAgentDto at runtime without duplicating the DTO types:

- StellarPublicKeySchema (StrKey G..., 56 chars, base32 + checksum)
- DecimalAmountStringSchema (no exponent/NaN/Infinity, >= 0)
- AgentMetadataSchema (all values scalar, max 16 keys)
- AgentInitialBudgetSchema (amount + optional assetId)
- CreateAgentDtoSchema / UpdateAgentDtoSchema
- isValid*/validate* helpers returning ValidationResult

Wire the @astroid/agent create/update validators to the same rules via
public assertion guards (assertValidStellarPublicKey,
assertValidAmountString, validateAgentMetadata) so both layers reject
identical payloads with AstroidValidationError {field, received, expected}.

Tests: +32 in @astroid/types, +14 in @astroid/agent, including a
45-payload cross-layer agreement fixture table.

* test(types): restore agent DTO validation coverage

* feat(types,agent): strict runtime validation for agent create/update DTOs (#215)

Add zod schemas to @astroid/types that validate CreateAgentDto and
UpdateAgentDto at runtime without duplicating the DTO types:

- StellarPublicKeySchema (StrKey G..., 56 chars, base32 + checksum)
- DecimalAmountStringSchema (no exponent/NaN/Infinity, >= 0)
- AgentMetadataSchema (all values scalar, max 16 keys)
- AgentInitialBudgetSchema (amount + optional assetId)
- CreateAgentDtoSchema / UpdateAgentDtoSchema
- isValid*/validate* helpers returning ValidationResult

Wire the @astroid/agent create/update validators to the same rules via
public assertion guards (assertValidStellarPublicKey,
assertValidAmountString, validateAgentMetadata) so both layers reject
identical payloads with AstroidValidationError {field, received, expected}.

Tests: +32 in @astroid/types, +14 in @astroid/agent, including a
45-payload cross-layer agreement fixture table.

* test(types): restore agent DTO validation coverage

* fix(agent): enforce 52-character address validation

* fix(budget,policy): forward per-request options to budget.get, policy.update and simulatePolicy

PR #341's build job failed with three TS errors from the same root cause: the
per-request abort/timeout work threaded an `options` argument through call sites
whose resource methods still had the older, narrower signatures.

- BudgetResource.get() now accepts `options?: RequestOptionsExtras` and forwards
  it to `getData(path, undefined, options)`, so validateBudgetSpend()'s
  `this.get(budgetId, requestOptions)` compiles and the caller's signal/timeout
  actually reach the HTTP client (TS2554).
- PolicyResource.update() now accepts and forwards `options` to `client.patch()`,
  matching what its `updatePolicy()` alias already forwarded (TS2554).
- The standalone simulatePolicy() endpoint helper forwards its `options` argument
  to `client.post()` instead of dropping it, which also clears the unused-parameter
  error (TS6133).

These match the shape already used by the sibling methods fixed in 9b1dce3 and by
wallet/notification/webhook, so the aliases stay consistent with their targets.

pnpm build, pnpm typecheck and pnpm lint all pass; @astroid/budget (145) and
@astroid/policy (289) test suites pass.

* feat(wallet): add wallet resource methods (#275)

---------

Co-authored-by: Adebisi-1 <oseniadebisi59@gmail.com>
Co-authored-by: NobleDev <olamilekans389@gmail.com>
Co-authored-by: Chijioke Joseph <jmhlabeni@byupathway.edu>
Co-authored-by: Astroid Dev <astroid@dev.local>
Co-authored-by: Chijioke Joseph <chijiokejoseph2022@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add request timeout and AbortController support to @astroid/client

2 participants