Hono Built-in Middleware
Hono Built-in Middleware
This document describes the built-in Hono middleware integrated into the bloqr-backend Worker API to improve performance, observability, and bandwidth efficiency.
Overview
Three Hono middleware packages have been integrated:
- compress - Automatic response compression (brotli/gzip/deflate)
- logger - Standardized request/response logging
- cache - HTTP caching for static API endpoints
These middleware work together to optimize API performance while maintaining Zero Trust Architecture (ZTA) principles.
1. Compress Middleware
Purpose
Reduces bandwidth usage by automatically compressing HTTP responses based on the client’s Accept-Encoding header.
Configuration
import { compress } from 'hono/compress';
routes.use('*', compress());Applied to
- Business routes sub-app only (
routessub-app — never/api/auth/*)
Behavior
The compress middleware:
- Inspects the
Accept-Encodingrequest header - Selects the best compression algorithm in this priority order:
- Brotli (
br) - highest compression ratio - Gzip (
gzip) - widely supported, good compression - Deflate (
deflate) - fallback option
- Brotli (
- Sets the
Content-Encodingresponse header to match the chosen algorithm - Skips compression if:
- No
Accept-Encodingheader is present - Response body is too small (< 1KB)
- Response is already compressed
- No
Example Request/Response
Request:
GET /compile HTTP/1.1Host: api.example.comAccept-Encoding: br, gzip, deflateResponse:
HTTP/1.1 200 OKContent-Type: application/jsonContent-Encoding: brContent-Length: 1523
<compressed response body>Impact
- Bandwidth reduction: 60-80% for typical JSON responses
- Latency: Negligible compression overhead (< 5ms for most responses)
- Cloudflare egress: Reduced egress costs
Testing
Verify compression is applied:
curl -H "Accept-Encoding: gzip" https://api.example.com/compile | gzip -dExpected: Content-Encoding: gzip header present in response.
2. Logger Middleware
Purpose
Provides standardized HTTP request/response logging for observability and debugging.
Configuration
import { logger } from 'hono/logger';
routes.use('*', logger());Applied to
- Business routes sub-app only (
routessub-app — never/api/auth/*)
Behavior
The logger middleware outputs log entries to console.log in the following format:
<-- METHOD PATH STATUS DURATIONFields:
METHOD- HTTP method (GET, POST, etc.)PATH- Request pathSTATUS- HTTP status codeDURATION- Request processing time in milliseconds
Example Log Output
<-- GET /compile 200 45ms<-- POST /compile/batch 200 127ms<-- GET /api/version 200 3ms<-- POST /validate 400 12msPosition in Middleware Stack
Logger is applied after timing() but before compress() to log uncompressed response sizes and accurate timing.
timing() → logger() → compress() → [other middleware]Integration with Cloudflare Logs
Worker logs are captured by Cloudflare Workers Logs and can be:
- Viewed in the Cloudflare dashboard (Workers → Logs)
- Streamed to external logging services (Logpush)
- Queried via the Cloudflare API
Filtering Logs
To filter for specific routes or status codes:
# View logs in Cloudflare dashboard# Filter: "GET /compile 200"Testing
Check Worker logs after making a request:
curl https://api.example.com/api/version# Then check Cloudflare dashboard → Workers → bloqr-backend → Logs3. Cache Middleware
Purpose
Sets HTTP Cache-Control headers on static API endpoints to reduce redundant computation and D1 queries.
Configuration
import { cache } from 'hono/cache';
app.get('/api/version', cache({ cacheName: 'api-version', cacheControl: 'public, max-age=3600' }), handler);Applied to
The cache middleware is applied selectively at the route level:
| Route | Cache Duration | Rationale |
|---|---|---|
/api/version | 3600s (1 hour) | Static version metadata |
/api/schemas | 3600s (1 hour) | Static JSON schemas |
/configuration/defaults | 300s (5 minutes) | Default configuration template (rarely changes) |
Behavior
The cache middleware:
- Sets the
Cache-Controlresponse header to the configured value - Instructs CDN and browser caches to store the response
- Does not implement server-side caching (relies on Cloudflare CDN cache)
Cache-Control Directives
public, max-age=3600 (1 hour)
public- Response can be cached by any cache (CDN, browser, proxy)max-age=3600- Cache is valid for 3600 seconds (1 hour)
Used for: /api/version, /api/schemas
public, max-age=300, stale-while-revalidate=60 (5 minutes + SWR)
max-age=300- Cache is valid for 300 seconds (5 minutes)stale-while-revalidate=60- Serve stale response for up to 60 s while revalidating in the background
Used for: /configuration/defaults
Position in Middleware Stack
Cache middleware is applied at the route level, before route-specific middleware:
routes.get( '/configuration/defaults', cache({ cacheName: 'config-defaults', cacheControl: 'public, max-age=300, stale-while-revalidate=60' }), rateLimitMiddleware(), // Applied after cache handler);Cloudflare CDN Integration
Cloudflare CDN respects Cache-Control headers and caches responses accordingly:
- First request → MISS (queries Worker + D1)
- Subsequent requests → HIT (served from CDN edge cache)
- Cache purge: Use Cloudflare API or dashboard to purge specific URLs
Example Request/Response
First request (cache miss):
GET /api/version HTTP/1.1Host: api.example.com
HTTP/1.1 200 OKCache-Control: public, max-age=3600CF-Cache-Status: MISSContent-Type: application/json
{"version":"0.77.3"}Second request (cache hit):
GET /api/version HTTP/1.1Host: api.example.com
HTTP/1.1 200 OKCache-Control: public, max-age=3600CF-Cache-Status: HITAge: 42Content-Type: application/json
{"version":"0.77.3"}Testing Cache Behavior
# First request (MISS)curl -I https://api.example.com/api/version# Expected: CF-Cache-Status: MISS, Cache-Control: public, max-age=3600
# Second request (HIT)curl -I https://api.example.com/api/version# Expected: CF-Cache-Status: HIT, Age: <seconds since cached>Cache Invalidation
To invalidate cached responses:
- Cloudflare dashboard: Cache → Configuration → Purge Cache → Custom Purge → enter URL
- Cloudflare API:
curl -X POST "https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache" \ -H "Authorization: Bearer {api_token}" \ -H "Content-Type: application/json" \ --data '{"files":["https://api.example.com/api/version"]}'Middleware Execution Order
The complete middleware pipeline for a typical request:
flowchart TD
R[Incoming Request] --> T[timing]
T --> META[Request Metadata]
META --> SSR[SSR Detection]
SSR --> BA{Better Auth Route?}
BA -->|yes| BA_HANDLER[Better Auth Handler]
BA -->|no| AGENT{Agent Route?}
BA_HANDLER --> RESP[Response]
AGENT -->|yes| AGENT_HANDLER[Agent Router<br/>(cors + secureHeaders)]
AGENT -->|no| AUTH[Unified Auth]
AUTH --> CORS[CORS]
CORS --> SH[Secure Headers]
SH --> L[logger<br/>(routes sub-app)]
L --> C[compress<br/>(routes sub-app)]
C --> ZTA[ZTA Checks]
AGENT_HANDLER --> ZTA
ZTA --> CACHE{Cache middleware?}
CACHE -->|yes| CACHE_MW[cache]
CACHE -->|no| RL
CACHE_MW --> RL[Rate Limit]
RL --> H[Route Handler]
H --> RESP
Critical observations:
timing()wraps all operations (must be first)- Better Auth handler is resolved before the
routessub-app is mounted, sologger()andcompress()(scoped toroutes) never touch/api/auth/*traffic. logger()andcompress()are registered on theroutessub-app (not globally onapp), ensuring they only apply to business routes and cannot interfere with authentication responses.cache()is applied at the route level, beforerateLimitMiddleware()
Why compress/logger must be scoped to routes (not global app):
Better Auth manages its own response formatting and returns a native CF Response with a one-shot ReadableStream body without calling the middleware chain’s next(). When compress() or logger() is registered globally with app.use('*'), Hono’s app-level middleware chain still wraps every response — including those from app.on('/api/auth/*'). This causes:
- Worker CPU hangs: The compress pipeline stalls on a CF
Responsestream, causing the Worker runtime to cancel the request SyntaxError: Unexpected end of JSON input: Truncated/corrupted body from a partially-consumed compressed stream- “Unable to reach API” errors: Frontend cannot establish a session because every auth call times out or returns binary garbage
Scoping to the routes sub-app (via routes.use('*')) is the structural fix: the routes sub-app is only entered for business routes, never for Better Auth paths.
Zero Trust Architecture (ZTA) Compliance
All middleware integrations comply with ZTA principles:
compress
- No auth bypass: Compression is content-agnostic and is scoped to the
routessub-app, so it never touches/api/auth/*responses - No data leakage: Compression does not expose sensitive headers or timing information
logger
- Sanitized logs: No sensitive data (tokens, passwords) is logged
- IP privacy: Only
CF-Connecting-IPheader is logged (already trusted)
cache
- Cache scope: Only applied to anonymous-tier public endpoints
- No credential caching: Authenticated endpoints are never cached
- Cache keys: Cloudflare CDN uses full URL as cache key (no cross-user leakage)
Performance Impact
Bandwidth Reduction (compress)
- JSON responses: 60-80% smaller
- Large filter lists: 70-85% smaller
- Small responses (< 1KB): No compression (overhead not worth it)
Latency (compress)
- Compression time: < 5ms for typical responses
- Decompression time (client): < 2ms
Cache Hit Rate (cache)
Expected cache hit rates after warm-up:
/api/version: 95%+ (static, rarely changes)/api/schemas: 95%+ (static)/configuration/defaults: 80%+ (changes infrequently)
Reduced D1 Queries (cache)
/api/version: Eliminates ~95% of D1 queries for version metadata/configuration/defaults: Eliminates ~80% of D1 queries for default config
Monitoring and Observability
Cloudflare Analytics
- Bandwidth saved: View in Cloudflare dashboard → Analytics → Performance → Bandwidth
- Cache hit rate: Analytics → Caching → Cache Analytics
- Request logs: Workers → bloqr-backend → Logs
Custom Metrics (Analytics Engine)
The AnalyticsService tracks:
- Request count by route
- Response time by route
- Error count by status code
Use logger() output in combination with Analytics Engine to correlate:
- High latency routes
- High error rates
- Cache miss rates
Alerts
Set up Cloudflare Alerts for:
- Low cache hit rate (< 80% for
/api/version) - High 5xx error rate (> 1% of requests)
- High Worker CPU time (> 50ms average)
Troubleshooting
compress middleware not working
Symptom: No Content-Encoding header in response
Possible causes:
- Client did not send
Accept-Encodingheader - Response body is too small (< 1KB)
- Response is already compressed
Fix:
# Test with explicit Accept-Encoding headercurl -H "Accept-Encoding: gzip" https://api.example.com/compilelogger middleware not outputting logs
Symptom: No log entries in Cloudflare dashboard
Possible causes:
- Logs not enabled for Worker
- Log retention period expired
Fix:
- Enable logs: Cloudflare dashboard → Workers → bloqr-backend → Settings → Logs
- Use Real-time logs (live tail) for immediate debugging
cache middleware not caching responses
Symptom: All requests show CF-Cache-Status: MISS
Possible causes:
Cache-Controlheader not set correctly- Cloudflare cache rules override middleware headers
- Response has
Set-Cookieheader (bypass cache)
Fix:
# Verify Cache-Control header is presentcurl -I https://api.example.com/api/version | grep -i cache-control
# Expected: Cache-Control: public, max-age=3600Related Documentation
- Hono Routing Architecture - Middleware pipeline and execution order
- Zero Trust Architecture - Security principles and enforcement
- Cloudflare CDN Caching - CDN cache behavior and configuration
- Hono Middleware Documentation - Official Hono middleware docs