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

+3-1
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
+73
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

+1
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

+3-2
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

+5
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+
}
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+
})
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

+34-5
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

+10
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

+1
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

demos/middleware/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@
99
"ntl": "ntl-internal"
1010
},
1111
"dependencies": {
12+
"@netlify/plugin-nextjs": "*",
1213
"next": "^12.2.0",
1314
"react": "18.0.0",
1415
"react-dom": "18.0.0"
1516
},
1617
"devDependencies": {
17-
"@netlify/plugin-nextjs": "*",
1818
"@types/fs-extra": "^9.0.13",
1919
"@types/jest": "^27.4.1",
2020
"@types/node": "^17.0.25",
@@ -23,4 +23,4 @@
2323
"npm-run-all": "^4.1.5",
2424
"typescript": "^4.6.3"
2525
}
26-
}
26+
}

demos/middleware/pages/api/hello.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
22

33
export default function handler(req, res) {
4-
res.status(200).json({ name: 'John Doe' })
4+
res.status(200).json({ name: 'John Doe', headers: req.headers })
55
}

demos/middleware/pages/index.js

+9
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,15 @@ export default function Home() {
3333
Cookie API
3434
</Link>
3535
</p>
36+
<p>
37+
<Link href="/api/hello">Adds `x-hello` request header</Link>
38+
</p>
39+
<p>
40+
<Link href="/static">Rewrite static page content</Link>
41+
</p>
42+
<p>
43+
<Link href="/headers">Adds `x-hello` request header to a rewrite</Link>
44+
</p>
3645
</main>
3746
</div>
3847
)

demos/middleware/pages/static.js

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import * as React from 'react'
2+
3+
const useHydrated = () => {
4+
const [hydrated, setHydrated] = React.useState(false)
5+
React.useEffect(() => {
6+
setHydrated(true)
7+
}, [])
8+
return hydrated
9+
}
10+
11+
const Page = ({ message, showAd }) => {
12+
const hydrated = useHydrated()
13+
return (
14+
<div>
15+
<p id="message">{message}</p>
16+
{hydrated && showAd ? (
17+
<div>
18+
<p>This is an ad that isn't shown by default</p>
19+
<img src="http://placekitten.com/400/300" />
20+
</div>
21+
) : (
22+
<p>No ads for me</p>
23+
)}
24+
</div>
25+
)
26+
}
27+
28+
export async function getStaticProps() {
29+
return {
30+
props: {
31+
message: 'This is a static page',
32+
showAd: false,
33+
},
34+
}
35+
}
36+
37+
export default Page

0 commit comments

Comments
 (0)