Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Middleware: remove req.ua #37512

Merged
merged 6 commits into from
Jun 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions errors/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,10 @@
{
"title": "middleware-request-page.md",
"path": "/errors/middleware-request-page.md"
},
{
"title": "middleware-user-agent.md",
"path": "/errors/middleware-user-agent.md"
}
]
}
Expand Down
2 changes: 1 addition & 1 deletion errors/middleware-request-page.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Deprecated page into Middleware API
# Removed page from Middleware API

#### Why This Error Occurred

Expand Down
34 changes: 34 additions & 0 deletions errors/middleware-user-agent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Removed req.ua from Middleware API
Kikobeats marked this conversation as resolved.
Show resolved Hide resolved

#### Why This Error Occurred

Your middleware is interacting with `req.ua` and this feature needs to opt-in.

```typescript
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'

export function middleware(request: NextRequest) {
const url = request.nextUrl
const viewport = request.ua.device.type === 'mobile' ? 'mobile' : 'desktop'
url.searchParams.set('viewport', viewport)
return NextResponse.rewrites(url)
}
```

#### Possible Ways to Fix It

To parse the user agent, import `userAgent` function from `next/server` and give it your request:

```typescript
// middleware.ts
import { NextRequest, NextResponse, userAgent } from 'next/server'

export function middleware(request: NextRequest) {
const url = request.nextUrl
const { device } = userAgent(request)
const viewport = device.type === 'mobile' ? 'mobile' : 'desktop'
url.searchParams.set('viewport', viewport)
return NextResponse.rewrites(url)
}
```
2 changes: 2 additions & 0 deletions packages/next/server.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ export { NextFetchEvent } from 'next/dist/server/web/spec-extension/fetch-event'
export { NextRequest } from 'next/dist/server/web/spec-extension/request'
export { NextResponse } from 'next/dist/server/web/spec-extension/response'
export { NextMiddleware } from 'next/dist/server/web/types'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder why NextMiddleware is in the types, but not really exposed from the JS code

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's necessary to type it properly when you write a Middleware using TypeScript.

export { userAgentFromString } from 'next/dist/server/web/spec-extension/user-agent'
export { userAgent } from 'next/dist/server/web/spec-extension/user-agent'
4 changes: 4 additions & 0 deletions packages/next/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,8 @@ module.exports = {
.NextRequest,
NextResponse: require('next/dist/server/web/spec-extension/response')
.NextResponse,
userAgentFromString: require('next/dist/server/web/spec-extension/user-agent')
.userAgentFromString,
userAgent: require('next/dist/server/web/spec-extension/user-agent')
.userAgent,
}
8 changes: 4 additions & 4 deletions packages/next/server/web/adapter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { NextMiddleware, RequestData, FetchEventResult } from './types'
import type { RequestInit } from './spec-extension/request'
import { DeprecationSignatureError } from './error'
import { PageSignatureError } from './error'
import { fromNodeHeaders } from './utils'
import { NextFetchEvent } from './spec-extension/fetch-event'
import { NextRequest } from './spec-extension/request'
Expand Down Expand Up @@ -162,14 +162,14 @@ class NextRequestHint extends NextRequest {
}

get request() {
throw new DeprecationSignatureError({ page: this.sourcePage })
throw new PageSignatureError({ page: this.sourcePage })
}

respondWith() {
throw new DeprecationSignatureError({ page: this.sourcePage })
throw new PageSignatureError({ page: this.sourcePage })
}

