Skip to content

Commit 0d84d32

Browse files
committed
fix: proper 404 handling for missing docs/examples
- Return null from fetchApiContents on 404 instead of throwing - Handle ENOENT in local filesystem fetch - Convert serialized isNotFound errors to proper notFound() in route loaders
1 parent 5cc4c88 commit 0d84d32

File tree

4 files changed

+96
-49
lines changed

4 files changed

+96
-49
lines changed

src/routes/$libraryId/$version.docs.$.tsx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,36 @@ import {
88
createFileRoute,
99
useLocation,
1010
getRouteApi,
11+
isNotFound,
1112
} from '@tanstack/react-router'
1213

1314
const docsRouteApi = getRouteApi('/$libraryId/$version/docs')
1415

1516
export const Route = createFileRoute('/$libraryId/$version/docs/$')({
1617
staleTime: 1000 * 60 * 5,
17-
loader: (ctx) => {
18+
loader: async (ctx) => {
1819
const { _splat: docsPath, version, libraryId } = ctx.params
1920
const library = findLibrary(libraryId)
2021

2122
if (!library) {
2223
throw notFound()
2324
}
2425

25-
return loadDocs({
26-
repo: library.repo,
27-
branch: getBranch(library, version),
28-
docsPath: `${library.docsRoot || 'docs'}/${docsPath}`,
29-
})
26+
try {
27+
return await loadDocs({
28+
repo: library.repo,
29+
branch: getBranch(library, version),
30+
docsPath: `${library.docsRoot || 'docs'}/${docsPath}`,
31+
})
32+
} catch (error) {
33+
const isNotFoundError =
34+
isNotFound(error) ||
35+
(error && typeof error === 'object' && 'isNotFound' in error)
36+
if (isNotFoundError) {
37+
throw notFound()
38+
}
39+
throw error
40+
}
3041
},
3142
head: ({ loaderData, params }) => {
3243
const { libraryId } = params

src/routes/$libraryId/$version.docs.framework.$framework.examples.$.tsx

Lines changed: 44 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createFileRoute } from '@tanstack/react-router'
1+
import { createFileRoute, isNotFound, notFound } from '@tanstack/react-router'
22
import {
33
queryOptions,
44
useQuery,
@@ -62,35 +62,49 @@ export const Route = createFileRoute(
6262
// Used to tell the github contents api where to start looking for files in the target repository
6363
const repoStartingDirPath = `examples/${examplePath}`
6464

65-
// Fetching and Caching the contents of the target directory
66-
const githubContents = await queryClient.ensureQueryData(
67-
repoDirApiContentsQueryOptions(library.repo, branch, repoStartingDirPath),
68-
)
69-
70-
// Used to determine the starting file name for the explorer
71-
// It's either the selected path in the search params or a default we can derive
72-
// i.e. app.tsx, main.tsx, src/routes/__root.tsx, etc.
73-
// This value is not absolutely guaranteed to be available, so further resolution may be necessary
74-
const explorerCandidateStartingFileName =
75-
path ||
76-
getExampleStartingPath(params.framework as Framework, params.libraryId)
77-
78-
// Using the fetched contents, get the actual starting file-path for the explorer
79-
// The `explorerCandidateStartingFileName` is used for matching, but the actual file-path may differ
80-
const currentPath = determineStartingFilePath(
81-
githubContents,
82-
explorerCandidateStartingFileName,
83-
params.framework as Framework,
84-
params.libraryId,
85-
)
86-
87-
// Now that we've resolved the starting file path, we can
88-
// fetching and caching the file content for the starting file path
89-
await queryClient.ensureQueryData(
90-
fileQueryOptions(library.repo, branch, currentPath),
91-
)
92-
93-
return { repoStartingDirPath, currentPath }
65+
try {
66+
// Fetching and Caching the contents of the target directory
67+
const githubContents = await queryClient.ensureQueryData(
68+
repoDirApiContentsQueryOptions(
69+
library.repo,
70+
branch,
71+
repoStartingDirPath,
72+
),
73+
)
74+
75+
// Used to determine the starting file name for the explorer
76+
// It's either the selected path in the search params or a default we can derive
77+
// i.e. app.tsx, main.tsx, src/routes/__root.tsx, etc.
78+
// This value is not absolutely guaranteed to be available, so further resolution may be necessary
79+
const explorerCandidateStartingFileName =
80+
path ||
81+
getExampleStartingPath(params.framework as Framework, params.libraryId)
82+
83+
// Using the fetched contents, get the actual starting file-path for the explorer
84+
// The `explorerCandidateStartingFileName` is used for matching, but the actual file-path may differ
85+
const currentPath = determineStartingFilePath(
86+
githubContents,
87+
explorerCandidateStartingFileName,
88+
params.framework as Framework,
89+
params.libraryId,
90+
)
91+
92+
// Now that we've resolved the starting file path, we can
93+
// fetching and caching the file content for the starting file path
94+
await queryClient.ensureQueryData(
95+
fileQueryOptions(library.repo, branch, currentPath),
96+
)
97+
98+
return { repoStartingDirPath, currentPath }
99+
} catch (error) {
100+
const isNotFoundError =
101+
isNotFound(error) ||
102+
(error && typeof error === 'object' && 'isNotFound' in error)
103+
if (isNotFoundError) {
104+
throw notFound()
105+
}
106+
throw error
107+
}
94108
},
95109
head: ({ params }) => {
96110
const library = getLibrary(params.libraryId)

src/utils/docs.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@ export const fetchRepoDirectoryContents = createServerFn({
103103
.handler(async ({ data: { repo, branch, startingPath } }) => {
104104
const githubContents = await fetchApiContents(repo, branch, startingPath)
105105

106+
if (!githubContents) {
107+
throw notFound()
108+
}
109+
106110
// Cache for 60 minutes on shared cache
107111
// Revalidate in the background
108112
setResponseHeader('Cache-Control', 'public, max-age=0, must-revalidate')

src/utils/documents.server.ts

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -466,23 +466,38 @@ async function fetchApiContentsFs(
466466
async function getContentsForPath(
467467
filePath: string,
468468
): Promise<Array<GitHubFile>> {
469-
const list = await fsp.readdir(filePath, { withFileTypes: true })
470-
return list
471-
.filter((item) => !dirsAndFilesToIgnore.includes(item.name))
472-
.map((item) => {
473-
return {
474-
name: item.name,
475-
path: path.join(filePath, item.name),
476-
type: item.isDirectory() ? 'dir' : 'file',
477-
_links: {
478-
self: path.join(filePath, item.name),
479-
},
480-
}
481-
})
469+
try {
470+
const list = await fsp.readdir(filePath, { withFileTypes: true })
471+
return list
472+
.filter((item) => !dirsAndFilesToIgnore.includes(item.name))
473+
.map((item) => {
474+
return {
475+
name: item.name,
476+
path: path.join(filePath, item.name),
477+
type: item.isDirectory() ? 'dir' : 'file',
478+
_links: {
479+
self: path.join(filePath, item.name),
480+
},
481+
}
482+
})
483+
} catch (error) {
484+
if (
485+
error instanceof Error &&
486+
'code' in error &&
487+
error.code === 'ENOENT'
488+
) {
489+
return []
490+
}
491+
throw error
492+
}
482493
}
483494

484495
const data = await getContentsForPath(fsStartPath)
485496

497+
if (data.length === 0) {
498+
return null
499+
}
500+
486501
async function buildFileTree(
487502
nodes: Array<GitHubFile> | undefined,
488503
depth: number,
@@ -539,6 +554,9 @@ async function fetchApiContentsRemote(
539554
)
540555

541556
if (!res.ok) {
557+
if (res.status === 404) {
558+
return null
559+
}
542560
throw new Error(
543561
`Failed to fetch repo contents for ${repo}/${branch}/${startingPath}: Status is ${res.statusText} - ${res.status}`,
544562
)

0 commit comments

Comments
 (0)