From d30d94c68c64abdb232eb950ce81eb8becada894 Mon Sep 17 00:00:00 2001 From: george-hub331 Date: Sat, 5 Apr 2025 15:56:30 +0100 Subject: [PATCH 1/4] feat: enhance error handling and user feedback for CAR file uploads --- public/locales/en/explore.json | 10 +++- src/components/StartExploringPage.tsx | 46 ++++++++++++------ src/components/explore/IpldCarExploreForm.tsx | 2 +- .../explore/IpldExploreErrorComponent.tsx | 47 +++++++++++++++++-- src/lib/errors.ts | 2 + src/lib/import-car.ts | 45 ++++++++++++++---- src/providers/explore.tsx | 31 ++++++++++-- 7 files changed, 147 insertions(+), 36 deletions(-) diff --git a/public/locales/en/explore.json b/public/locales/en/explore.json index b6422f5d..410c8579 100644 --- a/public/locales/en/explore.json +++ b/public/locales/en/explore.json @@ -69,6 +69,14 @@ } }, "errors": { - "BlockFetchTimeoutError": "Failed to fetch content in {timeout}s. Please refresh the page to retry or try a different CID." + "BlockFetchTimeoutError": "Failed to fetch content in {timeout}s. Please refresh the page to retry or try a different CID.", + "CARFetchError": "Failed to import CAR file: {{message}}", + "checkIpfsNetwork": "Check content availability on IPFS Network", + "troubleshootingTips": { + "title": "Troubleshooting Tips", + "checkCarFormat": "Check if the file is a valid CAR format", + "tryDifferentCar": "Try uploading a different CAR file" + }, + "clearError": "Okay, take me back to start exploring page" } } diff --git a/src/components/StartExploringPage.tsx b/src/components/StartExploringPage.tsx index 2083d586..9a02c6be 100644 --- a/src/components/StartExploringPage.tsx +++ b/src/components/StartExploringPage.tsx @@ -4,7 +4,9 @@ import { useTranslation } from 'react-i18next' import ReactJoyride from 'react-joyride' import { type ExplorePageLink, explorePageLinks } from '../lib/explore-page-suggestions.js' import { projectsTour } from '../lib/tours.js' +import { useExplore } from '../providers/explore.js' import { AboutIpld } from './about/AboutIpld.js' +import { IpldExploreErrorComponent } from './explore/IpldExploreErrorComponent.js' import IpldExploreForm from './explore/IpldExploreForm.js' import { type NodeStyle, colorForNode, nameForNode, shortNameForNode } from './object-info/ObjectInfo.js' @@ -29,6 +31,9 @@ interface StartExploringPageProps { export const StartExploringPage: React.FC = ({ embed, runTour = false, joyrideCallback, links }) => { const { t } = useTranslation('explore') + const { exploreState } = useExplore() + + const { error } = exploreState return (
@@ -37,22 +42,35 @@ export const StartExploringPage: React.FC = ({ embed, r
-
-

{t('StartExploringPage.header')}

-

{t('StartExploringPage.leadParagraph')}

-
+ {error == null && ( +
+

{t('StartExploringPage.header')}

+

{t('StartExploringPage.leadParagraph')}

+
+ )} {embed != null ? : null} -
    - {(links ?? explorePageLinks).map((suggestion) => ( -
  • - -
  • - ))} -
-
-
- + {error != null + ? ( +
+ +
+ ) + : null} + {error == null && ( +
    + {(links ?? explorePageLinks).map((suggestion) => ( +
  • + +
  • + ))} +
+ )}
+ {error == null && ( +
+ +
+ )}
{ const { t } = useTranslation('explore') - // const [file, setFile] = useState({}) const { doUploadUserProvidedCar } = useExplore() const { selectHeliaReady } = useHelia() @@ -35,6 +34,7 @@ export const IpldCarExploreForm: React.FC = () => { console.error('no file selected') return } + void doUploadUserProvidedCar(selectedFile, uploadImage) }, [doUploadUserProvidedCar]) diff --git a/src/components/explore/IpldExploreErrorComponent.tsx b/src/components/explore/IpldExploreErrorComponent.tsx index 842841d7..e330bc0e 100644 --- a/src/components/explore/IpldExploreErrorComponent.tsx +++ b/src/components/explore/IpldExploreErrorComponent.tsx @@ -1,18 +1,55 @@ import React from 'react' import { useTranslation } from 'react-i18next' +import { CARFetchError } from '../../lib/errors' +import { useExplore } from '../../providers/explore.js' import type IpldExploreError from '../../lib/errors' +import type { ExploreState } from '../../providers/explore.js' export interface IpldExploreErrorComponentProps { error: IpldExploreError | null } -export function IpldExploreErrorComponent ({ error }: IpldExploreErrorComponentProps): JSX.Element | null { +const CIDTroubleshootingTips: React.FC = () => { const { t } = useTranslation('explore', { keyPrefix: 'errors' }) - if (error == null) return null + const { setExploreState } = useExplore() + + const handleClearError = (): void => { + const hashSplit = window.location.hash.split('/') + if (hashSplit.length > 2) { + window.location.hash = '#/explore' + } + setExploreState((state: ExploreState) => ({ ...state, path: hashSplit.length > 2 ? '/' : state.path, error: null })) + } return ( -
-
{error.toString(t)}
-
+ <> +
+

{t('troubleshootingTips.title')}

+
    +
  • {t('troubleshootingTips.checkCarFormat')}
  • +
  • {t('troubleshootingTips.tryDifferentCar')}
  • +
+
+
+ +
+ ) } + +export function IpldExploreErrorComponent ({ error }: IpldExploreErrorComponentProps): JSX.Element | null { + const { t } = useTranslation('explore', { keyPrefix: 'errors' }) + if (error == null) return null + + return
+ {error instanceof CARFetchError + ? + :
{error?.toString(t)}
+ } +
+} diff --git a/src/lib/errors.ts b/src/lib/errors.ts index 5b01f086..e672d90e 100644 --- a/src/lib/errors.ts +++ b/src/lib/errors.ts @@ -22,3 +22,5 @@ export default class IpldExploreError extends Error { } export class BlockFetchTimeoutError extends IpldExploreError {} + +export class CARFetchError extends IpldExploreError {} diff --git a/src/lib/import-car.ts b/src/lib/import-car.ts index e8646c68..7bb59f66 100644 --- a/src/lib/import-car.ts +++ b/src/lib/import-car.ts @@ -2,6 +2,7 @@ import { type Helia } from '@helia/interface' import { CarBlockIterator } from '@ipld/car' import { type CID } from 'multiformats' import { source } from 'stream-to-it' +import { BlockFetchTimeoutError } from './errors.js' /** * Given a file object representing a CAR archive, import it into the given Helia instance, @@ -9,15 +10,39 @@ import { source } from 'stream-to-it' * * TODO: Handle multiple roots */ -export async function importCar (file: File, helia: Helia): Promise { - const inStream = file.stream() - const CarIterator = await CarBlockIterator.fromIterable(source(inStream)) - for await (const { cid, bytes } of CarIterator) { - // add blocks to helia to ensure they are available while navigating children - await helia.blockstore.put(cid, bytes) - } - const cidRoots = await CarIterator.getRoots() +export async function importCar (file: File, helia: Helia, timeout = 30000): Promise { + const controller = new AbortController() + const { signal } = controller + + const timeoutId = setTimeout(() => { + controller.abort('Request timed out') + }, timeout) + + try { + const inStream = file.stream() - // @todo: Handle multiple roots - return cidRoots[0] + const CarIterator = await CarBlockIterator.fromIterable(source(inStream)) + + for await (const { cid, bytes } of CarIterator) { + if (signal.aborted) { + throw new BlockFetchTimeoutError({ timeout: timeout / 1000, cid: 'CAR_IMPORT' }) + } + // add blocks to helia to ensure they are available while navigating children + await helia.blockstore.put(cid, bytes) + } + + const cidRoots = await CarIterator.getRoots() + if (cidRoots.length === 0) { + throw new Error('Invalid CAR file: no roots found') + } + // @todo: Handle multiple roots + return cidRoots[0] + } catch (err) { + if (err instanceof BlockFetchTimeoutError) { + throw err + } + throw new Error(err instanceof Error ? err.message : 'Failed to import CAR file') + } finally { + clearTimeout(timeoutId) + } } diff --git a/src/providers/explore.tsx b/src/providers/explore.tsx index bf62533e..81191eb4 100644 --- a/src/providers/explore.tsx +++ b/src/providers/explore.tsx @@ -1,6 +1,7 @@ import { CID } from 'multiformats/cid' import React, { createContext, useContext, useState, useEffect, type ReactNode, useCallback } from 'react' import { type LinkObject } from '../components/object-info/links-table' +import { CARFetchError } from '../lib/errors.js' import { ensureLeadingSlash } from '../lib/helpers.js' import { importCar } from '../lib/import-car.js' import { parseIpldPath } from '../lib/parse-ipld-path.js' @@ -17,6 +18,7 @@ interface ExploreContextProps { doExploreLink(link: any): void doExploreUserProvidedPath(path: string): void doUploadUserProvidedCar(file: File, uploadImage: string): Promise + setExploreState: React.Dispatch> } export interface ExploreState { @@ -185,6 +187,13 @@ export const ExploreProvider = ({ children, state, explorePathPrefix = '#/explor } const doUploadUserProvidedCar = useCallback(async (file: File, uploadImage: string): Promise => { + const resolveLoader = (image: string): void => { + const imageFileLoader = document.getElementById('car-loader-image') as HTMLImageElement + if (imageFileLoader != null) { + imageFileLoader.src = image + } + } + if (helia == null) { console.error('FIXME: Helia not ready yet, but user tried to upload a car file') return @@ -194,17 +203,29 @@ export const ExploreProvider = ({ children, state, explorePathPrefix = '#/explor const hash = rootCid.toString() != null ? `${explorePathPrefix}${ensureLeadingSlash(rootCid.toString())}` : explorePathPrefix window.location.hash = hash - const imageFileLoader = document.getElementById('car-loader-image') as HTMLImageElement - if (imageFileLoader != null) { - imageFileLoader.src = uploadImage - } + resolveLoader(uploadImage) } catch (err) { console.error('Could not import car file', err) + + setExploreState((prevState) => ({ + ...prevState, + targetNode: null, + error: new CARFetchError({ + message: err instanceof Error ? err.message : 'Failed to import CAR file' + }) + })) + + resolveLoader(uploadImage) + + const carFileInputEl = document.getElementById('car-file') as HTMLInputElement + if (carFileInputEl != null) { + carFileInputEl.value = '' + } } }, [explorePathPrefix, helia]) return ( - + {children} ) From a6e424c4ccaa64ae678027802d3df72730221154 Mon Sep 17 00:00:00 2001 From: george-hub331 Date: Sat, 5 Apr 2025 16:12:03 +0100 Subject: [PATCH 2/4] style: update ErrorComponent for improved readability and visual consistency --- src/components/explore/IpldExploreErrorComponent.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/explore/IpldExploreErrorComponent.tsx b/src/components/explore/IpldExploreErrorComponent.tsx index e330bc0e..4e2fe4a2 100644 --- a/src/components/explore/IpldExploreErrorComponent.tsx +++ b/src/components/explore/IpldExploreErrorComponent.tsx @@ -46,10 +46,12 @@ export function IpldExploreErrorComponent ({ error }: IpldExploreErrorComponentP const { t } = useTranslation('explore', { keyPrefix: 'errors' }) if (error == null) return null - return
+ return
+
{error instanceof CARFetchError ? - :
{error?.toString(t)}
+ :
{error.toString(t)}
}
+
} From 2ecdaa5e8f30a18ec4c5f071a44976b7a31826f3 Mon Sep 17 00:00:00 2001 From: george-hub331 Date: Sat, 5 Apr 2025 22:15:56 +0100 Subject: [PATCH 3/4] fix: improve error messages and handling for CAR file imports --- public/locales/en/explore.json | 4 ++-- src/components/explore/IpldExploreErrorComponent.tsx | 4 ++-- src/lib/import-car.ts | 2 +- src/providers/explore.tsx | 9 ++++++--- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/public/locales/en/explore.json b/public/locales/en/explore.json index 410c8579..5a820d58 100644 --- a/public/locales/en/explore.json +++ b/public/locales/en/explore.json @@ -69,8 +69,8 @@ } }, "errors": { - "BlockFetchTimeoutError": "Failed to fetch content in {timeout}s. Please refresh the page to retry or try a different CID.", - "CARFetchError": "Failed to import CAR file: {{message}}", + "BlockFetchTimeoutError": "Failed to fetch content in {timeout}s. Please refresh the page to retry or try a different CID/CAR file.", + "CARFetchError": "Failed to import CAR file: {message}", "checkIpfsNetwork": "Check content availability on IPFS Network", "troubleshootingTips": { "title": "Troubleshooting Tips", diff --git a/src/components/explore/IpldExploreErrorComponent.tsx b/src/components/explore/IpldExploreErrorComponent.tsx index 4e2fe4a2..6eb0d6c5 100644 --- a/src/components/explore/IpldExploreErrorComponent.tsx +++ b/src/components/explore/IpldExploreErrorComponent.tsx @@ -48,10 +48,10 @@ export function IpldExploreErrorComponent ({ error }: IpldExploreErrorComponentP return
+
{error.toString(t)}
{error instanceof CARFetchError ? - :
{error.toString(t)}
- } + : null}
} diff --git a/src/lib/import-car.ts b/src/lib/import-car.ts index 7bb59f66..351dbe33 100644 --- a/src/lib/import-car.ts +++ b/src/lib/import-car.ts @@ -25,7 +25,7 @@ export async function importCar (file: File, helia: Helia, timeout = 30000): Pro for await (const { cid, bytes } of CarIterator) { if (signal.aborted) { - throw new BlockFetchTimeoutError({ timeout: timeout / 1000, cid: 'CAR_IMPORT' }) + throw new BlockFetchTimeoutError({ timeout: timeout / 1000 }) } // add blocks to helia to ensure they are available while navigating children await helia.blockstore.put(cid, bytes) diff --git a/src/providers/explore.tsx b/src/providers/explore.tsx index 81191eb4..d2847405 100644 --- a/src/providers/explore.tsx +++ b/src/providers/explore.tsx @@ -198,7 +198,12 @@ export const ExploreProvider = ({ children, state, explorePathPrefix = '#/explor console.error('FIXME: Helia not ready yet, but user tried to upload a car file') return } + try { + if (file.size === 0) { + throw new Error('CAR file is empty') + } + const rootCid = await importCar(file, helia) const hash = rootCid.toString() != null ? `${explorePathPrefix}${ensureLeadingSlash(rootCid.toString())}` : explorePathPrefix window.location.hash = hash @@ -210,9 +215,7 @@ export const ExploreProvider = ({ children, state, explorePathPrefix = '#/explor setExploreState((prevState) => ({ ...prevState, targetNode: null, - error: new CARFetchError({ - message: err instanceof Error ? err.message : 'Failed to import CAR file' - }) + error: new CARFetchError({ message: err instanceof Error ? err.message : 'Failed to import CAR file' }) })) resolveLoader(uploadImage) From 5c35bdfbe3137bb92f18e793125ab587ca9ac930 Mon Sep 17 00:00:00 2001 From: george-hub331 Date: Sat, 5 Apr 2025 22:45:20 +0100 Subject: [PATCH 4/4] fix: update error handling for CAR file imports to provide clearer feedback on invalid files --- src/lib/import-car.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/import-car.ts b/src/lib/import-car.ts index 351dbe33..8c256410 100644 --- a/src/lib/import-car.ts +++ b/src/lib/import-car.ts @@ -38,8 +38,8 @@ export async function importCar (file: File, helia: Helia, timeout = 30000): Pro // @todo: Handle multiple roots return cidRoots[0] } catch (err) { - if (err instanceof BlockFetchTimeoutError) { - throw err + if (err instanceof Error && err.message === 'Unexpected end of data') { + throw new Error('Invalid CAR file') } throw new Error(err instanceof Error ? err.message : 'Failed to import CAR file') } finally {