diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index ffdf743d..8f617854 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -2,11 +2,11 @@ ## [Unreleased] +### Added + ### Changed - test: automatically deploys trap canister if it doesn't exist yet during e2e - -### Added - fix: handle v3 traps correctly, pulling the reject_code and message from the certificate in the error response like v2. Example trap error message: ```txt @@ -19,6 +19,7 @@ AgentError: Call failed: "Reject message": "foo" ``` - feat: the `UpdateCallRejected` error now exposes `reject_code: ReplicaRejectCode`, `reject_message: string`, and `error_code?: string` properties directly on the error object. +- fix: recalculates body to use a fresh `Expiry` when polling for `read_state` requests. This prevents the request from exceeding the `maximum_ingress_expiry` when the replica is slow to respond. ## [2.1.2] - 2024-09-30 - fix: revert https://github.com/dfinity/agent-js/pull/923 allow option to set agent replica time diff --git a/packages/agent/src/agent/http/index.ts b/packages/agent/src/agent/http/index.ts index b6c47bdf..f3aa840e 100644 --- a/packages/agent/src/agent/http/index.ts +++ b/packages/agent/src/agent/http/index.ts @@ -42,6 +42,7 @@ import { Ed25519PublicKey } from '../../public_key'; import { decodeTime } from '../../utils/leb'; import { ObservableLog } from '../../observable'; import { BackoffStrategy, BackoffStrategyFactory, ExponentialBackoff } from '../../polling/backoff'; +import { DEFAULT_INGRESS_EXPIRY_DELTA_IN_MSECS } from '../../constants'; export * from './transforms'; export { Nonce, makeNonce } from './types'; @@ -54,9 +55,6 @@ export enum RequestStatusResponseStatus { Done = 'done', } -// Default delta for ingress expiry is 5 minutes. -const DEFAULT_INGRESS_EXPIRY_DELTA_IN_MSECS = 5 * 60 * 1000; - // Root public key for the IC, encoded as hex export const IC_ROOT_KEY = '308182301d060d2b0601040182dc7c0503010201060c2b0601040182dc7c05030201036100814' + @@ -779,7 +777,6 @@ export class HttpAgent implements Agent { const requestId = await requestIdOf(request); - // TODO: remove this any. This can be a Signed or UnSigned request. // eslint-disable-next-line @typescript-eslint/no-explicit-any let transformedRequest: HttpAgentRequest = await this._transform({ request: { @@ -946,9 +943,8 @@ export class HttpAgent implements Agent { } const sender = id?.getPrincipal() || Principal.anonymous(); - // TODO: remove this any. This can be a Signed or UnSigned request. // eslint-disable-next-line @typescript-eslint/no-explicit-any - const transformedRequest: any = await this._transform({ + const transformedRequest = await this._transform({ request: { method: 'POST', headers: { @@ -979,7 +975,14 @@ export class HttpAgent implements Agent { const canister = typeof canisterId === 'string' ? Principal.fromText(canisterId) : canisterId; const transformedRequest = request ?? (await this.createReadStateRequest(fields, identity)); - const body = cbor.encode(transformedRequest.body); + + // With read_state, we should always use a fresh expiry, even beyond the point where the initial request would have expired + const bodyWithAdjustedExpiry = { + ...transformedRequest.body, + ingress_expiry: new Expiry(DEFAULT_INGRESS_EXPIRY_DELTA_IN_MSECS), + }; + + const body = cbor.encode(bodyWithAdjustedExpiry); this.log.print( `fetching "/api/v2/canister/${canister}/read_state" with request:`, diff --git a/packages/agent/src/constants.ts b/packages/agent/src/constants.ts new file mode 100644 index 00000000..b7182105 --- /dev/null +++ b/packages/agent/src/constants.ts @@ -0,0 +1,2 @@ +// Default delta for ingress expiry is 5 minutes. +export const DEFAULT_INGRESS_EXPIRY_DELTA_IN_MSECS = 5 * 60 * 1000; diff --git a/packages/agent/src/polling/index.ts b/packages/agent/src/polling/index.ts index ccd399eb..1518992e 100644 --- a/packages/agent/src/polling/index.ts +++ b/packages/agent/src/polling/index.ts @@ -1,11 +1,12 @@ import { Principal } from '@dfinity/principal'; -import { Agent, RequestStatusResponseStatus } from '../agent'; +import { Agent, Expiry, RequestStatusResponseStatus } from '../agent'; import { Certificate, CreateCertificateOptions, lookupResultToBuffer } from '../certificate'; import { RequestId } from '../request_id'; import { toHex } from '../utils/buffer'; export * as strategy from './strategy'; import { defaultStrategy } from './strategy'; +import { DEFAULT_INGRESS_EXPIRY_DELTA_IN_MSECS } from '../constants'; export { defaultStrategy } from './strategy'; export type PollStrategy = ( canisterId: Principal, @@ -38,6 +39,10 @@ export async function pollForResponse( }> { const path = [new TextEncoder().encode('request_status'), requestId]; const currentRequest = request ?? (await agent.createReadStateRequest?.({ paths: [path] })); + + // Use a fresh expiry for the readState call. + currentRequest.body.content.ingress_expiry = new Expiry(DEFAULT_INGRESS_EXPIRY_DELTA_IN_MSECS); + const state = await agent.readState(canisterId, { paths: [path] }, undefined, currentRequest); if (agent.rootKey == null) throw new Error('Agent root key not initialized before polling'); const cert = await Certificate.create({