diff --git a/README.md b/README.md index 6c8b199..9bad79f 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ 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` @@ -99,10 +99,12 @@ If the user agent is `Amazon CloudFront`, the module checks for the following he - `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 diff --git a/src/runtime/generateFlags.ts b/src/runtime/generateFlags.ts index d3cfdd1..a31ad16 100644 --- a/src/runtime/generateFlags.ts +++ b/src/runtime/generateFlags.ts @@ -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): boolean { + return CLOUDFRONT_DEVICE_HEADERS.some(header => headers[header] !== undefined) +} + export default function generateFlags(userAgent: string, headers: Record = {}): 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 diff --git a/test/basic.ts b/test/basic.ts index 894a310..df24dba 100644 --- a/test/basic.ts +++ b/test/basic.ts @@ -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', }, }) @@ -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', }, }) @@ -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', }, }) @@ -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: {