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

Add headers support to custom-routes #9879

Merged
merged 6 commits into from
Jan 1, 2020
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
25 changes: 18 additions & 7 deletions packages/next/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import path from 'path'
import { pathToRegexp } from 'path-to-regexp'
import { promisify } from 'util'
import formatWebpackMessages from '../client/dev/error-overlay/format-webpack-messages'
import checkCustomRoutes from '../lib/check-custom-routes'
import checkCustomRoutes, {
RouteType,
Redirect,
Rewrite,
Header,
} from '../lib/check-custom-routes'
import { PUBLIC_DIR_MIDDLEWARE_CONFLICT } from '../lib/constants'
import { findPagesDir } from '../lib/find-pages-dir'
import { recursiveDelete } from '../lib/recursive-delete'
Expand Down Expand Up @@ -99,8 +104,9 @@ export default async function build(dir: string, conf = null): Promise<void> {
const { target } = config
const buildId = await generateBuildId(config.generateBuildId, nanoid)
const distDir = path.join(dir, config.distDir)
const rewrites = []
const redirects = []
const rewrites: Rewrite[] = []
const redirects: Redirect[] = []
const headers: Header[] = []

if (typeof config.experimental.redirects === 'function') {
redirects.push(...(await config.experimental.redirects()))
Expand All @@ -110,6 +116,10 @@ export default async function build(dir: string, conf = null): Promise<void> {
rewrites.push(...(await config.experimental.rewrites()))
checkCustomRoutes(rewrites, 'rewrite')
}
if (typeof config.experimental.headers === 'function') {
headers.push(...(await config.experimental.headers()))
checkCustomRoutes(headers, 'header')
}

if (ciEnvironment.isCI) {
const cacheDir = path.join(distDir, 'cache')
Expand Down Expand Up @@ -223,7 +233,7 @@ export default async function build(dir: string, conf = null): Promise<void> {
source: string
statusCode?: number
},
isRedirect = false
type: RouteType
) => {
const keys: any[] = []
const routeRegex = pathToRegexp(r.source, keys, {
Expand All @@ -234,7 +244,7 @@ export default async function build(dir: string, conf = null): Promise<void> {

return {
...r,
...(isRedirect
...(type === 'redirect'
? {
statusCode: r.statusCode || DEFAULT_REDIRECT_STATUS,
}
Expand All @@ -250,8 +260,9 @@ export default async function build(dir: string, conf = null): Promise<void> {
JSON.stringify({
version: 2,
basePath: config.experimental.basePath,
redirects: redirects.map(r => buildCustomRoute(r, true)),
rewrites: rewrites.map(r => buildCustomRoute(r)),
redirects: redirects.map(r => buildCustomRoute(r, 'redirect')),
rewrites: rewrites.map(r => buildCustomRoute(r, 'rewrite')),
headers: headers.map(r => buildCustomRoute(r, 'header')),
dynamicRoutes: getSortedRoutes(dynamicRoutes).map(page => ({
page,
regex: getRouteRegex(page).re.source,
Expand Down
124 changes: 90 additions & 34 deletions packages/next/lib/check-custom-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,80 @@ export type Redirect = Rewrite & {
statusCode?: number
}

export type Header = {
source: string
headers: Array<{ key: string; value: string }>
}

const allowedStatusCodes = new Set([301, 302, 303, 307, 308])

function checkRedirect(route: Redirect) {
const invalidParts: string[] = []
let hadInvalidStatus: boolean = false

if (route.statusCode && !allowedStatusCodes.has(route.statusCode)) {
hadInvalidStatus = true
invalidParts.push(`\`statusCode\` is not undefined or valid statusCode`)
}
return {
invalidParts,
hadInvalidStatus,
}
}

function checkHeader(route: Header) {
const invalidParts: string[] = []

if (!Array.isArray(route.headers)) {
invalidParts.push('`headers` field must be an array')
} else {
for (const header of route.headers) {
if (!header || typeof header !== 'object') {
invalidParts.push(
"`headers` items must be object with { key: '', value: '' }"
)
break
}
if (typeof header.key !== 'string') {
invalidParts.push('`key` in header item must be string')
break
}
if (typeof header.value !== 'string') {
invalidParts.push('`value` in header item must be string')
break
}
}
}
return invalidParts
}

export type RouteType = 'rewrite' | 'redirect' | 'header'

export default function checkCustomRoutes(
routes: Array<Rewrite | Redirect>,
type: 'redirect' | 'rewrite'
routes: Redirect[] | Header[] | Rewrite[],
type: RouteType
): void {
let numInvalidRoutes = 0
let hadInvalidStatus = false

const isRedirect = type === 'redirect'
const allowedKeys = new Set([
'source',
'destination',
...(isRedirect ? ['statusCode'] : []),
])
let allowedKeys: Set<string>

if (type === 'rewrite' || isRedirect) {
allowedKeys = new Set([
'source',
'destination',
...(isRedirect ? ['statusCode'] : []),
])
} else {
allowedKeys = new Set(['source', 'headers'])
}

for (const route of routes) {
const keys = Object.keys(route)
const invalidKeys = keys.filter(key => !allowedKeys.has(key))
const invalidParts = []
const invalidParts: string[] = []

// TODO: investigate allowing RegExp directly
if (!route.source) {
invalidParts.push('`source` is missing')
} else if (typeof route.source !== 'string') {
Expand All @@ -38,35 +91,38 @@ export default function checkCustomRoutes(
invalidParts.push('`source` does not start with /')
}

if (!route.destination) {
invalidParts.push('`destination` is missing')
} else if (typeof route.destination !== 'string') {
invalidParts.push('`destination` is not a string')
} else if (type === 'rewrite' && !route.destination.startsWith('/')) {
invalidParts.push('`destination` does not start with /')
if (type === 'header') {
invalidParts.push(...checkHeader(route as Header))
} else {
let _route = route as Rewrite | Redirect
if (!_route.destination) {
invalidParts.push('`destination` is missing')
} else if (typeof _route.destination !== 'string') {
invalidParts.push('`destination` is not a string')
} else if (type === 'rewrite' && !_route.destination.startsWith('/')) {
invalidParts.push('`destination` does not start with /')
}
}

if (isRedirect) {
const redirRoute = route as Redirect

if (
redirRoute.statusCode &&
!allowedStatusCodes.has(redirRoute.statusCode)
) {
hadInvalidStatus = true
invalidParts.push(`\`statusCode\` is not undefined or valid statusCode`)
}
if (type === 'redirect') {
const result = checkRedirect(route as Redirect)
hadInvalidStatus = result.hadInvalidStatus
invalidParts.push(...result.invalidParts)
}

try {
// Make sure we can parse the source properly
regexpMatch(route.source)
} catch (err) {
// If there is an error show our err.sh but still show original error
console.error(
`\nError parsing ${route.source} https://err.sh/zeit/next.js/invalid-route-source`,
err
)
if (typeof route.source === 'string') {
// only show parse error if we didn't already show error
// for not being a string
try {
// Make sure we can parse the source properly
regexpMatch(route.source)
} catch (err) {
// If there is an error show our err.sh but still show original error
console.error(
`\nError parsing ${route.source} https://err.sh/zeit/next.js/invalid-route-source`,
err
)
}
}

const hasInvalidKeys = invalidKeys.length > 0
Expand Down
38 changes: 32 additions & 6 deletions packages/next/next-server/server/next-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,12 @@ import { sendHTML } from './send-html'
import { serveStatic } from './serve-static'
import { getSprCache, initializeSprCache, setSprCache } from './spr-cache'
import { isBlockedPage } from './utils'
import { Redirect, Rewrite } from '../../lib/check-custom-routes'
import {
Redirect,
Rewrite,
RouteType,
Header,
} from '../../lib/check-custom-routes'

const getCustomRouteMatcher = pathMatch(true)

Expand Down Expand Up @@ -105,6 +110,7 @@ export default class Server {
protected customRoutes?: {
rewrites: Rewrite[]
redirects: Redirect[]
headers: Header[]
}

public constructor({
Expand Down Expand Up @@ -402,17 +408,36 @@ export default class Server {
const routes: Route[] = []

if (this.customRoutes) {
const { redirects, rewrites } = this.customRoutes
const { redirects, rewrites, headers } = this.customRoutes

const getCustomRoute = (
r: { source: string; destination: string; statusCode?: number },
type: 'redirect' | 'rewrite'
r: Rewrite | Redirect | Header,
type: RouteType
) => ({
...r,
type,
matcher: getCustomRouteMatcher(r.source),
})

// Headers come very first
routes.push(
...headers.map(r => {
const route = getCustomRoute(r, 'header')
return {
check: true,
match: route.matcher,
type: route.type,
name: `${route.type} ${route.source} header route`,
fn: async (_req, res, _params, _parsedUrl) => {
for (const header of (route as Header).headers) {
res.setHeader(header.key, header.value)
}
return { finished: false }
},
} as Route
})
)

const customRoutes = [
...redirects.map(r => getCustomRoute(r, 'redirect')),
...rewrites.map(r => getCustomRoute(r, 'rewrite')),
Expand All @@ -424,7 +449,7 @@ export default class Server {
check: true,
match: route.matcher,
type: route.type,
statusCode: route.statusCode,
statusCode: (route as Redirect).statusCode,
name: `${route.type} ${route.source} route`,
fn: async (_req, res, params, _parsedUrl) => {
const parsedDestination = parseUrl(route.destination, true)
Expand Down Expand Up @@ -458,7 +483,8 @@ export default class Server {
hash: parsedNewUrl.hash,
})
)
res.statusCode = route.statusCode || DEFAULT_REDIRECT_STATUS
res.statusCode =
(route as Redirect).statusCode || DEFAULT_REDIRECT_STATUS
res.end()
return {
finished: true,
Expand Down
8 changes: 7 additions & 1 deletion packages/next/server/next-dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,8 +315,9 @@ export default class DevServer extends Server {
const result = {
redirects: [],
rewrites: [],
headers: [],
}
const { redirects, rewrites } = this.nextConfig.experimental
const { redirects, rewrites, headers } = this.nextConfig.experimental

if (typeof redirects === 'function') {
result.redirects = await redirects()
Expand All @@ -326,6 +327,11 @@ export default class DevServer extends Server {
result.rewrites = await rewrites()
checkCustomRoutes(result.rewrites, 'rewrite')
}
if (typeof headers === 'function') {
result.headers = await headers()
checkCustomRoutes(result.headers, 'header')
}

this.customRoutes = result
}

Expand Down
31 changes: 31 additions & 0 deletions test/integration/custom-routes/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,5 +110,36 @@ module.exports = {
},
]
},

async headers() {
return [
{
source: '/add-header',
headers: [
{
key: 'x-custom-header',
value: 'hello world',
},
{
key: 'x-another-header',
value: 'hello again',
},
],
},
{
source: '/my-headers/(.*)',
headers: [
{
key: 'x-first-header',
value: 'first',
},
{
key: 'x-second-header',
value: 'second',
},
],
},
]
},
},
}
Loading