diff --git a/.github/workflows/test-deno.yml b/.github/workflows/test-deno.yml new file mode 100644 index 0000000000..6c15856206 --- /dev/null +++ b/.github/workflows/test-deno.yml @@ -0,0 +1,21 @@ +name: Deno tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Git Checkout Deno Module + uses: actions/checkout@v2 + - name: Use Deno Version ${{ matrix.deno-version }} + uses: denolib/setup-deno@v2 + with: + deno-version: vx.x.x + - name: Test Deno + run: deno test packages/runtime/src/templates/edge-shared/ diff --git a/cypress/integration/middleware/enhanced.spec.ts b/cypress/integration/middleware/enhanced.spec.ts index 738353e16a..4d5f1f0b87 100644 --- a/cypress/integration/middleware/enhanced.spec.ts +++ b/cypress/integration/middleware/enhanced.spec.ts @@ -1,16 +1,4 @@ describe('Enhanced middleware', () => { - it('adds request headers', () => { - cy.request('/api/hello').then((response) => { - expect(response.body).to.have.nested.property('headers.x-hello', 'world') - }) - }) - - it('adds request headers to a rewrite', () => { - cy.request('/headers').then((response) => { - expect(response.body).to.have.nested.property('headers.x-hello', 'world') - }) - }) - it('rewrites the response body', () => { cy.visit('/static') cy.get('#message').contains('This was static but has been transformed in') diff --git a/cypress/integration/middleware/standard.spec.ts b/cypress/integration/middleware/standard.spec.ts index f97392b165..97387b7ca0 100644 --- a/cypress/integration/middleware/standard.spec.ts +++ b/cypress/integration/middleware/standard.spec.ts @@ -1,4 +1,16 @@ describe('Standard middleware', () => { + it('adds request headers', () => { + cy.request('/api/hello').then((response) => { + expect(response.body).to.have.nested.property('headers.x-hello', 'world') + }) + }) + + it('adds request headers to a rewrite', () => { + cy.request('/headers').then((response) => { + expect(response.body).to.have.nested.property('headers.x-hello', 'world') + }) + }) + it('rewrites to internal page', () => { // preview mode is off by default cy.visit('/shows/rewriteme') diff --git a/demos/middleware/middleware.ts b/demos/middleware/middleware.ts index 93c43a0c62..4ef5240597 100644 --- a/demos/middleware/middleware.ts +++ b/demos/middleware/middleware.ts @@ -8,6 +8,18 @@ export async function middleware(req: NextRequest) { let response const { pathname } = req.nextUrl + if (pathname.startsWith('/api/hello')) { + // Next 13 request header mutation functionality + const headers = new Headers(req.headers) + + headers.set('x-hello', 'world') + return NextResponse.next({ + request: { + headers + } + }) + } + const request = new MiddlewareRequest(req) if (pathname.startsWith('/static')) { // Unlike NextResponse.next(), this actually sends the request to the origin @@ -24,12 +36,6 @@ export async function middleware(req: NextRequest) { return res } - if (pathname.startsWith('/api/hello')) { - // Add a header to the request - req.headers.set('x-hello', 'world') - return request.next() - } - if (pathname.startsWith('/api/geo')) { req.headers.set('x-geo-country', req.geo.country) req.headers.set('x-geo-region', req.geo.region) diff --git a/package-lock.json b/package-lock.json index 390d779dc0..fa001c15f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24571,6 +24571,201 @@ "engines": { "node": ">=10" } + }, + "node_modules/@next/swc-android-arm-eabi": { + "version": "13.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-13.0.7.tgz", + "integrity": "sha512-QTEamOK/LCwBf05GZ261rULMbZEpE3TYdjHlXfznV+nXwTztzkBNFXwP67gv2wW44BROzgi/vrR9H8oP+J5jxg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-android-arm64": { + "version": "13.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-13.0.7.tgz", + "integrity": "sha512-wcy2H0Tl9ME8vKy2GnJZ7Mybwys+43F/Eh2Pvph7mSDpMbYBJ6iA0zeY62iYYXxlZhnAID3+h79FUqUEakkClw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "13.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.0.7.tgz", + "integrity": "sha512-F/mU7csN1/J2cqXJPMgTQ6MwAbc1pJ6sp6W+X0z5JEY4IFDzxKd3wRc3pCiNF7j8xW381JlNpWxhjCctnNmfaw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "13.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.0.7.tgz", + "integrity": "sha512-636AuRQynCPnIPRVzcCk5B7OMq9XjaYam2T0HeWUCE6y7EqEO3kxiuZ4QmN81T7A6Ydb+JnivYrLelHXmgdj6A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-freebsd-x64": { + "version": "13.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-13.0.7.tgz", + "integrity": "sha512-92XAMzNgQazowZ9t7uZmHRA5VdBl/SwEdrf5UybdfRovsxB4r3+yJWEvFaqYpSEp0gwndbwLokJdpz7OwFdL3Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm-gnueabihf": { + "version": "13.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-13.0.7.tgz", + "integrity": "sha512-3r1CWl5P6I5n5Yxip8EXv/Rfu2Cp6wVmIOpvmczyUR82j+bcMkwPAcUjNkG/vMCagS4xV7NElrcdGb39iFmfLg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "13.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.0.7.tgz", + "integrity": "sha512-RXo8tt6ppiwyS6hpDw3JdAjKcdVewsefxnxk9xOH4mRhMyq9V2lQx0e24X/dRiZqkx3jnWReR2WRrUlgN1UkSQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "13.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.0.7.tgz", + "integrity": "sha512-RWpnW+bmfXyxyY7iARbueYDGuIF+BEp3etLeYh/RUNHb9PhOHLDgJOG8haGSykud3a6CcyBI8hEjqOhoObaDpw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "13.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.0.7.tgz", + "integrity": "sha512-/ygUIiMMTYnbKlFs5Ba9J5k/tNxFWy8eI1bBF8UuMTvV8QJHl/aLDiA5dwsei2kk99/cu3eay62JnJXkSk3RSQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "13.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.0.7.tgz", + "integrity": "sha512-dLzr6AL77USJN0ejgx5AS8O8SbFlbYTzs0XwAWag4oQpUG2p3ARvxwQgYQ0Z+6EP0zIRZ/XfLkN/mhsyi3m4PA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "13.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.0.7.tgz", + "integrity": "sha512-+vFIVa82AwqFkpFClKT+n73fGxrhAZ2u1u3mDYEBdxO6c9U4Pj3S5tZFsGFK9kLT/bFvf/eeVOICSLCC7MSgJQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "13.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.0.7.tgz", + "integrity": "sha512-RNLXIhp+assD39dQY9oHhDxw+/qSJRARKhOFsHfOtf8yEfCHqcKkn3X/L+ih60ntaEqK294y1WkMk6ylotsxwA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "13.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.0.7.tgz", + "integrity": "sha512-kvdnlLcrnEq72ZP0lqe2Z5NqvB9N5uSCvtXJ0PhKvNncWWd0fEG9Ec9erXgwCmVlM2ytw41k9/uuQ+SVw4Pihw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } } }, "dependencies": { @@ -42820,6 +43015,84 @@ "compress-commons": "^4.1.0", "readable-stream": "^3.6.0" } + }, + "@next/swc-android-arm-eabi": { + "version": "13.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-13.0.7.tgz", + "integrity": "sha512-QTEamOK/LCwBf05GZ261rULMbZEpE3TYdjHlXfznV+nXwTztzkBNFXwP67gv2wW44BROzgi/vrR9H8oP+J5jxg==", + "optional": true + }, + "@next/swc-android-arm64": { + "version": "13.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-13.0.7.tgz", + "integrity": "sha512-wcy2H0Tl9ME8vKy2GnJZ7Mybwys+43F/Eh2Pvph7mSDpMbYBJ6iA0zeY62iYYXxlZhnAID3+h79FUqUEakkClw==", + "optional": true + }, + "@next/swc-darwin-arm64": { + "version": "13.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.0.7.tgz", + "integrity": "sha512-F/mU7csN1/J2cqXJPMgTQ6MwAbc1pJ6sp6W+X0z5JEY4IFDzxKd3wRc3pCiNF7j8xW381JlNpWxhjCctnNmfaw==", + "optional": true + }, + "@next/swc-darwin-x64": { + "version": "13.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.0.7.tgz", + "integrity": "sha512-636AuRQynCPnIPRVzcCk5B7OMq9XjaYam2T0HeWUCE6y7EqEO3kxiuZ4QmN81T7A6Ydb+JnivYrLelHXmgdj6A==", + "optional": true + }, + "@next/swc-freebsd-x64": { + "version": "13.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-13.0.7.tgz", + "integrity": "sha512-92XAMzNgQazowZ9t7uZmHRA5VdBl/SwEdrf5UybdfRovsxB4r3+yJWEvFaqYpSEp0gwndbwLokJdpz7OwFdL3Q==", + "optional": true + }, + "@next/swc-linux-arm-gnueabihf": { + "version": "13.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-13.0.7.tgz", + "integrity": "sha512-3r1CWl5P6I5n5Yxip8EXv/Rfu2Cp6wVmIOpvmczyUR82j+bcMkwPAcUjNkG/vMCagS4xV7NElrcdGb39iFmfLg==", + "optional": true + }, + "@next/swc-linux-arm64-gnu": { + "version": "13.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.0.7.tgz", + "integrity": "sha512-RXo8tt6ppiwyS6hpDw3JdAjKcdVewsefxnxk9xOH4mRhMyq9V2lQx0e24X/dRiZqkx3jnWReR2WRrUlgN1UkSQ==", + "optional": true + }, + "@next/swc-linux-arm64-musl": { + "version": "13.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.0.7.tgz", + "integrity": "sha512-RWpnW+bmfXyxyY7iARbueYDGuIF+BEp3etLeYh/RUNHb9PhOHLDgJOG8haGSykud3a6CcyBI8hEjqOhoObaDpw==", + "optional": true + }, + "@next/swc-linux-x64-gnu": { + "version": "13.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.0.7.tgz", + "integrity": "sha512-/ygUIiMMTYnbKlFs5Ba9J5k/tNxFWy8eI1bBF8UuMTvV8QJHl/aLDiA5dwsei2kk99/cu3eay62JnJXkSk3RSQ==", + "optional": true + }, + "@next/swc-linux-x64-musl": { + "version": "13.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.0.7.tgz", + "integrity": "sha512-dLzr6AL77USJN0ejgx5AS8O8SbFlbYTzs0XwAWag4oQpUG2p3ARvxwQgYQ0Z+6EP0zIRZ/XfLkN/mhsyi3m4PA==", + "optional": true + }, + "@next/swc-win32-arm64-msvc": { + "version": "13.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.0.7.tgz", + "integrity": "sha512-+vFIVa82AwqFkpFClKT+n73fGxrhAZ2u1u3mDYEBdxO6c9U4Pj3S5tZFsGFK9kLT/bFvf/eeVOICSLCC7MSgJQ==", + "optional": true + }, + "@next/swc-win32-ia32-msvc": { + "version": "13.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.0.7.tgz", + "integrity": "sha512-RNLXIhp+assD39dQY9oHhDxw+/qSJRARKhOFsHfOtf8yEfCHqcKkn3X/L+ih60ntaEqK294y1WkMk6ylotsxwA==", + "optional": true + }, + "@next/swc-win32-x64-msvc": { + "version": "13.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.0.7.tgz", + "integrity": "sha512-kvdnlLcrnEq72ZP0lqe2Z5NqvB9N5uSCvtXJ0PhKvNncWWd0fEG9Ec9erXgwCmVlM2ytw41k9/uuQ+SVw4Pihw==", + "optional": true } } } diff --git a/package.json b/package.json index 1e6110e318..d22d51f745 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "postinstall": "run-s build install-husky", "install-husky": "if-env CI=1 || husky install node_modules/@netlify/eslint-config-node/.husky", "test": "run-s build:demo test:jest", + "test:deno": "deno test packages/runtime/src/templates/edge-shared/", "test:next": "jest -c test/e2e/jest.config.js", "test:next:disabled": "RUN_SKIPPED_TESTS=1 jest -c test/e2e/jest.config.disabled.js", "test:next:all": "RUN_SKIPPED_TESTS=1 jest -c test/e2e/jest.config.all.js", @@ -101,7 +102,8 @@ "**/test/**/*.spec.ts", "!**/test/e2e/**", "!**/test/fixtures/**", - "!**/test/sample/**" + "!**/test/sample/**", + "!**/test/templates/edge-shared/**" ], "transform": { "\\.[jt]sx?$": "babel-jest" diff --git a/packages/runtime/src/templates/edge-shared/utils.test.ts b/packages/runtime/src/templates/edge-shared/utils.test.ts new file mode 100644 index 0000000000..9c988f6816 --- /dev/null +++ b/packages/runtime/src/templates/edge-shared/utils.test.ts @@ -0,0 +1,64 @@ +import { assertEquals } from 'https://deno.land/std@0.167.0/testing/asserts.ts' +import { beforeEach, describe, it } from 'https://deno.land/std@0.167.0/testing/bdd.ts' +import { updateModifiedHeaders, FetchEventResult } from './utils.ts' + +describe('updateModifiedHeaders', () => { + it("does not modify the headers if 'x-middleware-override-headers' is not found", () => { + const mockHeaders = new Headers() + // There shouldn't be a case where x-middleware-override-headers is not set and a header has + // been modified with 'x-middleware-request' added to it, this is more to confirm the test case + mockHeaders.set('x-middleware-request-foo', 'bar') + + const mockResponse = { + headers: mockHeaders, + } + + const mockRequest = { + headers: new Headers(), + } + + updateModifiedHeaders(mockRequest.headers, mockResponse.headers) + + assertEquals(mockRequest.headers.get('x-middleware-request-foo'), null) + }) + + describe("when the 'x-middleware-override-headers' headers is present", () => { + let mockHeaders + let mockRequest: { headers: Headers } + let mockResponse: { headers: Headers } + + beforeEach(() => { + mockHeaders = new Headers() + mockHeaders.set('foo', 'bar') + mockHeaders.set('x-middleware-request-hello', 'world') + mockHeaders.set('x-middleware-request-test', '123') + mockHeaders.set('x-middleware-override-headers', 'hello,test') + + mockRequest = { + headers: new Headers(), + } + + mockResponse = { + headers: mockHeaders, + } + + updateModifiedHeaders(mockRequest.headers, mockResponse.headers) + }) + + it("does not modify or add headers that are missing 'x-middleware-request' in the name", () => { + assertEquals(mockRequest.headers.get('foo'), null) + }) + + it("removes 'x-middleware-request-' from headers", () => { + assertEquals(mockRequest.headers.get('x-middleware-request-hello'), null) + assertEquals(mockRequest.headers.get('x-middleware-request-test'), null) + + assertEquals(mockRequest.headers.get('hello'), 'world') + assertEquals(mockRequest.headers.get('test'), '123') + }) + + it("removes 'x-middleware-override-headers' after cleaning headers", () => { + assertEquals(mockRequest.headers.get('x-middleware-override-headers'), null) + }) + }) +}) diff --git a/packages/runtime/src/templates/edge-shared/utils.ts b/packages/runtime/src/templates/edge-shared/utils.ts index 4d9b95cde6..1dc9153cc8 100644 --- a/packages/runtime/src/templates/edge-shared/utils.ts +++ b/packages/runtime/src/templates/edge-shared/utils.ts @@ -59,6 +59,32 @@ function isMiddlewareResponse(response: Response | MiddlewareResponse): response return 'dataTransforms' in response } +// Next 13 supports request header mutations and has the side effect of prepending header values with 'x-middleware-request' +// as part of invoking NextResponse.next() in the middleware. We need to remove that before sending the response the user +// as the code that removes it in Next isn't run based on how we handle the middleware +// +// Related Next.js code: +// * https://github.com/vercel/next.js/blob/68d06fe015b28d8f81da52ca107a5f4bd72ab37c/packages/next/server/next-server.ts#L1918-L1928 +// * https://github.com/vercel/next.js/blob/43c9d8940dc42337dd2f7d66aa90e6abf952278e/packages/next/server/web/spec-extension/response.ts#L10-L27 +export function updateModifiedHeaders(requestHeaders: Headers, responseHeaders: Headers) { + const overriddenHeaders = responseHeaders.get('x-middleware-override-headers') + + if (!overriddenHeaders) { + return + } + + const headersToUpdate = overriddenHeaders.split(',').map((header) => header.trim()) + + for (const header of headersToUpdate) { + const oldHeaderKey = 'x-middleware-request-' + header + const headerValue = responseHeaders.get(oldHeaderKey) || '' + + requestHeaders.set(header, headerValue) + responseHeaders.delete(oldHeaderKey) + } + responseHeaders.delete('x-middleware-override-headers') +} + export const buildResponse = async ({ result, request, @@ -68,6 +94,8 @@ export const buildResponse = async ({ request: Request context: Context }) => { + updateModifiedHeaders(request.headers, result.response.headers) + // They've returned the MiddlewareRequest directly, so we'll call `next()` for them. if (isMiddlewareRequest(result.response)) { result.response = await result.response.next()