Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: generalize next route error helpers #72774

Merged
merged 10 commits into from
Nov 18, 2024
Merged
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
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
Loading