{"record":{"id":"ac8eaa23ca122bd2","repo":"Crosstalk-Solutions/project-nomad","slug":"failed-to-fetch-remote-zim-files","errorCode":null,"errorMessage":"Failed to fetch remote ZIM files.","messagePattern":"Failed to fetch remote ZIM files\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"admin/inertia/pages/settings/zim/remote-explorer.tsx","lineNumber":153,"sourceCode":"  } = useQuery<BrowseResult>({\n    queryKey: ['browse-library', browseUrl],\n    queryFn: () => api.browseLibrary(browseUrl!) as Promise<BrowseResult>,\n    enabled: !!browseUrl && selectedSource !== 'default',\n    refetchOnWindowFocus: false,\n    retry: false,\n  })\n\n  const { data, fetchNextPage, isFetching, isLoading } =\n    useInfiniteQuery<ListRemoteZimFilesResponse>({\n      queryKey: ['remote-zim-files', query],\n      queryFn: async ({ pageParam = 0 }) => {\n        // pageParam is an opaque Kiwix offset returned by the backend as `next_start`.\n        // The backend accumulates across multiple upstream pages when needed (#731), so the\n        // frontend can't derive the next offset from a 12-item page assumption.\n        const start = typeof pageParam === 'number' ? pageParam : 0\n        const res = await api.listRemoteZimFiles({ start, count: 12, query: query || undefined })\n        if (!res) {\n          throw new Error('Failed to fetch remote ZIM files.')\n        }\n        return res.data\n      },\n      initialPageParam: 0,\n      getNextPageParam: (lastPage) => (lastPage.has_more ? lastPage.next_start : undefined),\n      refetchOnWindowFocus: false,\n      placeholderData: keepPreviousData,\n      enabled: selectedSource === 'default',\n    })\n\n  const flatData = useMemo(() => {\n    const mapped = data?.pages.flatMap((page) => page.items) || []\n    const localNames = new Set(localFiles?.map((f) => f.name) ?? [])\n    return mapped.filter((item) => {\n      const isDownloading = downloads?.some((download) => {\n        const filename = item.download_url.split('/').pop()\n        return filename && download.filepath.endsWith(filename)\n      })","sourceCodeStart":135,"sourceCodeEnd":171,"githubUrl":"https://github.com/Crosstalk-Solutions/project-nomad/blob/0bd1c6f4f9888d577fe232de06ac144bb8337131/admin/inertia/pages/settings/zim/remote-explorer.tsx#L135-L171","documentation":"Thrown inside a React Query infinite-query pageFetchParam callback when api.listRemoteZimFiles({start, count, query}) resolves falsy. The Kiwix remote catalog request failed or returned an empty/unparseable body while paginating the remote ZIM explorer.","triggerScenarios":"Fetching page 2+ of the Kiwix library catalog with an opaque next_start offset when the upstream Kiwix server is unreachable, rate-limits the appliance, or the backend proxy returns an error body without throwing.","commonSituations":"Appliance offline or with restricted egress to library.kiwix.org, upstream Kiwix API schema change breaking the backend mapper, rate limiting after rapid infinite-scroll paging.","solutions":["Test the backend ZIM catalog proxy endpoint directly with the same start/count to see the raw upstream status","Verify internet connectivity/egress rules from the appliance to the Kiwix library","Add rate-limit/backoff on the infinite scroll and rely on React Query retry rather than throwing on first falsy response","If upstream schema changed, update the backend mapper to keep returning {data, has_more, next_start}"],"exampleFix":"// before\nconst res = await api.listRemoteZimFiles({ start, count: 12, query: query || undefined })\nif (!res) {\n  throw new Error('Failed to fetch remote ZIM files.')\n}\nreturn res.data\n// after\nconst res = await api.listRemoteZimFiles({ start, count: 12, query: query || undefined })\nif (!res) {\n  throw new Error(`Failed to fetch remote ZIM files (offset ${start}).`)\n}\nreturn res.data","handlingStrategy":"retry","validationCode":"if (!(await navigator.onLine)) throw new Error('Offline — remote ZIM catalog unavailable')","typeGuard":"const isZimPage = (r: unknown): r is { data: ZimFile[]; has_more: boolean; next_start?: number } =>\n  Array.isArray((r as any)?.data)","tryCatchPattern":"pageFn: async ({ pageParam }) => {\n  try {\n    const res = await api.listRemoteZimFiles({ start: pageParam ?? 0, count: 12, query: query || undefined })\n    if (!res) throw new Error('Failed to fetch remote ZIM files.')\n    return res.data\n  } catch (e) {\n    queryClient.invalidateQueries({ queryKey: ['remote-zim'] })\n    throw e\n  }\n}","preventionTips":["Rely on React Query's retry/backoff for transient upstream failures","Show offline state in the remote explorer UI before fetching","Throttle infinite scroll to avoid hammering the Kiwix catalog"],"tags":["kiwix","zim","infinite-query","pagination","offline"],"backgroundTag":"upstream-catalog-unavailable","analyzedSha":"0bd1c6f4f9888d577fe232de06ac144bb8337131","analyzedAt":"2026-08-27T05:34:15.424Z","schemaVersion":2},"datasetVersion":"2026-08-27T08:17:20.692Z"}