waitUntil() {
throw new DeprecationSignatureError({ page: this.sourcePage })
throw new PageSignatureError({ page: this.sourcePage })
}
}
14 changes: 11 additions & 3 deletions packages/next/server/web/error.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export class DeprecationSignatureError extends Error {
export class PageSignatureError extends Error {
constructor({ page }: { page: string }) {
super(`The middleware "${page}" accepts an async API directly with the form:

Expand All @@ -11,10 +11,18 @@ export class DeprecationSignatureError extends Error {
}
}

export class DeprecationPageError extends Error {
export class RemovedPageError extends Error {
constructor() {
super(`The request.page has been deprecated in favour of URLPattern.
super(`The request.page has been deprecated in favour of \`URLPattern\`.
Read more: https://nextjs.org/docs/messages/middleware-request-page
`)
}
}

export class RemovedUAError extends Error {
constructor() {
super(`The request.page has been removed in favour of \`userAgent\` function.
Read more: https://nextjs.org/docs/messages/middleware-parse-user-agent
`)
}
}
6 changes: 3 additions & 3 deletions packages/next/server/web/spec-extension/fetch-event.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DeprecationSignatureError } from '../error'
import { PageSignatureError } from '../error'
import { NextRequest } from './request'

const responseSymbol = Symbol('response')
Expand Down Expand Up @@ -42,7 +42,7 @@ export class NextFetchEvent extends FetchEvent {
* Read more: https://nextjs.org/docs/messages/middleware-new-signature
*/
get request() {
throw new DeprecationSignatureError({
throw new PageSignatureError({
page: this.sourcePage,
})
}
Expand All @@ -53,7 +53,7 @@ export class NextFetchEvent extends FetchEvent {
* Read more: https://nextjs.org/docs/messages/middleware-new-signature
*/
respondWith() {
throw new DeprecationSignatureError({
throw new PageSignatureError({
page: this.sourcePage,
})
}
Expand Down
50 changes: 3 additions & 47 deletions packages/next/server/web/spec-extension/request.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import type { I18NConfig } from '../../config-shared'
import type { RequestData } from '../types'
import { NextURL } from '../next-url'
import { isBot } from '../../utils'
import { toNodeHeaders, validateURL } from '../utils'
import parseua from 'next/dist/compiled/ua-parser-js'
import { DeprecationPageError } from '../error'

import { RemovedUAError, RemovedPageError } from '../error'
import { NextCookies } from './cookies'

export const INTERNALS = Symbol('internal request')
Expand All @@ -15,7 +12,6 @@ export class NextRequest extends Request {
cookies: NextCookies
geo: RequestData['geo']
ip?: string
ua?: UserAgent | null
url: NextURL
}

Expand Down Expand Up @@ -51,26 +47,11 @@ export class NextRequest extends Request {
}

public get page() {
throw new DeprecationPageError()
throw new RemovedPageError()
}

public get ua() {
if (typeof this[INTERNALS].ua !== 'undefined') {
return this[INTERNALS].ua || undefined
}

const uaString = this.headers.get('user-agent')
if (!uaString) {
this[INTERNALS].ua = null
return this[INTERNALS].ua || undefined
}

this[INTERNALS].ua = {
...parseua(uaString),
isBot: isBot(uaString),
}

return this[INTERNALS].ua
throw new RemovedUAError()
}

public get url() {
Expand All @@ -91,28 +72,3 @@ export interface RequestInit extends globalThis.RequestInit {
trailingSlash?: boolean
}
}

interface UserAgent {
isBot: boolean
ua: string
browser: {
name?: string
version?: string
}
device: {
model?: string
type?: string
vendor?: string
}
engine: {
name?: string
version?: string
}
os: {
name?: string
version?: string
}
cpu: {
architecture?: string
}
}
43 changes: 43 additions & 0 deletions packages/next/server/web/spec-extension/user-agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import parseua from 'next/dist/compiled/ua-parser-js'

interface UserAgent {
isBot: boolean
ua: string
browser: {
name?: string
version?: string
}
device: {
model?: string
type?: string
vendor?: string
}
engine: {
name?: string
version?: string
}
os: {
name?: string
version?: string
}
cpu: {
architecture?: string
}
}

export function isBot(input: string): boolean {
return /Googlebot|Mediapartners-Google|AdsBot-Google|googleweblight|Storebot-Google|Google-PageRenderer|Bingbot|BingPreview|Slurp|DuckDuckBot|baiduspider|yandex|sogou|LinkedInBot|bitlybot|tumblr|vkShare|quora link preview|facebookexternalhit|facebookcatalog|Twitterbot|applebot|redditbot|Slackbot|Discordbot|WhatsApp|SkypeUriPreview|ia_archiver/i.test(
input
)
}

export function userAgentFromString(input: string | undefined): UserAgent {
return {
...parseua(input),
isBot: input === undefined ? false : isBot(input),
}
}

export function userAgent({ headers }: { headers: Headers }): UserAgent {
return userAgentFromString(headers.get('user-agent') || undefined)
}
60 changes: 60 additions & 0 deletions test/unit/web-runtime/user-agent.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* @jest-environment @edge-runtime/jest-environment
*/

import { userAgentFromString, userAgent, NextRequest } from 'next/server'

const UA_STRING =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36'

it('parse an user agent', () => {
const parser = userAgentFromString(UA_STRING)
expect(parser.ua).toBe(
'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36'
)
expect(parser.browser).toStrictEqual({
name: 'Chrome',
version: '89.0.4389.90',
major: '89',
})
expect(parser.engine).toStrictEqual({
name: 'Blink',
version: '89.0.4389.90',
})
expect(parser.os).toStrictEqual({ name: 'Mac OS', version: '11.2.3' })
expect(parser.cpu).toStrictEqual({ architecture: undefined })
expect(parser.isBot).toBe(false)
})

it('parse empty user agent', () => {
expect.assertions(3)
for (const input of [undefined, null, '']) {
expect(userAgentFromString(input)).toStrictEqual({
ua: '',
browser: { name: undefined, version: undefined, major: undefined },
engine: { name: undefined, version: undefined },
os: { name: undefined, version: undefined },
device: { vendor: undefined, model: undefined, type: undefined },
cpu: { architecture: undefined },
isBot: false,
})
}
})

it('parse user agent from a NextRequest instance', () => {
const request = new NextRequest('https://vercel.com', {
headers: {
'user-agent': UA_STRING,
},
})

expect(userAgent(request)).toStrictEqual({
ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36',
browser: { name: 'Chrome', version: '89.0.4389.90', major: '89' },
engine: { name: 'Blink', version: '89.0.4389.90' },
os: { name: 'Mac OS', version: '11.2.3' },
device: { vendor: undefined, model: undefined, type: undefined },
cpu: { architecture: undefined },
isBot: false,
})
})