Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/core/utils/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Safely extracts an error message from an unknown error value.
*
* @param error - The error value to extract a message from
* @returns The error message string, or 'Unknown error' if the error type cannot be determined
*
* @example
* ```typescript
* try {
* // some operation
* } catch (error) {
* const message = getErrorMessage(error)
* logger.error(`Operation failed: ${message}`)
* }
* ```
*/
export function getErrorMessage(error: unknown): string {
if (error instanceof Error) {
return error.message
}
if (typeof error === 'string') {
return error
}
return 'Unknown error'
}
24 changes: 20 additions & 4 deletions src/core/utils/validate-ipni-advertisement.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ProviderInfo } from '@filoz/synapse-sdk'
import type { CID } from 'multiformats/cid'
import type { Logger } from 'pino'
import { getErrorMessage } from './errors.js'
import type { ProgressEvent, ProgressEventHandler } from './types.js'

/**
Expand Down Expand Up @@ -168,10 +169,17 @@ export async function waitForIpniProviderResults(
fetchOptions.signal = options?.signal
}

const response = await fetch(`${ipniIndexerUrl}/cid/${ipfsRootCid}`, fetchOptions)
let response: Response | undefined
try {
response = await fetch(`${ipniIndexerUrl}/cid/${ipfsRootCid}`, fetchOptions)
} catch (fetchError) {
lastActualMultiaddrs = new Set()
lastFailureReason = `Failed to query IPNI indexer: ${getErrorMessage(fetchError)}`
options?.logger?.warn({ error: fetchError }, `${lastFailureReason}. Retrying...`)
}

// Parse and validate response
if (response.ok) {
if (response?.ok) {
let providerResults: ProviderResult[] = []
try {
const body = (await response.json()) as IpniIndexerResponse
Expand All @@ -183,7 +191,7 @@ export async function waitForIpniProviderResults(
} catch (parseError) {
// Clear actual multiaddrs on parse error
lastActualMultiaddrs = new Set()
lastFailureReason = 'Failed to parse IPNI response body'
lastFailureReason = `Failed to parse IPNI response body: ${getErrorMessage(parseError)}`
options?.logger?.warn({ error: parseError }, `${lastFailureReason}. Retrying...`)
}

Expand Down Expand Up @@ -239,6 +247,13 @@ export async function waitForIpniProviderResults(
`${lastFailureReason}. Retrying...`
)
}
} else if (response != null) {
lastActualMultiaddrs = new Set()
lastFailureReason = `IPNI indexer request failed with status ${response.status}`
options?.logger?.info(
{ status: response.status, statusText: response.statusText },
`${lastFailureReason}. Retrying...`
)
}

// Retry or fail
Expand Down Expand Up @@ -311,7 +326,8 @@ export function serviceURLToMultiaddr(serviceURL: string, logger?: Logger): stri

return `/dns/${url.hostname}/tcp/${port}/${protocolComponent}`
} catch (error) {
logger?.warn({ serviceURL, error }, 'Unable to derive IPNI multiaddr from serviceURL')
const reason = getErrorMessage(error)
logger?.warn({ serviceURL, error }, `Unable to derive IPNI multiaddr from serviceURL: ${reason}`)
return undefined
}
}
Expand Down
15 changes: 8 additions & 7 deletions src/test/unit/validate-ipni-advertisement.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,14 +304,15 @@ describe('waitForIpniProviderResults', () => {
})

describe('edge cases', () => {
it('should handle fetch throwing an error', async () => {
mockFetch.mockRejectedValueOnce(new Error('Network error'))

const promise = waitForIpniProviderResults(testCid, {})
const expectPromise = expect(promise).rejects.toThrow('Network error')
it('should retry when fetch throws before succeeding within maxAttempts', async () => {
mockFetch.mockRejectedValueOnce(new Error('Network error')).mockResolvedValueOnce(successResponse())

const promise = waitForIpniProviderResults(testCid, { maxAttempts: 2, delayMs: 1 })
await vi.runAllTimersAsync()
await expectPromise
const result = await promise

expect(result).toBe(true)
expect(mockFetch).toHaveBeenCalledTimes(2)
})

it('should handle different CID formats', async () => {
Expand Down Expand Up @@ -403,7 +404,7 @@ describe('waitForIpniProviderResults', () => {
})

const expectPromise = expect(promise).rejects.toThrow(
'Failed to parse IPNI response body. Expected multiaddrs: [/dns/expected.example.com/tcp/443/https]. Actual multiaddrs in response: []'
`IPFS root CID "${testCid.toString()}" does not have expected IPNI ProviderResults after 2 attempts. Last observation: Failed to parse IPNI response body: Invalid JSON. Expected multiaddrs: [/dns/expected.example.com/tcp/443/https]. Actual multiaddrs in response: []`
)

await vi.runAllTimersAsync()
Expand Down