-
Notifications
You must be signed in to change notification settings - Fork 110
feat(did): add optional fetch timeout to the did:web resolver #129
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| --- | ||
| "@agentcommercekit/did": minor | ||
| --- | ||
|
|
||
| Add a `timeout` option to `getResolver`'s `DidWebResolverOptions` for the | ||
| `did:web` resolver, defaulting to 5000ms. Resolving a `did:web` DID fetches | ||
| the host named in the DID, so an unresponsive or slow host could otherwise | ||
| hang the caller indefinitely. The resolver passes `AbortSignal.timeout(timeout)` | ||
| to the underlying fetch; a custom `fetch` must honour `init.signal` for the | ||
| timeout to take effect. Values outside 1..2147483647 (the 32-bit | ||
| timer limit) throw a `RangeError`: beyond it, runtimes either clamp the timer | ||
| to 1ms or throw at fetch time, both of which would surface as a misleading | ||
| `notFound`. There is no first-class opt-out; passing the maximum (about 24.8 | ||
| days) effectively disables the timeout. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -44,10 +44,20 @@ export interface DidWebResolverOptions { | |
| * @default [] | ||
| */ | ||
| allowedHttpHosts?: string[] | ||
| /** | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| * Milliseconds to wait for the DID document fetch before aborting. Must | ||
| * be a positive integer of at most 2147483647 (the 32-bit timer limit). | ||
| * | ||
| * The timeout is applied via an `AbortSignal` on the request. A custom | ||
| * `fetch` must honour `init.signal` for it to take effect. | ||
| * @default 5000 | ||
| */ | ||
| timeout?: number | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
|
|
||
| const DEFAULT_ALLOWED_HTTP_HOSTS: string[] = [] | ||
| const DEFAULT_DOC_PATH = "/.well-known/did.json" | ||
| const MAX_TIMEOUT_MS = 2147483647 | ||
|
|
||
| /** | ||
| * Get a did document from a url and validate that it is a DidDocument | ||
|
|
@@ -57,9 +67,15 @@ const DEFAULT_DOC_PATH = "/.well-known/did.json" | |
| */ | ||
| async function fetchDidDocumentAtUrl( | ||
| url: string | URL, | ||
| { fetch = globalThis.fetch }: { fetch?: FetchLike } = {}, | ||
| { | ||
| fetch = globalThis.fetch, | ||
| timeout, | ||
| }: { fetch?: FetchLike; timeout?: number } = {}, | ||
| ): Promise<DidDocument> { | ||
| const res = await fetch(url, { mode: "cors" }) | ||
| const res = await fetch(url, { | ||
| mode: "cors", | ||
| ...(timeout !== undefined ? { signal: AbortSignal.timeout(timeout) } : {}), | ||
| }) | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| if (!res.ok) { | ||
| throw new Error( | ||
|
|
@@ -141,7 +157,20 @@ export function getResolver({ | |
| docPath = DEFAULT_DOC_PATH, | ||
| fetch = globalThis.fetch, | ||
| allowedHttpHosts = DEFAULT_ALLOWED_HTTP_HOSTS, | ||
| timeout = 5000, | ||
| }: DidWebResolverOptions = {}): { web: DIDResolver } { | ||
| // Fail fast on a bad timeout rather than surfacing it later as a | ||
| // misleading `notFound` resolution error. `AbortSignal.timeout` throws on | ||
| // negative, non-integer or non-finite values; 0 is legal for the API but | ||
| // would abort every request before it starts; and values beyond the | ||
| // 32-bit timer range either clamp the timer to 1ms or throw at fetch | ||
| // time, depending on the runtime. | ||
| if (timeout <= 0 || timeout > MAX_TIMEOUT_MS || !Number.isInteger(timeout)) { | ||
| throw new RangeError( | ||
| "`timeout` must be a positive integer of at most 2147483647 milliseconds", | ||
| ) | ||
| } | ||
|
|
||
| async function resolve( | ||
| did: string, | ||
| parsed: ParsedDID, | ||
|
|
@@ -155,7 +184,7 @@ export function getResolver({ | |
| let didDocument: DIDDocument | null = null | ||
|
|
||
| try { | ||
| didDocument = await fetchDidDocumentAtUrl(url, { fetch }) | ||
| didDocument = await fetchDidDocumentAtUrl(url, { fetch, timeout }) | ||
|
|
||
| if (!isDidDocumentForDid(didDocument, did)) { | ||
| throw new Error("DID document id does not match requested did") | ||
|
|
||
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.