Skip to content

Commit

Permalink
refactor: generalize next route error helpers (#72774)
Browse files Browse the repository at this point in the history
  • Loading branch information
huozhi authored Nov 18, 2024
1 parent e3e3461 commit 997105d
Show file tree
Hide file tree
Showing 15 changed files with 214 additions and 130 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client'

import React from 'react'
import { HTTPAccessFallbackBoundary } from './http-access-fallback-boundary'
import { HTTPAccessFallbackBoundary } from './http-access-fallback/error-boundary'

export function bailOnRootNotFound() {
throw new Error('notFound() is not allowed to use in root layout')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@
*/

import React, { useContext } from 'react'
import { useUntrackedPathname } from './navigation-untracked'
import { isNotFoundError } from './not-found'
import { warnOnce } from '../../shared/lib/utils/warn-once'
import { MissingSlotContext } from '../../shared/lib/app-router-context.shared-runtime'
import { useUntrackedPathname } from '../navigation-untracked'
import {
getAccessFallbackHTTPStatus,
isHTTPAccessFallbackError,
} from './http-access-fallback'
import { warnOnce } from '../../../shared/lib/utils/warn-once'
import { MissingSlotContext } from '../../../shared/lib/app-router-context.shared-runtime'

const HTTPErrorStatus = {
NOT_FOUND: 404,
Expand Down Expand Up @@ -77,9 +80,9 @@ class HTTPAccessFallbackErrorBoundary extends React.Component<
}

static getDerivedStateFromError(error: any) {
if (isNotFoundError(error)) {
if (isHTTPAccessFallbackError(error)) {
return {
triggeredStatus: HTTPErrorStatus.NOT_FOUND,
triggeredStatus: getAccessFallbackHTTPStatus(error),
}
}
// Re-throw if error is not for 404
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import React from 'react'

const styles: Record<string, React.CSSProperties> = {
error: {
// https://github.com/sindresorhus/modern-normalize/blob/main/modern-normalize.css#L38-L52
fontFamily:
'system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"',
height: '100vh',
textAlign: 'center',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
},

desc: {
display: 'inline-block',
},

h1: {
display: 'inline-block',
margin: '0 20px 0 0',
padding: '0 23px 0 0',
fontSize: 24,
fontWeight: 500,
verticalAlign: 'top',
lineHeight: '49px',
},

h2: {
fontSize: 14,
fontWeight: 400,
lineHeight: '49px',
margin: 0,
},
}

export function HTTPAccessErrorFallback({
status,
message,
}: {
status: number
message: string
}) {
return (
<>
{/* <head> */}
<title>{`${status}: ${message}`}</title>
{/* </head> */}
<div style={styles.error}>
<div>
<style
dangerouslySetInnerHTML={{
/* Minified CSS from
body { margin: 0; color: #000; background: #fff; }
.next-error-h1 {
border-right: 1px solid rgba(0, 0, 0, .3);
}
@media (prefers-color-scheme: dark) {
body { color: #fff; background: #000; }
.next-error-h1 {
border-right: 1px solid rgba(255, 255, 255, .3);
}
}
*/
__html: `body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}`,
}}
/>
<h1 className="next-error-h1" style={styles.h1}>
{status}
</h1>
<div style={styles.desc}>
<h2 style={styles.h2}>{message}</h2>
</div>
</div>
</div>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
const ALLOWED_CODES = new Set([404])

export const HTTP_ERROR_FALLBACK_ERROR_CODE = 'NEXT_HTTP_ERROR_FALLBACK'

export type HTTPAccessFallbackError = Error & {
digest: `${typeof HTTP_ERROR_FALLBACK_ERROR_CODE};${string}`
}

/**
* Checks an error to determine if it's an error generated by
* the HTTP navigation APIs `notFound()`, `forbidden()` or `unauthorized()`.
*
* @param error the error that may reference a HTTP access error
* @returns true if the error is a HTTP access error
*/
export function isHTTPAccessFallbackError(
error: unknown
): error is HTTPAccessFallbackError {
if (
typeof error !== 'object' ||
error === null ||
!('digest' in error) ||
typeof error.digest !== 'string'
) {
return false
}
const [prefix, httpStatus] = error.digest.split(';')

return (
prefix === HTTP_ERROR_FALLBACK_ERROR_CODE &&
ALLOWED_CODES.has(Number(httpStatus))
)
}

export function getAccessFallbackHTTPStatus(
error: HTTPAccessFallbackError
): number {
const httpStatus = error.digest.split(';')[1]
return Number(httpStatus)
}

export function getAccessFallbackErrorTypeByStatus(
status: number
): 'not-found' | undefined {
// TODO: support 403 and 401
switch (status) {
case 404:
return 'not-found'
default:
return
}
}
9 changes: 6 additions & 3 deletions packages/next/src/client/components/is-next-router-error.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { isNotFoundError, type NotFoundError } from './not-found'
import {
isHTTPAccessFallbackError,
type HTTPAccessFallbackError,
} from './http-access-fallback/http-access-fallback'
import { isRedirectError, type RedirectError } from './redirect'

/**
Expand All @@ -8,6 +11,6 @@ import { isRedirectError, type RedirectError } from './redirect'
*/
export function isNextRouterError(
error: unknown
): error is RedirectError | NotFoundError {
return isRedirectError(error) || isNotFoundError(error)
): error is RedirectError | HTTPAccessFallbackError {
return isRedirectError(error) || isHTTPAccessFallbackError(error)
}
2 changes: 1 addition & 1 deletion packages/next/src/client/components/layout-router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { ErrorBoundary } from './error-boundary'
import { matchSegment } from './match-segments'
import { handleSmoothScroll } from '../../shared/lib/router/utils/handle-smooth-scroll'
import { RedirectBoundary } from './redirect-boundary'
import { HTTPAccessFallbackBoundary } from './http-access-fallback-boundary'
import { HTTPAccessFallbackBoundary } from './http-access-fallback/error-boundary'
import { getSegmentValue } from './router-reducer/reducers/get-segment-value'
import { createRouterCacheKey } from './router-reducer/create-router-cache-key'
import { hasInterceptionRouteInCurrentTree } from './router-reducer/reducers/has-interception-route-in-current-tree'
Expand Down
74 changes: 5 additions & 69 deletions packages/next/src/client/components/not-found-error.tsx
Original file line number Diff line number Diff line change
@@ -1,74 +1,10 @@
import React from 'react'

const styles: Record<string, React.CSSProperties> = {
error: {
// https://github.com/sindresorhus/modern-normalize/blob/main/modern-normalize.css#L38-L52
fontFamily:
'system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"',
height: '100vh',
textAlign: 'center',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
},

desc: {
display: 'inline-block',
},

h1: {
display: 'inline-block',
margin: '0 20px 0 0',
padding: '0 23px 0 0',
fontSize: 24,
fontWeight: 500,
verticalAlign: 'top',
lineHeight: '49px',
},

h2: {
fontSize: 14,
fontWeight: 400,
lineHeight: '49px',
margin: 0,
},
}
import { HTTPAccessErrorFallback } from './http-access-fallback/error-fallback'

export default function NotFound() {
return (
<>
{/* <head> */}
<title>404: This page could not be found.</title>
{/* </head> */}
<div style={styles.error}>
<div>
<style
dangerouslySetInnerHTML={{
/* Minified CSS from
body { margin: 0; color: #000; background: #fff; }
.next-error-h1 {
border-right: 1px solid rgba(0, 0, 0, .3);
}
@media (prefers-color-scheme: dark) {
body { color: #fff; background: #000; }
.next-error-h1 {
border-right: 1px solid rgba(255, 255, 255, .3);
}
}
*/
__html: `body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}`,
}}
/>
<h1 className="next-error-h1" style={styles.h1}>
404
</h1>
<div style={styles.desc}>
<h2 style={styles.h2}>This page could not be found.</h2>
</div>
</div>
</div>
</>
<HTTPAccessErrorFallback
status={404}
message="This page could not be found."
/>
)
}
29 changes: 9 additions & 20 deletions packages/next/src/client/components/not-found.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const NOT_FOUND_ERROR_CODE = 'NEXT_NOT_FOUND'

export type NotFoundError = Error & { digest: typeof NOT_FOUND_ERROR_CODE }
import {
HTTP_ERROR_FALLBACK_ERROR_CODE,
type HTTPAccessFallbackError,
} from './http-access-fallback/http-access-fallback'

/**
* This function allows you to render the [not-found.js file](https://nextjs.org/docs/app/api-reference/file-conventions/not-found)
Expand All @@ -18,22 +19,10 @@ export type NotFoundError = Error & { digest: typeof NOT_FOUND_ERROR_CODE }
*/
export function notFound(): never {
// eslint-disable-next-line no-throw-literal
const error = new Error(NOT_FOUND_ERROR_CODE)
;(error as NotFoundError).digest = NOT_FOUND_ERROR_CODE
const error = new Error(
HTTP_ERROR_FALLBACK_ERROR_CODE
) as HTTPAccessFallbackError
;(error as HTTPAccessFallbackError).digest =
`${HTTP_ERROR_FALLBACK_ERROR_CODE};404`
throw error
}

/**
* Checks an error to determine if it's an error generated by the `notFound()`
* helper.
*
* @param error the error that may reference a not found error
* @returns true if the error is a not found error
*/
export function isNotFoundError(error: unknown): error is NotFoundError {
if (typeof error !== 'object' || error === null || !('digest' in error)) {
return false
}

return error.digest === NOT_FOUND_ERROR_CODE
}
13 changes: 7 additions & 6 deletions packages/next/src/lib/metadata/metadata.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,14 @@ import {
resolveMetadataItems,
accumulateMetadata,
accumulateViewport,
type MetadataErrorType,
} from './resolve-metadata'
import { MetaFilter } from './generate/meta'
import type {
ResolvedMetadata,
ResolvedViewport,
} from './types/metadata-interface'
import { isNotFoundError } from '../../client/components/not-found'
import { isHTTPAccessFallbackError } from '../../client/components/http-access-fallback/http-access-fallback'
import type { MetadataContext } from './types/resolvers'
import type { WorkStore } from '../../server/app-render/work-async-storage.external'
import {
Expand Down Expand Up @@ -61,7 +62,7 @@ export function createMetadataComponents({
metadataContext: MetadataContext
getDynamicParamFromSegment: GetDynamicParamFromSegment
appUsingSizeAdjustment: boolean
errorType?: 'not-found' | 'redirect'
errorType?: MetadataErrorType | 'redirect'
createServerParamsForMetadata: CreateServerParamsForMetadata
workStore: WorkStore
MetadataBoundary: (props: { children: React.ReactNode }) => React.ReactNode
Expand Down Expand Up @@ -96,7 +97,7 @@ export function createMetadataComponents({
try {
return await viewport()
} catch (error) {
if (!errorType && isNotFoundError(error)) {
if (!errorType && isHTTPAccessFallbackError(error)) {
try {
return await getNotFoundViewport(
tree,
Expand Down Expand Up @@ -132,7 +133,7 @@ export function createMetadataComponents({
try {
return await metadata()
} catch (error) {
if (!errorType && isNotFoundError(error)) {
if (!errorType && isHTTPAccessFallbackError(error)) {
try {
return await getNotFoundMetadata(
tree,
Expand Down Expand Up @@ -170,7 +171,7 @@ async function getResolvedMetadataImpl(
metadataContext: MetadataContext,
createServerParamsForMetadata: CreateServerParamsForMetadata,
workStore: WorkStore,
errorType?: 'not-found' | 'redirect'
errorType?: MetadataErrorType | 'redirect'
): Promise<React.ReactNode> {
const errorConvention = errorType === 'redirect' ? undefined : errorType

Expand Down Expand Up @@ -232,7 +233,7 @@ async function getResolvedViewportImpl(
getDynamicParamFromSegment: GetDynamicParamFromSegment,
createServerParamsForMetadata: CreateServerParamsForMetadata,
workStore: WorkStore,
errorType?: 'not-found' | 'redirect'
errorType?: MetadataErrorType | 'redirect'
): Promise<React.ReactNode> {
const errorConvention = errorType === 'redirect' ? undefined : errorType

Expand Down
Loading

0 comments on commit 997105d

Please sign in to comment.