Skip to content

Commit 689b2ac

Browse files
feat: add enhanced middleware support (#1479)
* fix: update patch syntax * feat: add support for rewriting middleware responses * chore: format * chore: add extra content * chore: use city in demo * feat: add html rewriting * feat: add request header support * feat: add rewriting * chore: move NetlifyReponse into a subpackage * feat: allow returning `NetlifyReponse` directly * chore: remove inlined types from middleware demo * chore: remove modified toml * chore: add demo links * chore: add comments to example * chore: don't lint generated types * chore: rename class * chore: add comment about source of htmlrewriter types * refactor: use type guards * chore: rename classes * chore: rename again * chore: rename again * chore: update example * chore: make req a subclass of Request * chore: switch from hidden fields to global map * ci: add cypress middleware tests * ci: add tests for middleware headers * ci: add tests for enhanced middleware * chore: fix test * fix: handle other HTTP verbs * feat: add helper methods * fix: less flaky test Co-authored-by: Nick Taylor <[email protected]>
1 parent 0333292 commit 689b2ac

26 files changed

+698
-93
lines changed

.eslintignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,6 @@ node_modules
33
test
44
lib
55
demos
6-
plugin/src/templates/edge
6+
plugin/src/templates/edge
7+
plugin/lib
8+
plugin/dist-types
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
name: Run e2e (middleware demo)
2+
on:
3+
pull_request:
4+
types: [opened, labeled, unlabeled, synchronize]
5+
push:
6+
branches:
7+
- main
8+
paths:
9+
- 'demos/middleware/**/*.{js,jsx,ts,tsx}'
10+
- 'cypress/integration/middleware/**/*.{ts,js}'
11+
- 'src/**/*.{ts,js}'
12+
jobs:
13+
cypress:
14+
name: Cypress
15+
runs-on: ubuntu-latest
16+
strategy:
17+
fail-fast: false
18+
matrix:
19+
containers: [1, 2, 3, 4]
20+
steps:
21+
- name: Checkout
22+
uses: actions/checkout@v2
23+
24+
- name: Generate Github token
25+
uses: navikt/github-app-token-generator@v1
26+
id: get-token
27+
with:
28+
private-key: ${{ secrets.TOKENS_PRIVATE_KEY }}
29+
app-id: ${{ secrets.TOKENS_APP_ID }}
30+
31+
- name: Checkout @netlify/wait-for-deploy-action
32+
uses: actions/checkout@v2
33+
with:
34+
repository: netlify/wait-for-deploy-action
35+
token: ${{ steps.get-token.outputs.token }}
36+
path: ./.github/actions/wait-for-netlify-deploy
37+
38+
- name: Wait for Netlify Deploy
39+
id: deploy
40+
uses: ./.github/actions/wait-for-netlify-deploy
41+
with:
42+
site-name: next-plugin-edge-middleware
43+
timeout: 300
44+
45+
- name: Deploy successful
46+
if: ${{ steps.deploy.outputs.origin-url }}
47+
run: echo ${{ steps.deploy.outputs.origin-url }}
48+
49+
- name: Node
50+
uses: actions/setup-node@v2
51+
with:
52+
node-version: '16'
53+
54+
- run: npm install
55+
56+
- name: Cypress run
57+
if: ${{ steps.deploy.outputs.origin-url }}
58+
id: cypress
59+
uses: cypress-io/github-action@v2
60+
with:
61+
browser: chrome
62+
headless: true
63+
record: true
64+
parallel: true
65+
config-file: cypress/config/middleware.json
66+
group: 'Next Plugin - Middleware'
67+
spec: cypress/integration/middleware/*
68+
env:
69+
DEBUG: '@cypress/github-action'
70+
CYPRESS_baseUrl: ${{ steps.deploy.outputs.origin-url }}
71+
CYPRESS_NETLIFY_CONTEXT: ${{ steps.deploy.outputs.context }}
72+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
73+
CYPRESS_RECORD_KEY: ${{ secrets.MIDDLEWARE_CYPRESS_RECORD_KEY }}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ Temporary Items
147147
demos/default/.next
148148
.parcel-cache
149149
plugin/lib
150+
plugin/dist-types
150151

151152
# Cypress
152153
cypress/screenshots

.prettierignore

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ node_modules
2121
lib
2222
tsconfig.json
2323
demos/nx-next-monorepo-demo
24-
plugin/src/templates/edge
2524

26-
plugin/CHANGELOG.md
25+
plugin/CHANGELOG.md
26+
plugin/lib
27+
plugin/dist-types

cypress/config/middleware.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"baseUrl": "http://localhost:8888",
3+
"integrationFolder": "cypress/integration/middleware",
4+
"projectId": "yn8qwi"
5+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
describe('Enhanced middleware', () => {
2+
it('adds request headers', () => {
3+
cy.request('/api/hello').then((response) => {
4+
expect(response.body).to.have.nested.property('headers.x-hello', 'world')
5+
})
6+
})
7+
8+
it('adds request headers to a rewrite', () => {
9+
cy.request('/headers').then((response) => {
10+
expect(response.body).to.have.nested.property('headers.x-hello', 'world')
11+
})
12+
})
13+
14+
it('rewrites the response body', () => {
15+
cy.visit('/static')
16+
cy.get('#message').contains('This was static but has been transformed in')
17+
cy.contains("This is an ad that isn't shown by default")
18+
})
19+
20+
it('modifies the page props', () => {
21+
cy.request('/_next/data/build-id/static.json').then((response) => {
22+
expect(response.body).to.have.nested.property('pageProps.showAd', true)
23+
expect(response.body)
24+
.to.have.nested.property('pageProps.message')
25+
.that.includes('This was static but has been transformed in')
26+
})
27+
})
28+
})
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
describe('Standard middleware', () => {
2+
it('rewrites to internal page', () => {
3+
// preview mode is off by default
4+
cy.visit('/shows/rewriteme')
5+
cy.get('h1').should('contain', 'Show #100')
6+
cy.url().should('eq', `${Cypress.config().baseUrl}/shows/rewriteme`)
7+
})
8+
9+
it('rewrites to external page', () => {
10+
cy.visit('/shows/rewrite-external')
11+
cy.get('h1').should('contain', 'Example Domain')
12+
cy.url().should('eq', `${Cypress.config().baseUrl}/shows/rewrite-external`)
13+
})
14+
15+
it('adds headers to static pages', () => {
16+
cy.request('/shows/static/3').then((response) => {
17+
expect(response.headers).to.have.property('x-middleware-date')
18+
expect(response.headers).to.have.property('x-is-deno', 'true')
19+
expect(response.headers).to.have.property('x-modified-edge', 'true')
20+
})
21+
})
22+
})

demos/middleware/middleware.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,40 @@
11
import { NextResponse } from 'next/server'
2-
import { NextFetchEvent, NextRequest } from 'next/server'
2+
import type { NextRequest } from 'next/server'
33

4-
export function middleware(request: NextRequest, ev: NextFetchEvent) {
4+
import { MiddlewareRequest } from '@netlify/plugin-nextjs/middleware'
5+
6+
export async function middleware(req: NextRequest) {
57
let response
68
const {
79
nextUrl: { pathname },
8-
} = request
10+
} = req
11+
12+
const request = new MiddlewareRequest(req)
13+
14+
if (pathname.startsWith('/static')) {
15+
// Unlike NextResponse.next(), this actually sends the request to the origin
16+
const res = await request.next()
17+
const message = `This was static but has been transformed in ${req.geo.city}`
18+
19+
// Transform the response HTML and props
20+
res.replaceText('p[id=message]', message)
21+
res.setPageProp('message', message)
22+
res.setPageProp('showAd', true)
23+
24+
return res
25+
}
26+
27+
if (pathname.startsWith('/api/hello')) {
28+
// Add a header to the request
29+
req.headers.set('x-hello', 'world')
30+
return request.next()
31+
}
32+
33+
if (pathname.startsWith('/headers')) {
34+
// Add a header to the rewritten request
35+
req.headers.set('x-hello', 'world')
36+
return request.rewrite('/api/hello')
37+
}
938

1039
if (pathname.startsWith('/cookies')) {
1140
response = NextResponse.next()
@@ -15,15 +44,15 @@ export function middleware(request: NextRequest, ev: NextFetchEvent) {
1544

1645
if (pathname.startsWith('/shows')) {
1746
if (pathname.startsWith('/shows/rewrite-absolute')) {
18-
response = NextResponse.rewrite(new URL('/shows/100', request.url))
47+
response = NextResponse.rewrite(new URL('/shows/100', req.url))
1948
response.headers.set('x-modified-in-rewrite', 'true')
2049
}
2150
if (pathname.startsWith('/shows/rewrite-external')) {
2251
response = NextResponse.rewrite('http://example.com/')
2352
response.headers.set('x-modified-in-rewrite', 'true')
2453
}
2554
if (pathname.startsWith('/shows/rewriteme')) {
26-
const url = request.nextUrl.clone()
55+
const url = req.nextUrl.clone()
2756
url.pathname = '/shows/100'
2857
response = NextResponse.rewrite(url)
2958
response.headers.set('x-modified-in-rewrite', 'true')

demos/middleware/netlify.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,13 @@ included_files = [
2121

2222
[dev]
2323
framework = "#static"
24+
25+
[[redirects]]
26+
from = "/_next/static/*"
27+
to = "/static/:splat"
28+
status = 200
29+
30+
[[redirects]]
31+
from = "/*"
32+
to = "/.netlify/functions/___netlify-handler"
33+
status = 200

demos/middleware/next.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const nextConfig = {
66
// your project has ESLint errors.
77
ignoreDuringBuilds: true,
88
},
9+
generateBuildId: () => 'build-id',
910
}
1011

1112
module.exports = nextConfig

0 commit comments

Comments
 (0)