A Hono middleware for extracting CloudFront viewer information from request headers when using AWS Lambda@Edge or Lambda Function URLs behind CloudFront.
When deploying Hono applications on AWS Lambda behind CloudFront, you get access to valuable viewer information through CloudFront's headers. These headers provide details about:
- 📍 Geolocation: Country, city, region, coordinates, and timezone
- 📱 Device Detection: Mobile, tablet, desktop, iOS, Android, and smart TV
- 🌐 Network Information: IP address, ASN (Autonomous System Number)
- 🔒 Protocol Details: HTTP version and protocol used
However, working with these headers directly can be challenging:
⚠️ Headers are only available if properly configured in CloudFront- 🤯 Different header formats for different types of information
- 😕 No type safety when accessing header values
- 🐌 Redundant header extraction code across routes
- 🔍 Hard to track which headers are actually needed
This middleware solves these problems by:
- Simplifying Access: Convert raw headers into a structured object with proper types
- Optimizing Performance: Only extract the headers you need
- Ensuring Type Safety: Full TypeScript support with configuration-based type inference
- Improving DX: IDE autocompletion and compile-time checks
- Reducing Errors: Consistent header handling with proper null checks
- 🎯 Selective Header Extraction: Configure exactly which headers you want to extract
- 🔒 Type-Safe: Full TypeScript support with type inference based on your configuration
- 🚀 Zero Dependencies: Only requires Hono as a peer dependency
- 📦 Lightweight: Small bundle size and efficient header processing
- ✅ Well Tested: 100% test coverage with comprehensive test suite
# npm
npm install hono-cloudfront
# pnpm
pnpm add hono-cloudfront
# yarn
yarn add hono-cloudfront
import { Hono } from 'hono';
import { cloudFrontViewer } from 'hono-cloudfront';
const app = new Hono();
// Extract all available headers
app.use(cloudFrontViewer());
app.get('/', (c) => {
const viewer = c.get('cloudFrontViewer');
// viewer has all properties typed
return c.json({
country: viewer.country,
city: viewer.city,
isDesktopViewer: viewer.isDesktopViewer
});
});
The middleware can be configured to extract only the headers you need:
import { cloudFrontViewer } from 'hono-cloudfront';
// Extract all headers (default)
app.use(cloudFrontViewer({ all: true }));
// Extract all headers from specific categories
app.use(cloudFrontViewer({
location: true, // All location headers
deviceType: true, // All device type headers
network: true, // All network headers
protocol: true // All protocol headers
}));
// Extract specific headers from each category
app.use(cloudFrontViewer({
location: {
country: true,
city: true
},
deviceType: {
mobile: true,
tablet: true
}
}));
country
: Two-letter country code (ISO 3166-1 alpha-2)city
: City namecountryName
: Full country namecountryRegion
: Region codecountryRegionName
: Full region namelatitude
: Approximate latitudelongitude
: Approximate longitudemetroCode
: Metro code (US only)postalCode
: Postal codetimeZone
: Time zone in IANA format
android
: Android device detectiondesktop
: Desktop browser detectionios
: iOS device detectionmobile
: Mobile device detectionsmartTv
: Smart TV detectiontablet
: Tablet detection
address
: IP address and portasn
: Autonomous system number
protocol
: Protocol used (e.g., "https")httpVersion
: HTTP version
The middleware provides type inference based on your configuration. This means you'll get precise TypeScript types for the extracted headers:
import { Hono } from 'hono';
import { cloudFrontViewer } from 'hono-cloudfront';
// To get proper type inference, define your configuration
const config = {
location: {
country: true,
city: true
},
deviceType: {
mobile: true
}
} as const; // Important: use 'as const'
const app = new Hono();
app.use(cloudFrontViewer(config));
// TypeScript now knows exactly which properties are available
app.get('/', (c) => {
const viewer = c.get('cloudFrontViewer');
viewer.country; // string | undefined ✅
viewer.city; // string | undefined ✅
viewer.isMobileViewer; // boolean | undefined ✅
viewer.isTabletViewer; // Property does not exist ❌
});
When using a specific configuration, you'll want to ensure type safety across your application. Here's how to properly type your CloudFront viewer configuration:
import type { Context } from 'hono';
import type { CloudFrontViewerConfig, InferCloudFrontViewerInfo } from 'hono-cloudfront';
// 1. Define your configuration
const config = {
location: {
country: true,
city: true,
timeZone: true
},
deviceType: {
mobile: true,
tablet: true
}
} as const;
// 2. Create a type from your configuration
type MyConfig = typeof config;
// 3. Extend Hono's context type with your configuration
declare module 'hono' {
interface ContextVariableMap {
cloudFrontViewer: InferCloudFrontViewerInfo<MyConfig>;
}
}
// Now you have type safety everywhere in your application
const app = new Hono();
app.use(cloudFrontViewer(config));
// Example route handler with proper typing
app.get('/', (c) => {
const viewer = c.get('cloudFrontViewer');
// TypeScript knows exactly what's available
return c.json({
location: {
city: viewer.city, // ✅ string | undefined
country: viewer.country, // ✅ string | undefined
timezone: viewer.timeZone // ✅ string | undefined
},
device: {
isMobile: viewer.isMobileViewer, // ✅ boolean | undefined
isTablet: viewer.isTabletViewer, // ✅ boolean | undefined
isDesktop: viewer.isDesktopViewer // ✅ boolean | undefined
},
// @ts-expect-error - Property does not exist
isDesktop: viewer.isDesktopViewer // ❌ Not included in config
});
});
// Type safety in other functions
const getLocation = (c: Context) => {
const viewer = c.get('cloudFrontViewer');
// Full type safety based on your config
return {
city: viewer.city,
country: viewer.country
};
};
If you're using different configurations in different parts of your application, you can create a helper type to manage context types:
// types/cloudfront.ts
import type { CloudFrontViewerConfig, InferCloudFrontViewerInfo } from 'hono-cloudfront';
// Helper type to create a typed context
export type TypedContext<T extends CloudFrontViewerConfig> = {
cloudFrontViewer: InferCloudFrontViewerInfo<T>;
};
// Example configurations
export const geoConfig = {
location: {
country: true,
city: true,
latitude: true,
longitude: true
}
} as const;
export const deviceConfig = {
deviceType: {
mobile: true,
tablet: true,
desktop: true
}
} as const;
// Type your routes based on configuration
app.get('/geo', (c) => {
type GeoContext = TypedContext<typeof geoConfig>;
const viewer = c.get<keyof GeoContext>('cloudFrontViewer');
return c.json({
position: {
lat: viewer.latitude, // ✅ string | undefined
lng: viewer.longitude // ✅ string | undefined
}
});
});
app.get('/device', (c) => {
type DeviceContext = TypedContext<typeof deviceConfig>;
const viewer = c.get<keyof DeviceContext>('cloudFrontViewer');
return c.json({
device: {
isMobile: viewer.isMobileViewer, // ✅ boolean | undefined
isTablet: viewer.isTabletViewer, // ✅ boolean | undefined
isDesktop: viewer.isDesktopViewer // ✅ boolean | undefined
}
});
});
// You can also combine configurations
type CombinedContext = TypedContext<typeof geoConfig & typeof deviceConfig>;
// Create type-safe middleware
const requireLocation = (c: Context) => {
const viewer = c.get('cloudFrontViewer');
if (!viewer.country || !viewer.city) {
return c.json({ error: 'Location required' }, 400);
}
return c.next();
};
This approach provides:
- Type-safe access to CloudFront viewer information based on your configuration
- Ability to use different configurations in different parts of your application
- Type checking for middleware and route handlers
- IDE autocompletion for available properties
- Compile-time errors when accessing undefined properties
To use this middleware, you need to configure CloudFront to forward the required headers to your Lambda function. Here's an example of the required headers for each category:
{
"HeadersConfig": {
"HeaderBehavior": "whitelist",
"Headers": {
"Items": [
"CloudFront-Viewer-Address",
"CloudFront-Viewer-ASN",
"CloudFront-Viewer-Country",
"CloudFront-Viewer-City",
"CloudFront-Viewer-Country-Name",
"CloudFront-Viewer-Country-Region",
"CloudFront-Viewer-Country-Region-Name",
"CloudFront-Viewer-Latitude",
"CloudFront-Viewer-Longitude",
"CloudFront-Viewer-Metro-Code",
"CloudFront-Viewer-Postal-Code",
"CloudFront-Viewer-Time-Zone",
"CloudFront-Forwarded-Proto",
"CloudFront-Viewer-Http-Version",
"CloudFront-Is-Android-Viewer",
"CloudFront-Is-Desktop-Viewer",
"CloudFront-Is-IOS-Viewer",
"CloudFront-Is-Mobile-Viewer",
"CloudFront-Is-SmartTV-Viewer",
"CloudFront-Is-Tablet-Viewer"
]
}
}
}
MIT