Skip to content

feat(did): add optional fetch timeout to the did:web resolver - #129

Open
EfeDurmaz16 wants to merge 3 commits into
agentcommercekit:mainfrom
EfeDurmaz16:did-resolver-fetch-timeout
Open

feat(did): add optional fetch timeout to the did:web resolver#129
EfeDurmaz16 wants to merge 3 commits into
agentcommercekit:mainfrom
EfeDurmaz16:did-resolver-fetch-timeout

Conversation

@EfeDurmaz16

@EfeDurmaz16 EfeDurmaz16 commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

What

Adds an optional timeout (ms) to DidWebResolverOptions / getResolver for
the did:web resolver. When set, the resolver passes
AbortSignal.timeout(timeout) to the underlying fetch.

Why

Resolving a did:web DID fetches the host named in the DID. Today
fetchDidDocumentAtUrl calls fetch(url, { mode: "cors" }) with no
AbortSignal, so an unresponsive or slow host hangs the resolver
indefinitely. The resolver's own header comment says it exists for "additional
checks and more control for fetching and resolution", so a bounded fetch fits
its purpose. This is resource-exhaustion hardening, not an SSRF fix (did:web
inherently fetches the domain named in the DID).

Change

  • timeout?: number on DidWebResolverOptions
  • threaded through getResolver -> resolve -> fetchDidDocumentAtUrl
  • omitted = no timeout, so existing behaviour is unchanged (backward compatible)

Tests

Three new tests in web-did-resolver.test.ts: the resolver builds the signal
from the configured timeout (AbortSignal.timeout is called with the value),
passes no signal when the option is omitted, and surfaces an aborted/timed-out
fetch as the standard notFound resolution error.
pnpm --filter @agentcommercekit/did test -> 73 passing. Changeset included
(minor). A response-size cap is a natural follow-up but needs body streaming,
so it is out of scope here.

AI assistance disclosure

Per the repository AI policy: implemented and tested by gpt 5.6-sol and fable 5, then reviewed and understood by me. I can explain the implementation and its trade-offs without AI aid. Tests, typecheck, and formatting pass locally against the package suite.

Summary by CodeRabbit

  • New Features
    • Added an optional timeout setting for did:web resolution requests (defaults to 5000ms).
    • Resolver requests are automatically cancelled when the timeout elapses (by wiring an AbortSignal to the fetch call).
  • Bug Fixes
    • Invalid timeout values now fail fast with a clear RangeError (valid range: 1..2147483647).
    • Timeout failures surface as a notFound resolution result with timeout-related error details.
  • Documentation
    • Updated resolver docs to clarify timeout behavior and limitations.
  • Tests
    • Updated/added unit tests covering signal wiring, default timeout, invalid values, and timeout error mapping.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d6986319-7c31-4e4c-bf64-aa724c880296

📥 Commits

Reviewing files that changed from the base of the PR and between 814f8d8 and f1e9670.

📒 Files selected for processing (3)
  • .changeset/did-web-resolver-fetch-timeout.md
  • packages/did/src/did-resolvers/web-did-resolver.test.ts
  • packages/did/src/did-resolvers/web-did-resolver.ts

Walkthrough

The did:web resolver now accepts a validated timeout, defaults it to 5000 ms, passes an abort signal to document fetches, maps timeout failures to notFound, and documents the behavior in a Changesets entry.

Changes

did:web fetch timeout

Layer / File(s) Summary
Configure and apply fetch timeout
packages/did/src/did-resolvers/web-did-resolver.ts
Adds and validates the timeout option, applies a 5000 ms default, and passes AbortSignal.timeout(timeout) to DID document fetches.
Validate timeout behavior and document option
packages/did/src/did-resolvers/web-did-resolver.test.ts, .changeset/did-web-resolver-fetch-timeout.md
Tests signal propagation, configured, default, invalid, and timeout-error cases, and documents the valid range and timeout behavior.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • agentcommercekit/ack#133: Updates the same resolver fetch arguments and test expectations while adding redirect handling.

