diff --git a/docs/advanced-features/preview-mode.md b/docs/advanced-features/preview-mode.md index 18bda9c383f46..dd4065482248c 100644 --- a/docs/advanced-features/preview-mode.md +++ b/docs/advanced-features/preview-mode.md @@ -111,8 +111,7 @@ export default async (req, res) => { // Redirect to the path from the fetched post // We don't redirect to req.query.slug as that might lead to open redirect vulnerabilities - res.writeHead(307, { Location: post.slug }) - res.end() + res.redirect(post.slug) } ``` diff --git a/docs/api-routes/response-helpers.md b/docs/api-routes/response-helpers.md index 93fd1623f03ea..9912571a659b7 100644 --- a/docs/api-routes/response-helpers.md +++ b/docs/api-routes/response-helpers.md @@ -25,3 +25,4 @@ The included helpers are: - `res.status(code)` - A function to set the status code. `code` must be a valid [HTTP status code](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes) - `res.json(json)` - Sends a JSON response. `json` must be a valid JSON object - `res.send(body)` - Sends the HTTP response. `body` can be a `string`, an `object` or a `Buffer` +- `res.redirect([status,] path)` - Redirects to a specified path or URL. `status` must be a valid [HTTP status code](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes). If not specified, `status` defaults to "302" "Found". diff --git a/packages/next/next-server/lib/utils.ts b/packages/next/next-server/lib/utils.ts index e820d1d3616fe..6dee4015678c3 100644 --- a/packages/next/next-server/lib/utils.ts +++ b/packages/next/next-server/lib/utils.ts @@ -225,6 +225,7 @@ export type NextApiResponse = ServerResponse & { */ json: Send status: (statusCode: number) => NextApiResponse + redirect: (statusOrUrl: string | number, url?: string) => NextApiResponse /** * Set preview data for Next.js' prerender mode diff --git a/packages/next/next-server/server/api-utils.ts b/packages/next/next-server/server/api-utils.ts index 1a707ad63dcff..930412aeb20a6 100644 --- a/packages/next/next-server/server/api-utils.ts +++ b/packages/next/next-server/server/api-utils.ts @@ -68,6 +68,7 @@ export async function apiResolver( apiRes.status = (statusCode) => sendStatusCode(apiRes, statusCode) apiRes.send = (data) => sendData(apiReq, apiRes, data) apiRes.json = (data) => sendJson(apiRes, data) + apiRes.redirect = (statusOrUrl, url) => redirect(apiRes, statusOrUrl, url) apiRes.setPreviewData = (data, options = {}) => setPreviewData(apiRes, data, Object.assign({}, apiContext, options)) apiRes.clearPreviewData = () => clearPreviewData(apiRes) @@ -218,6 +219,26 @@ export function sendStatusCode( return res } +/** + * + * @param res response object + * @param [statusOrUrl] `HTTP` status code of redirect + * @param url URL of redirect + */ +export function redirect( + res: NextApiResponse, + statusOrUrl: string | number, + url?: string +): NextApiResponse { + if (typeof statusOrUrl === 'string') { + url = statusOrUrl + statusOrUrl = 307 + } + + res.writeHead(statusOrUrl, { Location: url }).end() + return res +} + function sendEtagResponse( req: NextApiRequest, res: NextApiResponse, diff --git a/test/integration/api-support/pages/api/redirect-301.js b/test/integration/api-support/pages/api/redirect-301.js new file mode 100644 index 0000000000000..49875945bdecf --- /dev/null +++ b/test/integration/api-support/pages/api/redirect-301.js @@ -0,0 +1,3 @@ +export default (req, res) => { + res.redirect(301, '/login') +} diff --git a/test/integration/api-support/pages/api/redirect-307.js b/test/integration/api-support/pages/api/redirect-307.js new file mode 100644 index 0000000000000..1bee2516cbe8a --- /dev/null +++ b/test/integration/api-support/pages/api/redirect-307.js @@ -0,0 +1,3 @@ +export default (req, res) => { + res.redirect('/login') +} diff --git a/test/integration/api-support/test/index.test.js b/test/integration/api-support/test/index.test.js index 9252311a1c62b..2694f18314777 100644 --- a/test/integration/api-support/test/index.test.js +++ b/test/integration/api-support/test/index.test.js @@ -236,6 +236,29 @@ function runTests(dev = false) { expect(data).toEqual({ message: 'Parsed body' }) }) + it('should redirect with status code 307', async () => { + const res = await fetchViaHTTP(appPort, '/api/redirect-307', null, { + redirect: 'manual', + }) + + expect(res.status).toEqual(307) + }) + + it('should redirect to login', async () => { + const res = await fetchViaHTTP(appPort, '/api/redirect-307', null, {}) + + expect(res.redirected).toBe(true) + expect(res.url).toContain('/login') + }) + + it('should redirect with status code 301', async () => { + const res = await fetchViaHTTP(appPort, '/api/redirect-301', null, { + redirect: 'manual', + }) + + expect(res.status).toEqual(301) + }) + it('should return empty query object', async () => { const data = await fetchViaHTTP(appPort, '/api/query', null, {}).then( (res) => res.ok && res.json()