Skip to content

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:

  1. compress - Automatic response compression (brotli/gzip/deflate)
  2. logger - Standardized request/response logging
  3. 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 (routes sub-app — never /api/auth/*)

Behavior

The compress middleware:

  • Inspects the Accept-Encoding request header
  • Selects the best compression algorithm in this priority order:
    1. Brotli (br) - highest compression ratio
    2. Gzip (gzip) - widely supported, good compression
    3. Deflate (deflate) - fallback option
  • Sets the Content-Encoding response header to match the chosen algorithm
  • Skips compression if:
    • No Accept-Encoding header is present
    • Response body is too small (< 1KB)
    • Response is already compressed

Example Request/Response

Request:

GET /compile HTTP/1.1
Host: api.example.com
Accept-Encoding: br, gzip, deflate

Response:

HTTP/1.1 200 OK
Content-Type: application/json
Content-Encoding: br
Content-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:

Terminal window
curl -H "Accept-Encoding: gzip" https://api.example.com/compile | gzip -d

Expected: 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 (routes sub-app — never /api/auth/*)

Behavior

The logger middleware outputs log entries to console.log in the following format:

<-- METHOD PATH STATUS DURATION

Fields:

  • METHOD - HTTP method (GET, POST, etc.)
  • PATH - Request path
  • STATUS - HTTP status code
  • DURATION - 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 12ms

Position 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:

Terminal window
# View logs in Cloudflare dashboard
# Filter: "GET /compile 200"

Testing

Check Worker logs after making a request:

Terminal window
curl https://api.example.com/api/version
# Then check Cloudflare dashboard → Workers → bloqr-backend → Logs

3. 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:

RouteCache DurationRationale
/api/version3600s (1 hour)Static version metadata
/api/schemas3600s (1 hour)Static JSON schemas
/configuration/defaults300s (5 minutes)Default configuration template (rarely changes)

Behavior

The cache middleware:

  • Sets the Cache-Control response 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.1
Host: api.example.com
HTTP/1.1 200 OK
Cache-Control: public, max-age=3600
CF-Cache-Status: MISS
Content-Type: application/json
{"version":"0.77.3"}

Second request (cache hit):

GET /api/version HTTP/1.1
Host: api.example.com
HTTP/1.1 200 OK
Cache-Control: public, max-age=3600
CF-Cache-Status: HIT
Age: 42
Content-Type: application/json
{"version":"0.77.3"}

Testing Cache Behavior

Terminal window
# 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:

  1. Cloudflare dashboard: Cache → Configuration → Purge Cache → Custom Purge → enter URL
  2. Cloudflare API:
Terminal window
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:

  1. timing() wraps all operations (must be first)
  2. Better Auth handler is resolved before the routes sub-app is mounted, so logger() and compress() (scoped to routes) never touch /api/auth/* traffic.
  3. logger() and compress() are registered on the routes sub-app (not globally on app), ensuring they only apply to business routes and cannot interfere with authentication responses.
  4. cache() is applied at the route level, before rateLimitMiddleware()

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 Response stream, 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 routes sub-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-IP header 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:

  1. Client did not send Accept-Encoding header
  2. Response body is too small (< 1KB)
  3. Response is already compressed

Fix:

Terminal window
# Test with explicit Accept-Encoding header
curl -H "Accept-Encoding: gzip" https://api.example.com/compile

logger middleware not outputting logs

Symptom: No log entries in Cloudflare dashboard

Possible causes:

  1. Logs not enabled for Worker
  2. Log retention period expired

Fix:

  1. Enable logs: Cloudflare dashboard → Workers → bloqr-backend → Settings → Logs
  2. Use Real-time logs (live tail) for immediate debugging

cache middleware not caching responses

Symptom: All requests show CF-Cache-Status: MISS

Possible causes:

  1. Cache-Control header not set correctly
  2. Cloudflare cache rules override middleware headers
  3. Response has Set-Cookie header (bypass cache)

Fix:

Terminal window
# Verify Cache-Control header is present
curl -I https://api.example.com/api/version | grep -i cache-control
# Expected: Cache-Control: public, max-age=3600


References