Skip to content
Open
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
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,18 +91,20 @@ To detect the device on each request, deploy with `nuxt build` instead. For a cl

## Amazon CloudFront Support

If the user agent is `Amazon CloudFront`, the module checks for the following headers:
When [CloudFront device detection](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/adding-cloudfront-headers.html#cloudfront-headers-device-type) is enabled, the module checks for the following headers, regardless of the incoming user agent:

- `CloudFront-Is-Android-Viewer`
- `CloudFront-Is-Desktop-Viewer`
- `CloudFront-Is-IOS-Viewer`
- `CloudFront-Is-Mobile-Viewer`
- `CloudFront-Is-Tablet-Viewer`

These headers take precedence over user agent parsing. This works both when CloudFront forwards the viewer's browser user agent and when it replaces it with `Amazon CloudFront`.

Read more about determining the viewer's device type in the [Amazon CloudFront docs](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/adding-cloudfront-headers.html#cloudfront-headers-device-type).

> [!CAUTION]
> `isWindows` and `isMacOS` flags are not available in Amazon CloudFront.
> [!NOTE]
> If CloudFront replaces the user agent with `Amazon CloudFront`, `isWindows`, `isMacOS` and the browser flags can't be detected, because the original user agent is no longer available. Forward the `User-Agent` header to the origin (in your cache or origin request policy) if you need them.

## Cloudflare Support

Expand Down
16 changes: 15 additions & 1 deletion src/runtime/generateFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,27 @@ function getBrowserName(userAgent: string): string {
return ''
}

// CloudFront adds these headers when device detection is enabled, regardless of
// whether the viewer's `User-Agent` is forwarded or replaced with `Amazon CloudFront`.
const CLOUDFRONT_DEVICE_HEADERS = [
'cloudfront-is-mobile-viewer',
'cloudfront-is-tablet-viewer',
'cloudfront-is-desktop-viewer',
'cloudfront-is-ios-viewer',
'cloudfront-is-android-viewer',
]

function isCloudFrontRequest(headers: Record<string, string>): boolean {
return CLOUDFRONT_DEVICE_HEADERS.some(header => headers[header] !== undefined)
}

export default function generateFlags(userAgent: string, headers: Record<string, string> = {}): Device {
let mobile = false
let mobileOrTablet = false
let ios = false
let android = false

if (userAgent === 'Amazon CloudFront') {
if (isCloudFrontRequest(headers)) {
if (headers['cloudfront-is-mobile-viewer'] === 'true') {
mobile = true
mobileOrTablet = true
Expand Down
57 changes: 43 additions & 14 deletions test/basic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,10 +260,16 @@ export function describeDevice(name: string, rootDir: string) {
})
})

it('detects cloudfront headers - mobile', async () => {
// CloudFront device headers must win over the User-Agent, which can be either
// the viewer's browser (when it is forwarded by the cache policy) or
// `Amazon CloudFront` (when CloudFront replaces it).
const DESKTOP_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
const IPHONE_UA = 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1'

it('detects cloudfront headers - mobile (browser user agent)', async () => {
const html = await $fetch('/', {
headers: {
'User-Agent': 'Amazon CloudFront',
'User-Agent': DESKTOP_UA,
'Cloudfront-Is-Mobile-Viewer': 'true',
},
})
Expand All @@ -272,34 +278,34 @@ export function describeDevice(name: string, rootDir: string) {
expect({ isMobile, isMobileOrTablet }).toEqual({ isMobile: true, isMobileOrTablet: true })
})

it('detects cloudfront headers - tablet', async () => {
it('detects cloudfront headers - tablet (browser user agent)', async () => {
const html = await $fetch('/', {
headers: {
'User-Agent': 'Amazon CloudFront',
'User-Agent': DESKTOP_UA,
'Cloudfront-Is-Tablet-Viewer': 'true',
},
})
const { isMobile, isMobileOrTablet } = parseHtml(html)
const { isMobile, isMobileOrTablet, isTablet } = parseHtml(html)

expect({ isMobile, isMobileOrTablet }).toEqual({ isMobile: false, isMobileOrTablet: true })
expect({ isMobile, isMobileOrTablet, isTablet }).toEqual({ isMobile: false, isMobileOrTablet: true, isTablet: true })
})

it('detects cloudfront headers - desktop', async () => {
it('detects cloudfront headers - desktop (browser user agent)', async () => {
const html = await $fetch('/', {
headers: {
'User-Agent': 'Amazon CloudFront',
'User-Agent': IPHONE_UA,
'Cloudfront-Is-Desktop-Viewer': 'true',
},
})
const { isDesktop } = parseHtml(html)
const { isDesktop, isMobile } = parseHtml(html)

expect(isDesktop).toEqual(true)
expect({ isDesktop, isMobile }).toEqual({ isDesktop: true, isMobile: false })
})

it('detects cloudfront headers - ios', async () => {
it('detects cloudfront headers - ios (browser user agent)', async () => {
const html = await $fetch('/', {
headers: {
'User-Agent': 'Amazon CloudFront',
'User-Agent': DESKTOP_UA,
'Cloudfront-Is-Ios-Viewer': 'true',
},
})
Expand All @@ -308,10 +314,10 @@ export function describeDevice(name: string, rootDir: string) {
expect(isIos).toEqual(true)
})

it('detects cloudfront headers - android', async () => {
it('detects cloudfront headers - android (browser user agent)', async () => {
const html = await $fetch('/', {
headers: {
'User-Agent': 'Amazon CloudFront',
'User-Agent': DESKTOP_UA,
'Cloudfront-Is-Android-Viewer': 'true',
},
})
Expand All @@ -320,6 +326,29 @@ export function describeDevice(name: string, rootDir: string) {
expect(isAndroid).toEqual(true)
})

it('detects cloudfront headers - replaced user agent', async () => {
const html = await $fetch('/', {
headers: {
'User-Agent': 'Amazon CloudFront',
'Cloudfront-Is-Mobile-Viewer': 'true',
},
})
const { isMobile, isMobileOrTablet } = parseHtml(html)

expect({ isMobile, isMobileOrTablet }).toEqual({ isMobile: true, isMobileOrTablet: true })
})

it('does not detect cloudfront when no headers are present', async () => {
const html = await $fetch('/', {
headers: {
'User-Agent': DESKTOP_UA,
},
})
const { isDesktop, isMobile } = parseHtml(html)

expect({ isDesktop, isMobile }).toEqual({ isDesktop: true, isMobile: false })
})

it('detects cloudflare headers - mobile', async () => {
const html = await $fetch('/', {
headers: {
Expand Down
Loading