diff --git a/public/locales/en/explore.json b/public/locales/en/explore.json
index 4c7ff5e8..82a61c7d 100644
--- a/public/locales/en/explore.json
+++ b/public/locales/en/explore.json
@@ -66,5 +66,8 @@
"paragraph1": "A visual representation of the node and its children."
}
}
+ },
+ "errors": {
+ "BlockFetchTimeoutError": "Failed to fetch content in {timeout}s. Please refresh the page to retry or try a different CID."
}
}
diff --git a/src/bundles/explore.js b/src/bundles/explore.js
index ad5aee05..34bd4b40 100644
--- a/src/bundles/explore.js
+++ b/src/bundles/explore.js
@@ -52,7 +52,7 @@ const makeBundle = () => {
}
} catch (error) {
console.warn('Failed to resolve path', path, error)
- return { path, error: error.toString() }
+ return { path, error }
}
},
staleAfter: Infinity,
diff --git a/src/components/ExplorePage.jsx b/src/components/ExplorePage.jsx
index 1a70d709..fecd12e2 100644
--- a/src/components/ExplorePage.jsx
+++ b/src/components/ExplorePage.jsx
@@ -6,6 +6,7 @@ import { connect } from 'redux-bundler-react'
import CidInfo from './cid-info/CidInfo'
import ErrorBoundary from './error/ErrorBoundary'
+import { IpldExploreErrorComponent } from './explore/IpldExploreErrorComponent'
import IpldGraph from './graph/LoadableIpldGraph'
import GraphCrumb from './graph-crumb/GraphCrumb'
import ComponentLoader from './loader/ComponentLoader'
@@ -51,13 +52,7 @@ export class ExplorePage extends React.Component {
- {error
- ? (
-
- {error}
-
- )
- : null}
+
{targetNode
? (
diff --git a/src/components/explore/IpldExploreErrorComponent.tsx b/src/components/explore/IpldExploreErrorComponent.tsx
new file mode 100644
index 00000000..341a9fc7
--- /dev/null
+++ b/src/components/explore/IpldExploreErrorComponent.tsx
@@ -0,0 +1,19 @@
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+
+import type IpldExploreError from '../../lib/errors'
+
+export interface IpldExploreErrorComponentProps {
+ error?: IpldExploreError
+}
+
+export function IpldExploreErrorComponent ({ error }: IpldExploreErrorComponentProps): JSX.Element | null {
+ const { t } = useTranslation('explore', { keyPrefix: 'errors' })
+ if (error == null) return null
+
+ return (
+
+ )
+}
diff --git a/src/lib/errors.ts b/src/lib/errors.ts
new file mode 100644
index 00000000..785a6089
--- /dev/null
+++ b/src/lib/errors.ts
@@ -0,0 +1,24 @@
+import { type TFunction } from 'i18next'
+
+export default class IpldExploreError extends Error {
+ constructor (private readonly options: Record
) {
+ super()
+ this.name = this.constructor.name
+ }
+
+ /**
+ * See IpldExploreError for usage.
+ * You must pass a t function that is registered with namespace 'explore' and keyPrefix 'errors'.
+ *
+ * @param t - the i18next-react t function
+ * @returns the translated string
+ * @example
+ * const {t} = useTranslation('explore', { keyPrefix: 'errors' })
+ * t('NameOfErrorClassThatExtendsIpldExploreError')
+ */
+ toString (t: TFunction<'translation', undefined, 'translation'>): string {
+ return t(this.name, this.options)
+ }
+}
+
+export class BlockFetchTimeoutError extends IpldExploreError {}
diff --git a/src/lib/get-raw-block.ts b/src/lib/get-raw-block.ts
index 128cd082..a6a6c053 100644
--- a/src/lib/get-raw-block.ts
+++ b/src/lib/get-raw-block.ts
@@ -3,6 +3,7 @@ import { type IPFSHTTPClient } from 'kubo-rpc-client'
import { CID } from 'multiformats'
import type { Version as CIDVersion } from 'multiformats/cid'
+import { BlockFetchTimeoutError } from './errors'
import getHasherForCode from './hash-importer.js'
async function getCidFromBytes (bytes: Uint8Array, cidVersion: CIDVersion, codecCode: number, multihashCode: number): Promise {
@@ -130,6 +131,10 @@ export async function getRawBlock (helia: Helia, kuboClient: IPFSHTTPClient, cid
return rawBlock
} catch (err) {
console.error('unable to get raw block', err)
+ if (abortController.signal.aborted) {
+ // if we timed out, we want to throw a timeout error, not a Promise.any error
+ throw new BlockFetchTimeoutError({ timeout: timeout / 1000 })
+ }
throw err
}
}