Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions src/routes/$libraryId/$version.docs.$.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { seo } from '~/utils/seo'
import { Doc } from '~/components/Doc'
import { isDocsNotFoundError } from '~/utils/docs-errors'
import { loadDocsPage, resolveDocsRedirect } from '~/utils/docs'
import { findLibrary, getBranch, getLibrary } from '~/libraries'
import { DocContainer } from '~/components/DocContainer'
Expand Down Expand Up @@ -36,6 +37,7 @@ export const Route = createFileRoute('/$libraryId/$version/docs/$')({
})
} catch (error) {
const isNotFoundError =
isDocsNotFoundError(error) ||
isNotFound(error) ||
(error && typeof error === 'object' && 'isNotFound' in error)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from '@tanstack/react-router'
import { seo } from '~/utils/seo'
import { Doc } from '~/components/Doc'
import { isDocsNotFoundError } from '~/utils/docs-errors'
import { loadDocsPage, resolveDocsRedirect } from '~/utils/docs'
import { getBranch, getLibrary } from '~/libraries'
import { capitalize } from '~/utils/utils'
Expand Down Expand Up @@ -36,6 +37,7 @@ export const Route = createFileRoute(
// This handles cases like switching frameworks where the same doc path doesn't exist
// Check both isNotFound() and the serialized form from server functions
const isNotFoundError =
isDocsNotFoundError(error) ||
isNotFound(error) ||
(error && typeof error === 'object' && 'isNotFound' in error)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { findLibrary, getBranch } from '~/libraries'
import { isDocsNotFoundError } from '~/utils/docs-errors'
import { loadDocs } from '~/utils/docs'
import { notFound, createFileRoute } from '@tanstack/react-router'
import { filterFrameworkContent } from '~/utils/markdown/filterFrameworkContent'
Expand Down Expand Up @@ -34,12 +35,22 @@ export const Route = createFileRoute(

const root = library.docsRoot || 'docs'

const doc = await loadDocs({
repo: library.repo,
branch: getBranch(library, version),
docsRoot: root,
docsPath: `framework/${framework}/${docsPath}`,
})
let doc

try {
doc = await loadDocs({
repo: library.repo,
branch: getBranch(library, version),
docsRoot: root,
docsPath: `framework/${framework}/${docsPath}`,
})
} catch (error) {
if (isDocsNotFoundError(error)) {
throw notFound()
}

throw error
}

// Filter framework-specific content using framework from URL path
const filteredContent = filterFrameworkContent(doc.content, {
Expand Down
25 changes: 18 additions & 7 deletions src/routes/$libraryId/$version.docs.{$}[.]md.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createFileRoute } from '@tanstack/react-router'
import { createFileRoute, notFound } from '@tanstack/react-router'
import { getBranch, getLibrary, type LibraryId } from '~/libraries'
import { isDocsNotFoundError } from '~/utils/docs-errors'
import { loadDocs } from '~/utils/docs'
import { filterFrameworkContent } from '~/utils/markdown/filterFrameworkContent'
import { getPackageManager } from '~/utils/markdown/installCommand'
Expand All @@ -23,12 +24,22 @@ export const Route = createFileRoute('/$libraryId/$version/docs/{$}.md')({
const library = getLibrary(libraryId as LibraryId)
const root = library.docsRoot || 'docs'

const doc = await loadDocs({
repo: library.repo,
branch: getBranch(library, version),
docsRoot: root,
docsPath,
})
let doc

try {
doc = await loadDocs({
repo: library.repo,
branch: getBranch(library, version),
docsRoot: root,
docsPath,
})
} catch (error) {
if (isDocsNotFoundError(error)) {
throw notFound()
}

throw error
}

// Filter framework-specific content only if framework is explicitly specified
const filteredContent = framework
Expand Down
25 changes: 25 additions & 0 deletions src/utils/docs-errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
type DocsNotFoundError = Error & {
data: { message: string }
isDocsNotFound: true
}

export function createDocsNotFoundError(message = 'No doc was found here!') {
const error = new Error(message) as DocsNotFoundError

error.name = 'DocsNotFoundError'
error.data = { message }
error.isDocsNotFound = true

return error
}

export function isDocsNotFoundError(
error: unknown,
): error is DocsNotFoundError {
return (
typeof error === 'object' &&
error !== null &&
'isDocsNotFound' in error &&
error.isDocsNotFound === true
)
}
Comment on lines +1 to +25
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

TanStack Start createServerFn thrown error serialization custom properties preserved client

💡 Result:

In TanStack Start, errors thrown inside a createServerFn handler are sent back to the client only after being serialized (TanStack uses JSON/seroval-style serialization). This means any non-JSON-serializable data on the thrown value (e.g., Error objects via JSON.stringify, or custom properties that don’t serialize) cannot be preserved as-is on the client. 1) Why “serialization/custom properties preserved client” fails - TanStack explicitly serializes the error to return it to the client. The server-function handler catches the thrown value and JSON.stringify’s the serialized form of the error before returning it in the response body. - As a result, custom properties that aren’t representable by the serializer (and plain Error instances) will be lost or break serialization instead of being preserved. 2) What you can do - Throw Error instances with only JSON-compatible fields (message, name), or construct a plain object error payload yourself and throw/return something that is known-serializable. - Avoid returning/throwing types that include unknown or other non-serializable values; TanStack validates serializability and will error if the return type includes fields like unknown (example shown in a createServerFn issue). - If you need to keep extra error fields, ensure they are serializable primitives/objects (and not instances with methods/prototypes like Error, Response, etc.). 3) Source-backed notes from TanStack - TanStack documents that “Errors are serialized to the client” for server functions. - TanStack Start issue reports that “server functions can’t serialize error objects” because JSON.stringify can’t serialize Error objects (originally). A later comment indicates it was likely fixed, but the core constraint remains: serialization rules apply. - The server runtime code shows the catch path JSON.stringify’ing a cross-JSON serialization of the thrown error. If you paste your exact error shape (what you throw, e.g., throw new Error(...) vs throw {message, ...custom} vs throw someObject) and your expected client-side access (e.g., error.customProp), I can tell you the safest way to structure it for TanStack Start’s serializer.

Citations:


🏁 Script executed:

# First, find the files mentioned in the review comment
fd -t f "docs.functions.ts" 

# Also find the route files mentioned
fd -t f "$libraryId" --extension tsx

# Find test files that might test error handling
fd -t f -e test.ts -e test.tsx -e spec.ts -e spec.tsx

Repository: TanStack/tanstack.com

Length of output: 15260


🏁 Script executed:

# Search for usage of isDocsNotFoundError and how errors are handled
rg -t ts -t tsx "isDocsNotFoundError" -A 3 -B 3

Repository: TanStack/tanstack.com

Length of output: 92


🏁 Script executed:

# Search for the isNotFound pattern mentioned as existing fallback
rg -t ts -t tsx "isNotFound" -A 2 -B 2 | head -50

Repository: TanStack/tanstack.com

Length of output: 92


🏁 Script executed:

# Check if there's any serialization/error configuration in the codebase
rg -t ts -t tsx "seroval|Serializer|createServerFn" -l | head -20

Repository: TanStack/tanstack.com

Length of output: 92


🏁 Script executed:

# Read the docs-errors.ts file
cat -n src/utils/docs-errors.ts

Repository: TanStack/tanstack.com

Length of output: 800


🏁 Script executed:

# Read docs.functions.ts to see how errors are thrown
cat -n src/utils/docs.functions.ts

Repository: TanStack/tanstack.com

Length of output: 11462


🏁 Script executed:

# Check the route file for error handling patterns
cat -n src/routes/'$libraryId'/'$version'.docs.'$'.tsx | head -100

Repository: TanStack/tanstack.com

Length of output: 3477


🏁 Script executed:

# Look for isNotFound usage in route files
rg "isNotFound" src/routes --type-add 'tsx:*.tsx' -t tsx -A 3 -B 3 | head -80

Repository: TanStack/tanstack.com

Length of output: 4512


🏁 Script executed:

# Look for test files that might test the error serialization
fd -type f "test\|spec" | grep -i "docs\|error" | head -20

Repository: TanStack/tanstack.com

Length of output: 235


🏁 Script executed:

# Search for any test files testing the error behavior
rg "isDocsNotFoundError|createDocsNotFoundError" -t ts -t tsx -A 5 -B 5 | head -100

Repository: TanStack/tanstack.com

Length of output: 92


🏁 Script executed:

# Check if there's a serialization configuration in the project
rg "serialize|serializer" src/ --type-add 'tsx:*.tsx' -l | head -10

Repository: TanStack/tanstack.com

Length of output: 167


🏁 Script executed:

# Look at the other route file mentioned to see its error handling
sed -n '1,100p' 'src/routes/$libraryId/$version.docs.framework.$framework.$.tsx'

Repository: TanStack/tanstack.com

Length of output: 3207


Add fallback structural check for serialized isDocsNotFound property.

TanStack Start serializes errors across the server-function boundary, and custom Error properties don't survive this serialization. The route handlers in src/routes/$libraryId/$version.docs.$.tsx and .framework.$framework.$.tsx already handle this for Router errors with the fallback 'isNotFound' in error check (lines 42 and 45), but lack an equivalent fallback for isDocsNotFound.

Update the error handling in both routes to include:

(error && typeof error === 'object' && 'isDocsNotFound' in error)

alongside the existing structural checks. This ensures that createDocsNotFoundError() thrown from fetchDocs, fetchFile, and fetchRepoDirectoryContents will be properly detected on the client after serialization, and the redirect/notFound() fallback logic will trigger correctly for client-side navigation into non-existent docs pages.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/docs-errors.ts` around lines 1 - 25, The route error guards fail to
detect serialized DocsNotFound errors because custom Error properties are lost
during TanStack Start serialization; update the error handling in the route
handlers that currently use isDocsNotFound/isDocsNotFoundError to also check for
the serialized shape by adding the structural fallback (error && typeof error
=== 'object' && 'isDocsNotFound' in error) alongside the existing checks so that
createDocsNotFoundError() thrown from
fetchDocs/fetchFile/fetchRepoDirectoryContents will be recognized and trigger
the redirect/notFound() logic.

8 changes: 4 additions & 4 deletions src/utils/docs.functions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { notFound } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'
import { setResponseHeader } from '@tanstack/react-start/server'
import removeMarkdown from 'remove-markdown'
Expand All @@ -9,6 +8,7 @@ import {
fetchRepoFile,
isRecoverableGitHubContentError,
} from '~/utils/documents.server'
import { createDocsNotFoundError } from './docs-errors'
import { renderMarkdownToRsc } from './markdown'
import { getCachedDocsArtifact } from './github-content-cache.server'
import { buildRedirectManifest, type RedirectManifestEntry } from './redirects'
Expand Down Expand Up @@ -223,7 +223,7 @@ export const fetchDocs = createServerFn({ method: 'GET' })
const file = await readRepoFileOrFallback(repo, branch, filePath)

if (!file) {
throw notFound()
throw createDocsNotFoundError()
}

const frontMatter = extractFrontMatter(file)
Expand Down Expand Up @@ -267,7 +267,7 @@ export const fetchFile = createServerFn({ method: 'GET' })
const file = await readRepoFileOrFallback(repo, branch, filePath)

if (!file) {
throw notFound()
throw createDocsNotFoundError()
}

setDocsCacheHeaders('max-age=3600, stale-while-revalidate=3600, durable')
Expand All @@ -284,7 +284,7 @@ export const fetchRepoDirectoryContents = createServerFn({
const githubContents = await fetchApiContents(repo, branch, startingPath)

if (!githubContents) {
throw notFound()
throw createDocsNotFoundError()
}

setDocsCacheHeaders('max-age=3600, stale-while-revalidate=3600, durable')
Expand Down
14 changes: 3 additions & 11 deletions src/utils/docs.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { notFound } from '@tanstack/react-router'
import {
fetchDocs,
fetchDocsPage,
Expand All @@ -7,6 +6,7 @@ import {
fetchFile,
fetchRepoDirectoryContents,
} from './docs.functions'
import { createDocsNotFoundError } from './docs-errors'
import { removeLeadingSlash } from './utils'

export const loadDocs = async ({
Expand All @@ -21,11 +21,7 @@ export const loadDocs = async ({
docsPath: string
}) => {
if (!branch || !docsRoot || !docsPath) {
throw notFound({
data: {
message: 'No doc was found here!',
},
})
throw createDocsNotFoundError()
}

return fetchDocs({
Expand All @@ -49,11 +45,7 @@ export const loadDocsPage = async ({
docsPath: string
}) => {
if (!branch || !docsRoot || !docsPath) {
throw notFound({
data: {
message: 'No doc was found here!',
},
})
throw createDocsNotFoundError()
}

return fetchDocsPage({
Expand Down