Suggested reviewers: venables

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding an optional fetch timeout to the did:web resolver.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/did/src/did-resolvers/web-did-resolver.ts`:
- Around line 70-78: Update getResolver() to validate timeout before calling
AbortSignal.timeout(), rejecting negative, non-integer, non-finite, and
non-number values explicitly rather than allowing them to become notFound DID
errors. Preserve valid timeout behavior and add tests covering each
invalid-value category.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8a295354-8cf0-4ada-af59-bed6ec27927b

📥 Commits

Reviewing files that changed from the base of the PR and between 43a066c and 7503cd5.

📒 Files selected for processing (3)
  • .changeset/did-web-resolver-fetch-timeout.md
  • packages/did/src/did-resolvers/web-did-resolver.test.ts
  • packages/did/src/did-resolvers/web-did-resolver.ts

Comment thread packages/did/src/did-resolvers/web-did-resolver.ts
@EfeDurmaz16
EfeDurmaz16 force-pushed the did-resolver-fetch-timeout branch from 7503cd5 to f288d45 Compare July 22, 2026 00:31
@EfeDurmaz16

Copy link
Copy Markdown
Contributor Author

@venables when you have a moment, would appreciate a review. Small backward-compatible hardening (optional fetch timeout on the did:web resolver) I hit while building an ACK-ID + x402 demo. CodeRabbit's note is addressed; CI is waiting on a maintainer 'approve and run' for the fork branch.

domleboss97
domleboss97 previously approved these changes Jul 30, 2026

@domleboss97 domleboss97 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

😎 some comments, but looks good!

expect(mockFetch).not.toHaveBeenCalled()
})

it("passes an abort signal built from the configured timeout", async () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

not related to this pr in particular, but i think some test helpers could make each of these tests less verbose

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed it would help. Left it out since it touches the pre-existing tests too; happy to do a small follow-up PR.

* @default []
*/
allowedHttpHosts?: string[]
/**

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this comment seems a bit verbose - it seems like we could drop the rationale sentence and the mention of previous behavior, which one could see through changesets anyways. could also add a @default annotation

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Trimmed and added @default 5000 in 814f8d8.

docPath = DEFAULT_DOC_PATH,
fetch = globalThis.fetch,
allowedHttpHosts = DEFAULT_ALLOWED_HTTP_HOSTS,
timeout,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

might make sense now that we're adding this to have a default value, perhaps 5 seconds or something?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 814f8d8: defaults to 5000ms now. While double checking the range for this I noticed values above 2^31-1 clamp to a 1ms timer in Node, and above 2^32 they throw inside the fetch, both surfacing as a misleading notFound.

So f1e9670 caps the range at 2^31 -1 . Passing the max (about 24.8 days) works as an effective opt-out. The changeset covers both.

allowedHttpHosts = DEFAULT_ALLOWED_HTTP_HOSTS,
timeout,
}: DidWebResolverOptions = {}): { web: DIDResolver } {
// Fail fast on a bad timeout: `AbortSignal.timeout` throws on non-positive,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

very minor - AbortSignal.timeout(0) does not actually throw

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Right, it doesn't throw natively. getResolver still rejects 0 with a RangeError since a 0ms timeout would abort every request before it starts. Fixed the comment in 814f8d8 so it no longer claims the API throws on it.

// non-integer or non-finite values, and that would otherwise surface as a
// misleading `notFound` resolution error rather than a programmer error.
if (timeout !== undefined && (!Number.isInteger(timeout) || timeout <= 0)) {
throw new TypeError("`timeout` must be a positive integer (milliseconds)")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit - RangeError might be more accurate here. the type is actually correct.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, switched in 814f8d8, tests updated.

Address review feedback:
- timeout now defaults to 5000ms instead of being opt-in
- out-of-range values throw RangeError instead of TypeError
- correct the validation rationale: AbortSignal.timeout(0) does not throw,
  0 is rejected because it would abort every request before it starts
- trim the option docs and add @default
- update the changeset (the previous backward-compat wording no longer held)
- tests: default-signal assertion, RangeError, exact-init updates

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/did/src/did-resolvers/web-did-resolver.ts`:
- Around line 47-54: Preserve an omitted timeout as undefined in the resolver
configuration and request flow, rather than applying a 5000ms default. Update
the timeout handling around getDidResolver/getResolver so an AbortSignal is
created and passed only when timeout is explicitly configured; keep configured
timeout behavior unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cddb25b4-2032-4768-90c3-bbc85cfd914b

📥 Commits

Reviewing files that changed from the base of the PR and between 7503cd5 and 814f8d8.

📒 Files selected for processing (3)
  • .changeset/did-web-resolver-fetch-timeout.md
  • packages/did/src/did-resolvers/web-did-resolver.test.ts
  • packages/did/src/did-resolvers/web-did-resolver.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/did/src/did-resolvers/web-did-resolver.test.ts

Comment thread packages/did/src/did-resolvers/web-did-resolver.ts
Values in (2^31-1, 2^32-1] pass AbortSignal.timeout in Node but the timer
clamps to 1ms (TimeoutOverflowWarning), aborting every request; values above
2^32-1 throw at fetch time. Both surfaced as a misleading notFound, which is
exactly what the fail-fast guard exists to prevent. Reject timeouts above
2147483647ms up front and document that the maximum is an effective opt-out.

@domleboss97 domleboss97 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants