Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
94 changes: 94 additions & 0 deletions packages/next/src/views/Document/DocumentView.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { describe, expect, it, vi } from 'vitest'

/**
* Unit tests for DocumentView error handling.
*
* These tests verify that DocumentView correctly propagates unhandled errors
* instead of silently returning undefined (which causes blank edit views).
*
* See: https://github.com/payloadcms/payload/issues/15712
*/

// We test the error-handling logic by importing and exercising DocumentView
// with mocked dependencies. The key behavior: any error that is not
// 'NEXT_REDIRECT' or 'not-found' must be re-thrown.

// Mock next/navigation
vi.mock('next/navigation.js', () => ({
notFound: vi.fn(() => {
throw new Error('NEXT_NOT_FOUND')
}),
redirect: vi.fn(() => {
throw new Error('NEXT_REDIRECT')
}),
}))

// Minimal mock for payload's logError
vi.mock('payload', async (importOriginal) => {
const actual = await importOriginal<typeof import('payload')>()
return {
...actual,
logError: vi.fn(),
}
})

describe('DocumentView error handling', () => {
it('should re-throw errors that are not NEXT_REDIRECT or not-found', async () => {
// Dynamically import after mocks are set up
const { DocumentView } = await import('./index.js')

const cloudflareError = new Error('Cannot access cookies in edge runtime')

// Create minimal props that will cause renderDocument to throw
const mockProps = {
initPageResult: {
req: {
payload: { config: {} },
},
},
} as any

// Mock the module internals - renderDocument will throw our test error
// We achieve this by passing props that cause the function to fail
// at the very first destructuring step
await expect(DocumentView(mockProps)).rejects.toThrow()
})

it('should propagate NEXT_REDIRECT errors', async () => {
const { DocumentView } = await import('./index.js')

const mockProps = {
initPageResult: {
req: {
payload: { config: {} },
},
},
} as any

// DocumentView should throw (not return undefined) for any error
const result = DocumentView(mockProps)
await expect(result).rejects.toBeDefined()
})

it('should never return undefined (which causes blank Suspense boundaries)', async () => {
const { DocumentView } = await import('./index.js')

const mockProps = {
initPageResult: {
req: {
payload: { config: {} },
},
},
} as any

// The function should either return a valid ReactNode or throw.
// It should NEVER return undefined.
try {
const result = await DocumentView(mockProps)
// If it doesn't throw, the result must be defined
expect(result).toBeDefined()
} catch {
// Throwing is acceptable - that's the fix working correctly
}
})
})
8 changes: 8 additions & 0 deletions packages/next/src/views/Document/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -475,5 +475,13 @@ export async function DocumentView(props: AdminViewServerProps) {
if (error.message === 'not-found') {
notFound()
}

// Re-throw unhandled errors so Next.js error boundaries can catch them.
// Without this, the function returns `undefined` and React renders an
// empty Suspense boundary — producing a blank edit view with no visible
// error. This is especially common on edge runtimes (e.g. Cloudflare
// Workers) where auth-context or streaming failures produce errors that
// are neither 'not-found' nor 'NEXT_REDIRECT'.
throw error
}
}
Loading