From 6652d783e42d5ee8b64777f59c365604cadd8c9d Mon Sep 17 00:00:00 2001 From: Meno Abels Date: Wed, 12 Oct 2022 14:02:25 +0200 Subject: [PATCH 1/5] proper error if middleware or api/route not return a Response (#41336) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Bug The error which is thrown if the fetch method returns not a falsy or Response value is misleading. Co-authored-by: Balázs Orbán --- packages/next/server/web/adapter.ts | 5 ++ .../edge-runtime-response-error/lib.js | 1 + .../edge-runtime-response-error/middleware.js | 6 ++ .../pages/api/route.js | 5 ++ .../pages/index.js | 3 + .../test/index.test.js | 88 +++++++++++++++++++ 6 files changed, 108 insertions(+) create mode 100644 test/integration/edge-runtime-response-error/lib.js create mode 100644 test/integration/edge-runtime-response-error/middleware.js create mode 100644 test/integration/edge-runtime-response-error/pages/api/route.js create mode 100644 test/integration/edge-runtime-response-error/pages/index.js create mode 100644 test/integration/edge-runtime-response-error/test/index.test.js diff --git a/packages/next/server/web/adapter.ts b/packages/next/server/web/adapter.ts index ddd8213f9906e..49a8ec2b52887 100644 --- a/packages/next/server/web/adapter.ts +++ b/packages/next/server/web/adapter.ts @@ -106,6 +106,11 @@ export async function adapter(params: { const event = new NextFetchEvent({ request, page: params.page }) let response = await params.handler(request, event) + // check if response is a Response object + if (response && !(response instanceof Response)) { + throw new TypeError('Expected an instance of Response to be returned') + } + /** * For rewrites we must always include the locale in the final pathname * so we re-create the NextURL forcing it to include it when the it is diff --git a/test/integration/edge-runtime-response-error/lib.js b/test/integration/edge-runtime-response-error/lib.js new file mode 100644 index 0000000000000..8cf56ac11d991 --- /dev/null +++ b/test/integration/edge-runtime-response-error/lib.js @@ -0,0 +1 @@ +// populated with tests diff --git a/test/integration/edge-runtime-response-error/middleware.js b/test/integration/edge-runtime-response-error/middleware.js new file mode 100644 index 0000000000000..172672681fb93 --- /dev/null +++ b/test/integration/edge-runtime-response-error/middleware.js @@ -0,0 +1,6 @@ +// populated with tests +export default () => { + return 'Boom' +} + +export const config = { matcher: '/' } diff --git a/test/integration/edge-runtime-response-error/pages/api/route.js b/test/integration/edge-runtime-response-error/pages/api/route.js new file mode 100644 index 0000000000000..a1ff06c9a8fc6 --- /dev/null +++ b/test/integration/edge-runtime-response-error/pages/api/route.js @@ -0,0 +1,5 @@ +export default async function handler(request) { + return 'Boom' +} + +export const config = { runtime: 'experimental-edge' } diff --git a/test/integration/edge-runtime-response-error/pages/index.js b/test/integration/edge-runtime-response-error/pages/index.js new file mode 100644 index 0000000000000..c5cc676685b67 --- /dev/null +++ b/test/integration/edge-runtime-response-error/pages/index.js @@ -0,0 +1,3 @@ +export default function Page() { + return
ok
+} diff --git a/test/integration/edge-runtime-response-error/test/index.test.js b/test/integration/edge-runtime-response-error/test/index.test.js new file mode 100644 index 0000000000000..ad57562801e83 --- /dev/null +++ b/test/integration/edge-runtime-response-error/test/index.test.js @@ -0,0 +1,88 @@ +/* eslint-disable jest/no-identical-title */ +/* eslint-env jest */ + +import { remove } from 'fs-extra' +import { join } from 'path' +import { + fetchViaHTTP, + File, + findPort, + killApp, + launchApp, + nextBuild, + nextStart, +} from 'next-test-utils' + +jest.setTimeout(1000 * 60 * 2) + +const context = { + appDir: join(__dirname, '../'), + logs: { output: '', stdout: '', stderr: '' }, + api: new File(join(__dirname, '../pages/api/route.js')), + lib: new File(join(__dirname, '../lib.js')), + middleware: new File(join(__dirname, '../middleware.js')), + page: new File(join(__dirname, '../pages/index.js')), +} +const appOption = { + env: { __NEXT_TEST_WITH_DEVTOOL: 1 }, + onStdout(msg) { + context.logs.output += msg + context.logs.stdout += msg + }, + onStderr(msg) { + context.logs.output += msg + context.logs.stderr += msg + }, +} +const routeUrl = '/api/route' +const middlewareUrl = '/' + +describe('Edge runtime code with imports', () => { + beforeEach(async () => { + context.appPort = await findPort() + context.logs = { output: '', stdout: '', stderr: '' } + await remove(join(__dirname, '../.next')) + }) + + afterEach(() => { + if (context.app) { + killApp(context.app) + } + context.api.restore() + context.middleware.restore() + context.lib.restore() + context.page.restore() + }) + + describe.each([ + { + title: 'Edge API', + url: routeUrl, + }, + { + title: 'Middleware', + url: middlewareUrl, + }, + ])('test error if response is not Response type', ({ title, url }) => { + it(`${title} dev test Response`, async () => { + context.app = await launchApp(context.appDir, context.appPort, appOption) + const res = await fetchViaHTTP(context.appPort, url) + expect(context.logs.stderr).toContain( + 'Expected an instance of Response to be returned' + ) + expect(res.status).toBe(500) + }) + + it(`${title} build test Response`, async () => { + await nextBuild(context.appDir, undefined, { + stderr: true, + }) + context.app = await nextStart(context.appDir, context.appPort, appOption) + const res = await fetchViaHTTP(context.appPort, url) + expect(context.logs.stderr).toContain( + 'Expected an instance of Response to be returned' + ) + expect(res.status).toBe(500) + }) + }) +}) From 9b106db25a861f31f58e394826d1a0984a9c4801 Mon Sep 17 00:00:00 2001 From: Hong-Kuan Wu Date: Wed, 12 Oct 2022 20:56:37 +0800 Subject: [PATCH 2/5] add Cloudflare Turnstile example (#41283) ## Description close #41110 ## Documentation / Examples - [x] Make sure the linting passes by running `pnpm lint` - [x] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md) --- .../cloudflare-turnstile/.env.local.example | 5 ++ examples/cloudflare-turnstile/.gitignore | 36 ++++++++++++ examples/cloudflare-turnstile/README.md | 55 +++++++++++++++++++ examples/cloudflare-turnstile/app.css | 30 ++++++++++ examples/cloudflare-turnstile/next.config.js | 10 ++++ examples/cloudflare-turnstile/package.json | 19 +++++++ examples/cloudflare-turnstile/pages/_app.tsx | 6 ++ .../cloudflare-turnstile/pages/api/handler.ts | 18 ++++++ .../cloudflare-turnstile/pages/explicit.tsx | 43 +++++++++++++++ .../cloudflare-turnstile/pages/implicit.tsx | 24 ++++++++ examples/cloudflare-turnstile/tsconfig.json | 20 +++++++ 11 files changed, 266 insertions(+) create mode 100644 examples/cloudflare-turnstile/.env.local.example create mode 100644 examples/cloudflare-turnstile/.gitignore create mode 100644 examples/cloudflare-turnstile/README.md create mode 100644 examples/cloudflare-turnstile/app.css create mode 100644 examples/cloudflare-turnstile/next.config.js create mode 100644 examples/cloudflare-turnstile/package.json create mode 100644 examples/cloudflare-turnstile/pages/_app.tsx create mode 100644 examples/cloudflare-turnstile/pages/api/handler.ts create mode 100644 examples/cloudflare-turnstile/pages/explicit.tsx create mode 100644 examples/cloudflare-turnstile/pages/implicit.tsx create mode 100644 examples/cloudflare-turnstile/tsconfig.json diff --git a/examples/cloudflare-turnstile/.env.local.example b/examples/cloudflare-turnstile/.env.local.example new file mode 100644 index 0000000000000..7e3b3485ce38f --- /dev/null +++ b/examples/cloudflare-turnstile/.env.local.example @@ -0,0 +1,5 @@ +# Public Environment variables that can be used in the browser. +NEXT_PUBLIC_CLOUDFLARE_TURNSTILE_SITE_KEY= + +# Secret environment variables only available to Node.js +CLOUDFLARE_TURNSTILE_SECRET_KEY= diff --git a/examples/cloudflare-turnstile/.gitignore b/examples/cloudflare-turnstile/.gitignore new file mode 100644 index 0000000000000..c87c9b392c020 --- /dev/null +++ b/examples/cloudflare-turnstile/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/examples/cloudflare-turnstile/README.md b/examples/cloudflare-turnstile/README.md new file mode 100644 index 0000000000000..af59d58eb4fb1 --- /dev/null +++ b/examples/cloudflare-turnstile/README.md @@ -0,0 +1,55 @@ +# Example with Cloudflare Turnstile + +[Turnstile](https://developers.cloudflare.com/turnstile/) is Cloudflare’s smart CAPTCHA alternative. It can be embedded into any website without sending traffic through Cloudflare and works without showing visitors a CAPTCHA. + +This example shows how you can use **Cloudflare Turnstile** with your Next.js project. You can see a [live version here](https://with-cloudflare-turnstile.vercel.app/). + +## Deploy your own + +Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example). + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/cloudflare-turnstile&project-name=cloudflare-turnstile&repository-name=cloudflare-turnstile) + +**Important**: When you import your project on Vercel, make sure to click on **Environment Variable**s and set them to match your .env.local file. + +## How to use + +Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init), [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/), or [pnpm](https://pnpm.io) to bootstrap the example: + +```bash +npx create-next-app --example cloudflare-turnstile cloudflare-turnstile-app +``` + +```bash +yarn create next-app --example cloudflare-turnstile cloudflare-turnstile-app +``` + +```bash +pnpm create next-app --example cloudflare-turnstile cloudflare-turnstile-app +``` + +Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)). + +## Configuring Cloudflare Turnstile + +### Get a sitekey and secret key + +1. Go to the [Cloudflare dashboard](https://dash.cloudflare.com/?to=/:account/turnstile) and select your account. +2. Go to Turnstile. +3. Select Add a site and fill out the form. +4. Copy your **Site Key** and **Secret Key**. + +### Set up environment variables + +To connect the app with Cloudflare Turnstile, you'll need to add the settings from your Cloudflare dashboard as environment variables + +Copy the .env.local.example file in this directory to .env.local. + +```bash +cp .env.local.example .env.local +``` + +Then, open .env.local and fill these environment variables: + +- `NEXT_PUBLIC_CLOUDFLARE_TURNSTILE_SITE_KEY` +- `CLOUDFLARE_TURNSTILE_SECRET_KEY` diff --git a/examples/cloudflare-turnstile/app.css b/examples/cloudflare-turnstile/app.css new file mode 100644 index 0000000000000..e278079ff756a --- /dev/null +++ b/examples/cloudflare-turnstile/app.css @@ -0,0 +1,30 @@ +html, +body { + display: grid; + place-content: center; + margin: 0; + height: 100%; +} + +main { + height: 50vh; + width: 300px; + text-align: center; +} + +button[type='submit'] { + cursor: pointer; + width: 100%; + outline: none; + border: none; + padding: 0.5rem 1rem; + margin-top: 1rem; + border-radius: 0.25rem; + background: #0d6efd; + font-size: 1.25rem; + color: #ffffff; +} + +.checkbox { + min-height: 70px; +} diff --git a/examples/cloudflare-turnstile/next.config.js b/examples/cloudflare-turnstile/next.config.js new file mode 100644 index 0000000000000..4488de1db71ec --- /dev/null +++ b/examples/cloudflare-turnstile/next.config.js @@ -0,0 +1,10 @@ +module.exports = { + async rewrites() { + return [ + { + source: '/', + destination: '/implicit', + }, + ] + }, +} diff --git a/examples/cloudflare-turnstile/package.json b/examples/cloudflare-turnstile/package.json new file mode 100644 index 0000000000000..9fa64a28dc61a --- /dev/null +++ b/examples/cloudflare-turnstile/package.json @@ -0,0 +1,19 @@ +{ + "private": true, + "scripts": { + "dev": "next", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "next": "latest", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/node": "^18.0.0", + "@types/react": "^18.0.14", + "@types/react-dom": "^18.0.5", + "typescript": "^4.7.4" + } +} diff --git a/examples/cloudflare-turnstile/pages/_app.tsx b/examples/cloudflare-turnstile/pages/_app.tsx new file mode 100644 index 0000000000000..7a774ea733105 --- /dev/null +++ b/examples/cloudflare-turnstile/pages/_app.tsx @@ -0,0 +1,6 @@ +import type { AppProps } from 'next/app' +import '../app.css' + +export default function App({ Component, pageProps }: AppProps) { + return +} diff --git a/examples/cloudflare-turnstile/pages/api/handler.ts b/examples/cloudflare-turnstile/pages/api/handler.ts new file mode 100644 index 0000000000000..00368bee542aa --- /dev/null +++ b/examples/cloudflare-turnstile/pages/api/handler.ts @@ -0,0 +1,18 @@ +import type { NextApiRequest, NextApiResponse } from 'next' + +export default async function Handler( + req: NextApiRequest, + res: NextApiResponse +) { + const form = new URLSearchParams() + form.append('secret', process.env.CLOUDFLARE_TURNSTILE_SECRET_KEY) + form.append('response', req.body['cf-turnstile-response']) + form.append('remoteip', req.headers['x-forwarded-for'] as string) + + const result = await fetch( + 'https://challenges.cloudflare.com/turnstile/v0/siteverify', + { method: 'POST', body: form } + ) + const json = await result.json() + res.status(result.status).json(json) +} diff --git a/examples/cloudflare-turnstile/pages/explicit.tsx b/examples/cloudflare-turnstile/pages/explicit.tsx new file mode 100644 index 0000000000000..fc2409ee72f7a --- /dev/null +++ b/examples/cloudflare-turnstile/pages/explicit.tsx @@ -0,0 +1,43 @@ +import Script from 'next/script' + +type RenderParameters = { + sitekey: string + theme?: 'light' | 'dark' + callback?(token: string): void +} + +declare global { + interface Window { + onloadTurnstileCallback(): void + turnstile: { + render(container: string | HTMLElement, params: RenderParameters): void + } + } +} + +export default function ExplicitRender() { + return ( +
+ +