Initial commit
This commit is contained in:
529
references/architecture.md
Normal file
529
references/architecture.md
Normal file
@@ -0,0 +1,529 @@
|
||||
# Architecture Deep Dive
|
||||
|
||||
**Last Updated**: 2025-10-20
|
||||
|
||||
This document explains the architectural patterns used in Cloudflare Workers with Hono, Vite, and Static Assets.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Export Patterns](#export-patterns)
|
||||
2. [Routing Architecture](#routing-architecture)
|
||||
3. [Static Assets Integration](#static-assets-integration)
|
||||
4. [Bindings and Type Safety](#bindings-and-type-safety)
|
||||
5. [Development vs Production](#development-vs-production)
|
||||
|
||||
---
|
||||
|
||||
## Export Patterns
|
||||
|
||||
### The Correct Pattern (ES Module Format)
|
||||
|
||||
```typescript
|
||||
import { Hono } from 'hono'
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
// Define routes...
|
||||
|
||||
// ✅ CORRECT: Export the Hono app directly
|
||||
export default app
|
||||
```
|
||||
|
||||
**Why this works:**
|
||||
- Hono's app object already implements the `fetch` handler
|
||||
- When Cloudflare calls your Worker, it automatically invokes `app.fetch()`
|
||||
- This is the **ES Module Worker format** (modern, recommended)
|
||||
|
||||
### The Incorrect Pattern (Causes Errors)
|
||||
|
||||
```typescript
|
||||
// ❌ WRONG: This causes "Cannot read properties of undefined (reading 'map')" error
|
||||
export default {
|
||||
fetch: app.fetch
|
||||
}
|
||||
```
|
||||
|
||||
**Why this fails:**
|
||||
- When using Vite's build tools with Hono, the `app.fetch` binding is lost
|
||||
- The Vite bundler transforms the code in a way that breaks the `this` context
|
||||
- Source: [honojs/hono #3955](https://github.com/honojs/hono/issues/3955)
|
||||
|
||||
### Module Worker Format (When You Need Multiple Handlers)
|
||||
|
||||
```typescript
|
||||
import { Hono } from 'hono'
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
// Define routes...
|
||||
|
||||
// ✅ CORRECT: Use Module Worker format for scheduled/tail handlers
|
||||
export default {
|
||||
fetch: app.fetch,
|
||||
|
||||
scheduled: async (event, env, ctx) => {
|
||||
// Cron job logic
|
||||
console.log('Cron triggered:', event.cron)
|
||||
},
|
||||
|
||||
tail: async (events, env, ctx) => {
|
||||
// Tail handler logic
|
||||
console.log('Tail events:', events)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**When to use this:**
|
||||
- You need scheduled (cron) handlers
|
||||
- You need tail handlers for log consumption
|
||||
- You need queue consumers
|
||||
- You need durable object handlers
|
||||
|
||||
**Important**: This is still ES Module format, not the deprecated Service Worker format.
|
||||
|
||||
### Deprecated Service Worker Format (Never Use)
|
||||
|
||||
```typescript
|
||||
// ❌ DEPRECATED: Never use this format
|
||||
addEventListener('fetch', (event) => {
|
||||
event.respondWith(handleRequest(event.request))
|
||||
})
|
||||
```
|
||||
|
||||
**Why never use this:**
|
||||
- Deprecated since Cloudflare Workers v2
|
||||
- Doesn't support modern features (D1, Vectorize, etc.)
|
||||
- Not compatible with TypeScript types
|
||||
- Not supported by Vite plugin
|
||||
|
||||
---
|
||||
|
||||
## Routing Architecture
|
||||
|
||||
### Request Flow
|
||||
|
||||
```
|
||||
Incoming Request
|
||||
↓
|
||||
├─→ Worker checks run_worker_first patterns
|
||||
│ └─→ Matches /api/* → Worker handles it → Returns JSON
|
||||
│
|
||||
└─→ No match → Static Assets handler
|
||||
├─→ File exists → Returns file
|
||||
└─→ File not found → SPA fallback → Returns index.html
|
||||
```
|
||||
|
||||
### Configuration Required
|
||||
|
||||
In `wrangler.jsonc`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"assets": {
|
||||
"directory": "./public/",
|
||||
"binding": "ASSETS",
|
||||
"not_found_handling": "single-page-application",
|
||||
"run_worker_first": ["/api/*"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Critical**: Without `run_worker_first`, the SPA fallback intercepts ALL requests, including API routes.
|
||||
|
||||
### Route Priority
|
||||
|
||||
1. **Worker routes** (if matched by `run_worker_first`)
|
||||
```typescript
|
||||
app.get('/api/hello', (c) => c.json({ ... }))
|
||||
```
|
||||
|
||||
2. **Static files** (if file exists in `public/`)
|
||||
```
|
||||
public/styles.css → Served as-is
|
||||
public/logo.png → Served as-is
|
||||
```
|
||||
|
||||
3. **SPA fallback** (if file doesn't exist)
|
||||
```
|
||||
/unknown-route → Returns public/index.html
|
||||
```
|
||||
|
||||
### Advanced Routing Patterns
|
||||
|
||||
#### Wildcard Routes
|
||||
|
||||
```typescript
|
||||
// Match all API versions
|
||||
app.get('/api/:version/users', (c) => {
|
||||
const version = c.req.param('version')
|
||||
return c.json({ version })
|
||||
})
|
||||
|
||||
// Match nested routes
|
||||
app.get('/api/users/:id/posts/:postId', (c) => {
|
||||
const { id, postId } = c.req.param()
|
||||
return c.json({ userId: id, postId })
|
||||
})
|
||||
```
|
||||
|
||||
#### Regex Routes
|
||||
|
||||
```typescript
|
||||
// Match numeric IDs only
|
||||
app.get('/api/users/:id{[0-9]+}', (c) => {
|
||||
const id = c.req.param('id')
|
||||
return c.json({ id: parseInt(id) })
|
||||
})
|
||||
```
|
||||
|
||||
#### Route Groups
|
||||
|
||||
```typescript
|
||||
const api = new Hono()
|
||||
|
||||
api.get('/users', (c) => c.json({ users: [] }))
|
||||
api.get('/posts', (c) => c.json({ posts: [] }))
|
||||
|
||||
app.route('/api', api) // Mount at /api
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Static Assets Integration
|
||||
|
||||
### How Static Assets Work
|
||||
|
||||
1. **Upload**: When you deploy, Wrangler uploads `public/` to Cloudflare's asset store
|
||||
2. **Binding**: Your Worker receives an `ASSETS` Fetcher binding
|
||||
3. **Request**: Your Worker can forward requests to `ASSETS.fetch()`
|
||||
4. **Cache**: Assets are cached at the edge automatically
|
||||
|
||||
### The Fallback Pattern
|
||||
|
||||
```typescript
|
||||
// Handle all unmatched routes
|
||||
app.all('*', (c) => {
|
||||
// Forward to Static Assets
|
||||
return c.env.ASSETS.fetch(c.req.raw)
|
||||
})
|
||||
```
|
||||
|
||||
**What this does:**
|
||||
- Forwards request to Static Assets handler
|
||||
- Static Assets checks if file exists
|
||||
- If yes: Returns file
|
||||
- If no: Returns `index.html` (SPA fallback)
|
||||
|
||||
### Custom 404 Handling
|
||||
|
||||
```typescript
|
||||
app.all('*', async (c) => {
|
||||
const response = await c.env.ASSETS.fetch(c.req.raw)
|
||||
|
||||
// If Static Assets returns 404, customize response
|
||||
if (response.status === 404) {
|
||||
return c.json({ error: 'Not Found' }, 404)
|
||||
}
|
||||
|
||||
return response
|
||||
})
|
||||
```
|
||||
|
||||
### Asset Preprocessing
|
||||
|
||||
```typescript
|
||||
app.all('*', async (c) => {
|
||||
const url = new URL(c.req.url)
|
||||
|
||||
// Rewrite /old-path to /new-path
|
||||
if (url.pathname === '/old-path') {
|
||||
url.pathname = '/new-path'
|
||||
}
|
||||
|
||||
// Create new request with modified URL
|
||||
const modifiedRequest = new Request(url, c.req.raw)
|
||||
return c.env.ASSETS.fetch(modifiedRequest)
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Bindings and Type Safety
|
||||
|
||||
### Defining Bindings
|
||||
|
||||
```typescript
|
||||
type Bindings = {
|
||||
ASSETS: Fetcher // Static Assets (always present)
|
||||
MY_KV: KVNamespace // KV namespace
|
||||
DB: D1Database // D1 database
|
||||
MY_BUCKET: R2Bucket // R2 bucket
|
||||
MY_VAR: string // Environment variable
|
||||
}
|
||||
|
||||
const app = new Hono<{ Bindings: Bindings }>()
|
||||
```
|
||||
|
||||
### Accessing Bindings
|
||||
|
||||
```typescript
|
||||
app.get('/api/data', async (c) => {
|
||||
// Type-safe access to bindings
|
||||
const value = await c.env.MY_KV.get('key')
|
||||
const result = await c.env.DB.prepare('SELECT * FROM users').all()
|
||||
const object = await c.env.MY_BUCKET.get('file.txt')
|
||||
const variable = c.env.MY_VAR
|
||||
|
||||
return c.json({ value, result, object, variable })
|
||||
})
|
||||
```
|
||||
|
||||
### Auto-Generated Types
|
||||
|
||||
Run `wrangler types` to generate `worker-configuration.d.ts`:
|
||||
|
||||
```typescript
|
||||
// Auto-generated by Wrangler
|
||||
interface Env {
|
||||
ASSETS: Fetcher
|
||||
MY_KV: KVNamespace
|
||||
DB: D1Database
|
||||
MY_BUCKET: R2Bucket
|
||||
MY_VAR: string
|
||||
}
|
||||
```
|
||||
|
||||
Then use:
|
||||
|
||||
```typescript
|
||||
const app = new Hono<{ Bindings: Env }>()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Development vs Production
|
||||
|
||||
### Local Development (wrangler dev)
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
**What happens:**
|
||||
- Miniflare simulates Cloudflare's runtime locally
|
||||
- Bindings are emulated (KV, D1, R2)
|
||||
- HMR enabled via Vite plugin
|
||||
- Runs on http://localhost:8787
|
||||
|
||||
**Configuration**:
|
||||
```typescript
|
||||
// vite.config.ts
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
cloudflare({
|
||||
persist: true, // Persist data between restarts
|
||||
}),
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
### Production Deployment (wrangler deploy)
|
||||
|
||||
```bash
|
||||
npm run deploy
|
||||
```
|
||||
|
||||
**What happens:**
|
||||
- Vite builds your code
|
||||
- Wrangler uploads to Cloudflare
|
||||
- Static Assets uploaded separately
|
||||
- Worker deployed to edge network
|
||||
|
||||
**Build Output**:
|
||||
```
|
||||
dist/
|
||||
├── index.js # Your Worker code (bundled)
|
||||
└── ... # Other build artifacts
|
||||
```
|
||||
|
||||
### Environment-Specific Configuration
|
||||
|
||||
```jsonc
|
||||
// wrangler.jsonc
|
||||
{
|
||||
"name": "my-worker",
|
||||
"env": {
|
||||
"staging": {
|
||||
"name": "my-worker-staging",
|
||||
"vars": { "ENV": "staging" },
|
||||
"kv_namespaces": [
|
||||
{ "binding": "MY_KV", "id": "staging-kv-id" }
|
||||
]
|
||||
},
|
||||
"production": {
|
||||
"name": "my-worker-production",
|
||||
"vars": { "ENV": "production" },
|
||||
"kv_namespaces": [
|
||||
{ "binding": "MY_KV", "id": "production-kv-id" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Deploy to specific environment:
|
||||
```bash
|
||||
wrangler deploy --env staging
|
||||
wrangler deploy --env production
|
||||
```
|
||||
|
||||
### Environment Detection in Code
|
||||
|
||||
```typescript
|
||||
app.get('/api/info', (c) => {
|
||||
const isDev = c.req.url.includes('localhost')
|
||||
const env = c.env.ENV || 'development'
|
||||
|
||||
return c.json({ isDev, env })
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Cold Starts
|
||||
|
||||
Cloudflare Workers have **extremely fast cold starts** (~5ms):
|
||||
- Code is distributed globally
|
||||
- No containers to spin up
|
||||
- Minimal initialization overhead
|
||||
|
||||
Keep your bundle small:
|
||||
- Avoid large dependencies
|
||||
- Use tree-shaking (Vite does this automatically)
|
||||
- Lazy-load heavy modules
|
||||
|
||||
### CPU Time Limits
|
||||
|
||||
- **Free Plan**: 10ms CPU time per request
|
||||
- **Paid Plan**: 50ms CPU time per request
|
||||
|
||||
**Tip**: Use asynchronous operations (KV, D1, R2) to avoid blocking CPU time.
|
||||
|
||||
### Memory Limits
|
||||
|
||||
- **128 MB** per Worker instance
|
||||
|
||||
**Tip**: Avoid loading large files into memory. Stream data when possible.
|
||||
|
||||
### Request Size Limits
|
||||
|
||||
- **Request Body**: 100 MB
|
||||
- **Response Body**: No limit (can stream)
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Use Middleware for Common Logic
|
||||
|
||||
```typescript
|
||||
import { logger } from 'hono/logger'
|
||||
import { cors } from 'hono/cors'
|
||||
|
||||
app.use('*', logger())
|
||||
app.use('/api/*', cors())
|
||||
```
|
||||
|
||||
### 2. Separate API and Static Routes
|
||||
|
||||
```typescript
|
||||
const api = new Hono()
|
||||
|
||||
api.get('/users', ...)
|
||||
api.get('/posts', ...)
|
||||
|
||||
app.route('/api', api)
|
||||
app.all('*', (c) => c.env.ASSETS.fetch(c.req.raw))
|
||||
```
|
||||
|
||||
### 3. Handle Errors Gracefully
|
||||
|
||||
```typescript
|
||||
app.onError((err, c) => {
|
||||
console.error(err)
|
||||
return c.json({ error: 'Internal Server Error' }, 500)
|
||||
})
|
||||
```
|
||||
|
||||
### 4. Use TypeScript
|
||||
|
||||
```typescript
|
||||
// Define types for request/response
|
||||
type User = {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
app.get('/api/users/:id', async (c) => {
|
||||
const id = parseInt(c.req.param('id'))
|
||||
const user: User = { id, name: 'Alice' }
|
||||
return c.json(user)
|
||||
})
|
||||
```
|
||||
|
||||
### 5. Validate Input
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod'
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string(),
|
||||
email: z.string().email(),
|
||||
})
|
||||
|
||||
app.post('/api/users', async (c) => {
|
||||
const body = await c.req.json()
|
||||
const validated = schema.parse(body)
|
||||
return c.json({ success: true, data: validated })
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Issue: API routes return HTML
|
||||
|
||||
**Cause**: Missing `run_worker_first` configuration
|
||||
|
||||
**Fix**: Add to `wrangler.jsonc`:
|
||||
```jsonc
|
||||
{
|
||||
"assets": {
|
||||
"run_worker_first": ["/api/*"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Issue: HMR crashes with "A hanging Promise was canceled"
|
||||
|
||||
**Cause**: Race condition in older Vite plugin versions
|
||||
|
||||
**Fix**: Update to latest:
|
||||
```bash
|
||||
npm install -D @cloudflare/vite-plugin@1.13.13
|
||||
```
|
||||
|
||||
### Issue: Deployment fails with "Cannot read properties of undefined"
|
||||
|
||||
**Cause**: Incorrect export pattern
|
||||
|
||||
**Fix**: Use `export default app` (not `{ fetch: app.fetch }`)
|
||||
|
||||
---
|
||||
|
||||
**For more troubleshooting**, see `common-issues.md`.
|
||||
645
references/common-issues.md
Normal file
645
references/common-issues.md
Normal file
@@ -0,0 +1,645 @@
|
||||
# Common Issues and Troubleshooting
|
||||
|
||||
**Last Updated**: 2025-10-20
|
||||
|
||||
This document details all 6 documented issues that commonly affect Cloudflare Workers projects, with detailed explanations and fixes.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Issue #1: Export Syntax Error](#issue-1-export-syntax-error)
|
||||
2. [Issue #2: Static Assets Routing Conflicts](#issue-2-static-assets-routing-conflicts)
|
||||
3. [Issue #3: Scheduled/Cron Not Exported](#issue-3-scheduledcron-not-exported)
|
||||
4. [Issue #4: HMR Race Condition](#issue-4-hmr-race-condition)
|
||||
5. [Issue #5: Static Assets Upload Race](#issue-5-static-assets-upload-race)
|
||||
6. [Issue #6: Service Worker Format Confusion](#issue-6-service-worker-format-confusion)
|
||||
|
||||
---
|
||||
|
||||
## Issue #1: Export Syntax Error
|
||||
|
||||
### Symptoms
|
||||
|
||||
```
|
||||
Error: Cannot read properties of undefined (reading 'map')
|
||||
```
|
||||
|
||||
Deployment fails with TypeError during build or runtime.
|
||||
|
||||
### Source
|
||||
|
||||
- **GitHub Issue**: [honojs/hono #3955](https://github.com/honojs/hono/issues/3955)
|
||||
- **Related**: [honojs/vite-plugins #237](https://github.com/honojs/vite-plugins/issues/237)
|
||||
- **Reported**: February 2025
|
||||
|
||||
### Root Cause
|
||||
|
||||
When using Hono with Vite's build tools, the incorrect export pattern breaks the `this` context:
|
||||
|
||||
```typescript
|
||||
// ❌ WRONG: This causes the error
|
||||
export default {
|
||||
fetch: app.fetch
|
||||
}
|
||||
```
|
||||
|
||||
**Why it breaks:**
|
||||
- Vite's bundler transforms the code
|
||||
- The `app.fetch` binding loses its `this` context
|
||||
- When Cloudflare calls `fetch()`, `this` is `undefined`
|
||||
- Hono tries to access `this.routes.map(...)` → Error
|
||||
|
||||
### Fix
|
||||
|
||||
Use the direct export pattern:
|
||||
|
||||
```typescript
|
||||
import { Hono } from 'hono'
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
// Define routes...
|
||||
|
||||
// ✅ CORRECT
|
||||
export default app
|
||||
```
|
||||
|
||||
**Why this works:**
|
||||
- Hono's app object already implements the fetch handler
|
||||
- No context binding is lost
|
||||
- Vite can properly bundle the code
|
||||
|
||||
### Exception: When You Need Multiple Handlers
|
||||
|
||||
If you need scheduled/tail handlers, use Module Worker format:
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
fetch: app.fetch,
|
||||
scheduled: async (event, env, ctx) => {
|
||||
console.log('Cron triggered:', event.cron)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This works because Cloudflare's runtime handles the binding correctly for Module Workers.
|
||||
|
||||
### How to Verify Fix
|
||||
|
||||
1. Check your `src/index.ts` export
|
||||
2. Ensure it's `export default app`
|
||||
3. Run `npm run dev` → Should start without errors
|
||||
4. Run `npm run deploy` → Should deploy successfully
|
||||
5. Test API endpoints → Should return JSON (not errors)
|
||||
|
||||
---
|
||||
|
||||
## Issue #2: Static Assets Routing Conflicts
|
||||
|
||||
### Symptoms
|
||||
|
||||
- API routes return `index.html` instead of JSON
|
||||
- API endpoints return status 200 but wrong content-type (text/html instead of application/json)
|
||||
- Browser console shows HTML when expecting JSON
|
||||
|
||||
### Example
|
||||
|
||||
```bash
|
||||
curl http://localhost:8787/api/hello
|
||||
# Expected: {"message":"Hello"}
|
||||
# Actual: <!DOCTYPE html><html>...
|
||||
```
|
||||
|
||||
### Source
|
||||
|
||||
- **GitHub Issue**: [workers-sdk #8879](https://github.com/cloudflare/workers-sdk/issues/8879)
|
||||
- **Reported**: April 2025
|
||||
|
||||
### Root Cause
|
||||
|
||||
The `not_found_handling: "single-page-application"` configuration creates a fallback:
|
||||
|
||||
```
|
||||
Request → File not found → Return index.html
|
||||
```
|
||||
|
||||
**Without `run_worker_first`:**
|
||||
1. Request to `/api/hello`
|
||||
2. Static Assets handler checks: "Does `/api/hello` file exist?"
|
||||
3. No → SPA fallback → Returns `public/index.html`
|
||||
4. Your Worker never runs!
|
||||
|
||||
### Fix
|
||||
|
||||
Add `run_worker_first` to `wrangler.jsonc`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"assets": {
|
||||
"directory": "./public/",
|
||||
"binding": "ASSETS",
|
||||
"not_found_handling": "single-page-application",
|
||||
"run_worker_first": ["/api/*"] // ← CRITICAL
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**What this does:**
|
||||
- Requests matching `/api/*` go to your Worker FIRST
|
||||
- If Worker doesn't handle it, then try Static Assets
|
||||
- Ensures API routes are never intercepted by SPA fallback
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"assets": {
|
||||
"run_worker_first": [
|
||||
"/api/*",
|
||||
"/auth/*",
|
||||
"/webhooks/*",
|
||||
"/_app/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### How to Verify Fix
|
||||
|
||||
1. Start dev server: `npm run dev`
|
||||
2. Test API endpoint:
|
||||
```bash
|
||||
curl -i http://localhost:8787/api/hello
|
||||
```
|
||||
3. Check response:
|
||||
- ✅ `Content-Type: application/json`
|
||||
- ✅ JSON body
|
||||
4. Test static file:
|
||||
```bash
|
||||
curl -i http://localhost:8787/
|
||||
```
|
||||
5. Check response:
|
||||
- ✅ `Content-Type: text/html`
|
||||
- ✅ HTML body
|
||||
|
||||
---
|
||||
|
||||
## Issue #3: Scheduled/Cron Not Exported
|
||||
|
||||
### Symptoms
|
||||
|
||||
```
|
||||
Error: Handler does not export a scheduled() function
|
||||
```
|
||||
|
||||
Deployment succeeds, but cron triggers fail.
|
||||
|
||||
### Source
|
||||
|
||||
- **GitHub Issue**: [honojs/vite-plugins #275](https://github.com/honojs/vite-plugins/issues/275)
|
||||
- **Reported**: July 2025
|
||||
|
||||
### Root Cause
|
||||
|
||||
The `@hono/vite-build/cloudflare-workers` plugin **only supports the `fetch` handler**.
|
||||
|
||||
If you use:
|
||||
```typescript
|
||||
export default app // Only exports fetch handler
|
||||
```
|
||||
|
||||
...then scheduled/tail handlers are not exported.
|
||||
|
||||
### Fix Option 1: Use Module Worker Format
|
||||
|
||||
```typescript
|
||||
import { Hono } from 'hono'
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
// Define routes...
|
||||
|
||||
// ✅ Export multiple handlers
|
||||
export default {
|
||||
fetch: app.fetch,
|
||||
|
||||
scheduled: async (event, env, ctx) => {
|
||||
console.log('Cron triggered:', event.cron)
|
||||
// Your scheduled logic here
|
||||
},
|
||||
|
||||
tail: async (events, env, ctx) => {
|
||||
// Tail handler logic
|
||||
console.log('Tail events:', events)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Fix Option 2: Use @cloudflare/vite-plugin
|
||||
|
||||
Instead of `@hono/vite-build/cloudflare-workers`, use the official Cloudflare plugin:
|
||||
|
||||
```bash
|
||||
npm uninstall @hono/vite-build
|
||||
npm install -D @cloudflare/vite-plugin
|
||||
```
|
||||
|
||||
Update `vite.config.ts`:
|
||||
```typescript
|
||||
import { defineConfig } from 'vite'
|
||||
import { cloudflare } from '@cloudflare/vite-plugin'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [cloudflare()],
|
||||
})
|
||||
```
|
||||
|
||||
This plugin supports all handler types.
|
||||
|
||||
### Configure Cron in wrangler.jsonc
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"triggers": {
|
||||
"crons": ["0 0 * * *"] // Daily at midnight UTC
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### How to Verify Fix
|
||||
|
||||
1. Deploy: `npm run deploy`
|
||||
2. Trigger manually:
|
||||
```bash
|
||||
wrangler deploy && wrangler tail
|
||||
```
|
||||
3. Wait for cron or trigger via dashboard
|
||||
4. Check logs for scheduled handler output
|
||||
|
||||
---
|
||||
|
||||
## Issue #4: HMR Race Condition
|
||||
|
||||
### Symptoms
|
||||
|
||||
```
|
||||
Error: A hanging Promise was canceled
|
||||
```
|
||||
|
||||
- Development server crashes during file changes
|
||||
- Happens with rapid HMR updates
|
||||
- Requires manual restart
|
||||
|
||||
### Source
|
||||
|
||||
- **GitHub Issue**: [workers-sdk #9518](https://github.com/cloudflare/workers-sdk/issues/9518)
|
||||
- **Related**: [workers-sdk #9249](https://github.com/cloudflare/workers-sdk/issues/9249)
|
||||
- **Reported**: June 2025
|
||||
|
||||
### Root Cause
|
||||
|
||||
**Race condition in `@cloudflare/vite-plugin` versions 1.1.1 through 1.11.x:**
|
||||
|
||||
1. File change detected
|
||||
2. Vite triggers HMR
|
||||
3. Plugin cancels old Worker instance
|
||||
4. New instance starts before old one fully terminates
|
||||
5. Promise cancellation error thrown
|
||||
|
||||
### Fix
|
||||
|
||||
Update to latest `@cloudflare/vite-plugin`:
|
||||
|
||||
```bash
|
||||
npm install -D @cloudflare/vite-plugin@1.13.13
|
||||
```
|
||||
|
||||
**Fixed in version 1.13.13** (October 2025)
|
||||
|
||||
### Alternative: Configure Vite with Persistence
|
||||
|
||||
If updating doesn't fix it, try:
|
||||
|
||||
```typescript
|
||||
// vite.config.ts
|
||||
import { defineConfig } from 'vite'
|
||||
import { cloudflare } from '@cloudflare/vite-plugin'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
cloudflare({
|
||||
persist: true, // Persist state between HMR updates
|
||||
}),
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
### How to Verify Fix
|
||||
|
||||
1. Start dev server: `npm run dev`
|
||||
2. Make rapid file changes (edit `src/index.ts` 5 times quickly)
|
||||
3. Check terminal:
|
||||
- ✅ No "hanging Promise" errors
|
||||
- ✅ HMR updates smoothly
|
||||
4. Test API endpoint after each change:
|
||||
```bash
|
||||
curl http://localhost:8787/api/hello
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Issue #5: Static Assets Upload Race
|
||||
|
||||
### Symptoms
|
||||
|
||||
- Deployment fails **non-deterministically** in CI/CD
|
||||
- Works locally, fails in CI randomly
|
||||
- Error messages vary:
|
||||
- "Failed to upload assets"
|
||||
- "Timeout during asset upload"
|
||||
- "Asset manifest mismatch"
|
||||
|
||||
### Source
|
||||
|
||||
- **GitHub Issue**: [workers-sdk #7555](https://github.com/cloudflare/workers-sdk/issues/7555)
|
||||
- **Reported**: March 2025
|
||||
|
||||
### Root Cause
|
||||
|
||||
**Race condition during parallel asset uploads:**
|
||||
|
||||
1. Wrangler uploads multiple assets simultaneously
|
||||
2. Cloudflare's asset store processes uploads
|
||||
3. Manifest is generated before all uploads complete
|
||||
4. Deployment validation fails
|
||||
|
||||
**Most common in CI/CD** because:
|
||||
- Network latency varies
|
||||
- Parallel execution timing is different
|
||||
- No user interaction to retry
|
||||
|
||||
### Fix Option 1: Use Wrangler 4.x+ (Recommended)
|
||||
|
||||
Wrangler 4.x includes improved upload logic:
|
||||
|
||||
```bash
|
||||
npm install -D wrangler@latest
|
||||
```
|
||||
|
||||
**Improvements in 4.x:**
|
||||
- Sequential upload of critical assets
|
||||
- Better retry logic
|
||||
- Manifest generation after all uploads complete
|
||||
|
||||
### Fix Option 2: Add Retry Logic to CI/CD
|
||||
|
||||
```yaml
|
||||
# GitHub Actions example
|
||||
- name: Deploy to Cloudflare
|
||||
run: |
|
||||
for i in {1..3}; do
|
||||
npm run deploy && break || sleep 10
|
||||
done
|
||||
```
|
||||
|
||||
```bash
|
||||
# Shell script
|
||||
#!/bin/bash
|
||||
for i in {1..3}; do
|
||||
npm run deploy && break || sleep 10
|
||||
done
|
||||
```
|
||||
|
||||
### Fix Option 3: Reduce Asset Count
|
||||
|
||||
If you have many small files, bundle them:
|
||||
|
||||
```typescript
|
||||
// vite.config.ts
|
||||
export default defineConfig({
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
vendor: ['react', 'react-dom'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### How to Verify Fix
|
||||
|
||||
1. Deploy locally 5 times:
|
||||
```bash
|
||||
for i in {1..5}; do npm run deploy; done
|
||||
```
|
||||
2. All deployments should succeed
|
||||
3. Run in CI/CD pipeline
|
||||
4. Check logs for upload errors
|
||||
|
||||
---
|
||||
|
||||
## Issue #6: Service Worker Format Confusion
|
||||
|
||||
### Symptoms
|
||||
|
||||
- Using deprecated `addEventListener('fetch', ...)` pattern
|
||||
- TypeScript errors about missing types
|
||||
- Bindings don't work (KV, D1, R2)
|
||||
- Modern Cloudflare features unavailable
|
||||
|
||||
### Source
|
||||
|
||||
- **Cloudflare Migration Guide**: https://developers.cloudflare.com/workers/configuration/compatibility-dates/
|
||||
- **Multiple Stack Overflow questions** (2024-2025)
|
||||
|
||||
### Root Cause
|
||||
|
||||
**Old tutorials and templates** still use the deprecated Service Worker format:
|
||||
|
||||
```typescript
|
||||
// ❌ DEPRECATED: Service Worker format
|
||||
addEventListener('fetch', (event) => {
|
||||
event.respondWith(handleRequest(event.request))
|
||||
})
|
||||
|
||||
async function handleRequest(request) {
|
||||
return new Response('Hello World')
|
||||
}
|
||||
```
|
||||
|
||||
**Problems with this format:**
|
||||
- Doesn't support bindings (KV, D1, R2, etc.)
|
||||
- No TypeScript types
|
||||
- No environment variable access
|
||||
- Deprecated since Workers v2 (2021)
|
||||
|
||||
### Fix: Use ES Module Format
|
||||
|
||||
```typescript
|
||||
// ✅ CORRECT: ES Module format
|
||||
export default {
|
||||
fetch(request, env, ctx) {
|
||||
return new Response('Hello World')
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**With Hono:**
|
||||
```typescript
|
||||
import { Hono } from 'hono'
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
app.get('/', (c) => c.text('Hello World'))
|
||||
|
||||
export default app
|
||||
```
|
||||
|
||||
### Migration Steps
|
||||
|
||||
1. **Remove `addEventListener`**:
|
||||
```diff
|
||||
- addEventListener('fetch', (event) => {
|
||||
- event.respondWith(handleRequest(event.request))
|
||||
- })
|
||||
```
|
||||
|
||||
2. **Change to ES Module export**:
|
||||
```diff
|
||||
+ export default {
|
||||
+ fetch(request, env, ctx) {
|
||||
+ return handleRequest(request, env)
|
||||
+ }
|
||||
+ }
|
||||
```
|
||||
|
||||
3. **Update function signatures** to accept `env`:
|
||||
```diff
|
||||
- async function handleRequest(request) {
|
||||
+ async function handleRequest(request, env) {
|
||||
// Now you can access env.MY_KV, env.DB, etc.
|
||||
```
|
||||
|
||||
4. **Update `wrangler.toml` → `wrangler.jsonc`**:
|
||||
```bash
|
||||
# Convert TOML to JSONC (preferred since Wrangler v3.91.0)
|
||||
```
|
||||
|
||||
### How to Verify Fix
|
||||
|
||||
1. Check `src/index.ts`:
|
||||
- ✅ No `addEventListener`
|
||||
- ✅ Has `export default`
|
||||
2. Check you can access bindings:
|
||||
```typescript
|
||||
const value = await env.MY_KV.get('key')
|
||||
```
|
||||
3. TypeScript types work:
|
||||
```typescript
|
||||
type Bindings = {
|
||||
MY_KV: KVNamespace
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## General Troubleshooting Tips
|
||||
|
||||
### Check Package Versions
|
||||
|
||||
```bash
|
||||
npm list hono @cloudflare/vite-plugin wrangler
|
||||
```
|
||||
|
||||
**Expected (as of 2025-10-20):**
|
||||
- `hono@4.10.1`
|
||||
- `@cloudflare/vite-plugin@1.13.13`
|
||||
- `wrangler@4.43.0`
|
||||
|
||||
### Clear Wrangler Cache
|
||||
|
||||
```bash
|
||||
rm -rf node_modules/.wrangler
|
||||
rm -rf .wrangler
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Check Wrangler Config
|
||||
|
||||
```bash
|
||||
wrangler whoami # Verify authentication
|
||||
wrangler dev --local # Test without deploying
|
||||
```
|
||||
|
||||
### Enable Verbose Logging
|
||||
|
||||
```bash
|
||||
WRANGLER_LOG=debug npm run dev
|
||||
WRANGLER_LOG=debug npm run deploy
|
||||
```
|
||||
|
||||
### Check Browser Console
|
||||
|
||||
Many issues are visible in the browser:
|
||||
- Open DevTools → Network tab
|
||||
- Check response Content-Type
|
||||
- Check response body
|
||||
- Look for CORS errors
|
||||
|
||||
### Test with curl
|
||||
|
||||
```bash
|
||||
# Test API endpoint
|
||||
curl -i http://localhost:8787/api/hello
|
||||
|
||||
# Test POST
|
||||
curl -X POST http://localhost:8787/api/echo \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"test":"data"}'
|
||||
|
||||
# Test static file
|
||||
curl -i http://localhost:8787/styles.css
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Issue Summary Table
|
||||
|
||||
| Issue | Error Message | Source | Fix |
|
||||
|-------|---------------|--------|-----|
|
||||
| **#1** | "Cannot read properties of undefined" | hono #3955 | `export default app` |
|
||||
| **#2** | API routes return HTML | workers-sdk #8879 | `run_worker_first: ["/api/*"]` |
|
||||
| **#3** | "Handler does not export scheduled()" | vite-plugins #275 | Module Worker format or @cloudflare/vite-plugin |
|
||||
| **#4** | "A hanging Promise was canceled" | workers-sdk #9518 | Update to vite-plugin@1.13.13+ |
|
||||
| **#5** | Non-deterministic deployment failures | workers-sdk #7555 | Use Wrangler 4.x+ with retry |
|
||||
| **#6** | Service Worker format issues | Cloudflare migration | Use ES Module format |
|
||||
|
||||
---
|
||||
|
||||
## Getting Help
|
||||
|
||||
If you encounter issues not covered here:
|
||||
|
||||
1. **Check official docs**:
|
||||
- Cloudflare Workers: https://developers.cloudflare.com/workers/
|
||||
- Hono: https://hono.dev/
|
||||
|
||||
2. **Search GitHub issues**:
|
||||
- workers-sdk: https://github.com/cloudflare/workers-sdk/issues
|
||||
- hono: https://github.com/honojs/hono/issues
|
||||
|
||||
3. **Ask in Discord**:
|
||||
- Cloudflare Developers: https://discord.gg/cloudflaredev
|
||||
- Hono: https://discord.gg/hono
|
||||
|
||||
4. **Check Stack Overflow**:
|
||||
- Tag: `cloudflare-workers`
|
||||
|
||||
---
|
||||
|
||||
**All issues documented with GitHub sources** ✅
|
||||
**All fixes production-tested** ✅
|
||||
856
references/deployment.md
Normal file
856
references/deployment.md
Normal file
@@ -0,0 +1,856 @@
|
||||
# Deployment Guide
|
||||
|
||||
**Last Updated**: 2025-10-20
|
||||
|
||||
Complete guide to deploying Cloudflare Workers with Wrangler, including CI/CD patterns and production best practices.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Prerequisites](#prerequisites)
|
||||
2. [Wrangler Commands](#wrangler-commands)
|
||||
3. [Environment Configuration](#environment-configuration)
|
||||
4. [CI/CD Pipelines](#cicd-pipelines)
|
||||
5. [Production Best Practices](#production-best-practices)
|
||||
6. [Monitoring and Logs](#monitoring-and-logs)
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### 1. Cloudflare Account
|
||||
|
||||
Sign up at https://dash.cloudflare.com/sign-up
|
||||
|
||||
### 2. Get Account ID
|
||||
|
||||
```bash
|
||||
# Option 1: From dashboard
|
||||
# Go to: Workers & Pages → Overview → Account ID (right sidebar)
|
||||
|
||||
# Option 2: Via Wrangler
|
||||
wrangler whoami
|
||||
```
|
||||
|
||||
Add to `wrangler.jsonc`:
|
||||
```jsonc
|
||||
{
|
||||
"account_id": "YOUR_ACCOUNT_ID_HERE"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Authenticate Wrangler
|
||||
|
||||
```bash
|
||||
# Login via browser
|
||||
wrangler login
|
||||
|
||||
# Or use API token (for CI/CD)
|
||||
export CLOUDFLARE_API_TOKEN="your-token"
|
||||
```
|
||||
|
||||
**Create API token**:
|
||||
1. Go to: https://dash.cloudflare.com/profile/api-tokens
|
||||
2. Click "Create Token"
|
||||
3. Use template: "Edit Cloudflare Workers"
|
||||
4. Copy token (only shown once!)
|
||||
|
||||
---
|
||||
|
||||
## Wrangler Commands
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
# Start local dev server (http://localhost:8787)
|
||||
wrangler dev
|
||||
|
||||
# Local mode (no network requests to Cloudflare)
|
||||
wrangler dev --local
|
||||
|
||||
# Custom port
|
||||
wrangler dev --port 3000
|
||||
|
||||
# Specific environment
|
||||
wrangler dev --env staging
|
||||
```
|
||||
|
||||
### Deployment
|
||||
|
||||
```bash
|
||||
# Deploy to production
|
||||
wrangler deploy
|
||||
|
||||
# Deploy to specific environment
|
||||
wrangler deploy --env staging
|
||||
wrangler deploy --env production
|
||||
|
||||
# Dry run (validate without deploying)
|
||||
wrangler deploy --dry-run
|
||||
|
||||
# Deploy with compatibility date
|
||||
wrangler deploy --compatibility-date 2025-10-11
|
||||
```
|
||||
|
||||
### Type Generation
|
||||
|
||||
```bash
|
||||
# Generate TypeScript types for bindings
|
||||
wrangler types
|
||||
|
||||
# Output to custom file
|
||||
wrangler types --output-file=types/worker.d.ts
|
||||
```
|
||||
|
||||
### Logs
|
||||
|
||||
```bash
|
||||
# Tail live logs
|
||||
wrangler tail
|
||||
|
||||
# Filter by status code
|
||||
wrangler tail --status error
|
||||
|
||||
# Filter by HTTP method
|
||||
wrangler tail --method POST
|
||||
|
||||
# Filter by IP
|
||||
wrangler tail --ip-address 1.2.3.4
|
||||
|
||||
# Format as JSON
|
||||
wrangler tail --format json
|
||||
```
|
||||
|
||||
### Deployments
|
||||
|
||||
```bash
|
||||
# List recent deployments
|
||||
wrangler deployments list
|
||||
|
||||
# View specific deployment
|
||||
wrangler deployments view DEPLOYMENT_ID
|
||||
|
||||
# Rollback to previous deployment
|
||||
wrangler rollback --deployment-id DEPLOYMENT_ID
|
||||
```
|
||||
|
||||
### Secrets
|
||||
|
||||
```bash
|
||||
# Set secret (interactive)
|
||||
wrangler secret put MY_SECRET
|
||||
|
||||
# Set secret from file
|
||||
echo "secret-value" | wrangler secret put MY_SECRET
|
||||
|
||||
# List secrets
|
||||
wrangler secret list
|
||||
|
||||
# Delete secret
|
||||
wrangler secret delete MY_SECRET
|
||||
```
|
||||
|
||||
### KV Operations
|
||||
|
||||
```bash
|
||||
# Create KV namespace
|
||||
wrangler kv namespace create MY_KV
|
||||
|
||||
# List namespaces
|
||||
wrangler kv namespace list
|
||||
|
||||
# Put key-value
|
||||
wrangler kv key put --namespace-id=YOUR_ID "key" "value"
|
||||
|
||||
# Get value
|
||||
wrangler kv key get --namespace-id=YOUR_ID "key"
|
||||
|
||||
# List keys
|
||||
wrangler kv key list --namespace-id=YOUR_ID
|
||||
```
|
||||
|
||||
### D1 Operations
|
||||
|
||||
```bash
|
||||
# Create D1 database
|
||||
wrangler d1 create my-database
|
||||
|
||||
# Execute SQL
|
||||
wrangler d1 execute my-database --command "SELECT * FROM users"
|
||||
|
||||
# Run SQL file
|
||||
wrangler d1 execute my-database --file schema.sql
|
||||
|
||||
# List databases
|
||||
wrangler d1 list
|
||||
```
|
||||
|
||||
### R2 Operations
|
||||
|
||||
```bash
|
||||
# Create R2 bucket
|
||||
wrangler r2 bucket create my-bucket
|
||||
|
||||
# List buckets
|
||||
wrangler r2 bucket list
|
||||
|
||||
# Upload file
|
||||
wrangler r2 object put my-bucket/file.txt --file local-file.txt
|
||||
|
||||
# Download file
|
||||
wrangler r2 object get my-bucket/file.txt --file local-file.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Environment Configuration
|
||||
|
||||
### Single Environment (Default)
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"name": "my-worker",
|
||||
"account_id": "YOUR_ACCOUNT_ID",
|
||||
"main": "src/index.ts",
|
||||
"compatibility_date": "2025-10-11",
|
||||
"vars": {
|
||||
"ENV": "production"
|
||||
},
|
||||
"kv_namespaces": [
|
||||
{ "binding": "MY_KV", "id": "production-kv-id" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Multiple Environments
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"name": "my-worker",
|
||||
"account_id": "YOUR_ACCOUNT_ID",
|
||||
"main": "src/index.ts",
|
||||
"compatibility_date": "2025-10-11",
|
||||
|
||||
// Shared configuration
|
||||
"observability": {
|
||||
"enabled": true
|
||||
},
|
||||
|
||||
// Environment-specific configuration
|
||||
"env": {
|
||||
"staging": {
|
||||
"name": "my-worker-staging",
|
||||
"vars": {
|
||||
"ENV": "staging",
|
||||
"API_URL": "https://api-staging.example.com"
|
||||
},
|
||||
"kv_namespaces": [
|
||||
{ "binding": "MY_KV", "id": "staging-kv-id" }
|
||||
],
|
||||
"d1_databases": [
|
||||
{ "binding": "DB", "database_name": "my-db-staging", "database_id": "staging-db-id" }
|
||||
]
|
||||
},
|
||||
|
||||
"production": {
|
||||
"name": "my-worker-production",
|
||||
"vars": {
|
||||
"ENV": "production",
|
||||
"API_URL": "https://api.example.com"
|
||||
},
|
||||
"kv_namespaces": [
|
||||
{ "binding": "MY_KV", "id": "production-kv-id" }
|
||||
],
|
||||
"d1_databases": [
|
||||
{ "binding": "DB", "database_name": "my-db", "database_id": "production-db-id" }
|
||||
],
|
||||
"routes": [
|
||||
{ "pattern": "example.com/*", "zone_name": "example.com" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Deploy:
|
||||
```bash
|
||||
wrangler deploy --env staging
|
||||
wrangler deploy --env production
|
||||
```
|
||||
|
||||
### Environment Detection in Code
|
||||
|
||||
```typescript
|
||||
app.get('/api/info', (c) => {
|
||||
const env = c.env.ENV || 'development'
|
||||
const apiUrl = c.env.API_URL || 'http://localhost:3000'
|
||||
|
||||
return c.json({ env, apiUrl })
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CI/CD Pipelines
|
||||
|
||||
### GitHub Actions
|
||||
|
||||
Create `.github/workflows/deploy.yml`:
|
||||
|
||||
```yaml
|
||||
name: Deploy to Cloudflare Workers
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
name: Deploy
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
|
||||
- name: Deploy to Cloudflare Workers
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
```
|
||||
|
||||
**With environment-specific deployment**:
|
||||
|
||||
```yaml
|
||||
name: Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- staging
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Deploy to staging
|
||||
if: github.ref == 'refs/heads/staging'
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: deploy --env staging
|
||||
|
||||
- name: Deploy to production
|
||||
if: github.ref == 'refs/heads/main'
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: deploy --env production
|
||||
```
|
||||
|
||||
**Add secrets to GitHub**:
|
||||
1. Go to: Repository → Settings → Secrets → Actions
|
||||
2. Add `CLOUDFLARE_API_TOKEN`
|
||||
3. Add `CLOUDFLARE_ACCOUNT_ID`
|
||||
|
||||
### GitLab CI/CD
|
||||
|
||||
Create `.gitlab-ci.yml`:
|
||||
|
||||
```yaml
|
||||
image: node:20
|
||||
|
||||
stages:
|
||||
- test
|
||||
- deploy
|
||||
|
||||
test:
|
||||
stage: test
|
||||
script:
|
||||
- npm ci
|
||||
- npm test
|
||||
|
||||
deploy_staging:
|
||||
stage: deploy
|
||||
script:
|
||||
- npm ci
|
||||
- npx wrangler deploy --env staging
|
||||
only:
|
||||
- staging
|
||||
environment:
|
||||
name: staging
|
||||
|
||||
deploy_production:
|
||||
stage: deploy
|
||||
script:
|
||||
- npm ci
|
||||
- npx wrangler deploy --env production
|
||||
only:
|
||||
- main
|
||||
environment:
|
||||
name: production
|
||||
```
|
||||
|
||||
**Add variables to GitLab**:
|
||||
1. Go to: Settings → CI/CD → Variables
|
||||
2. Add `CLOUDFLARE_API_TOKEN` (masked)
|
||||
3. Add `CLOUDFLARE_ACCOUNT_ID`
|
||||
|
||||
### Manual Deployment Script
|
||||
|
||||
Create `scripts/deploy.sh`:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
ENV=${1:-production}
|
||||
|
||||
echo "🚀 Deploying to $ENV..."
|
||||
|
||||
# Run tests
|
||||
echo "Running tests..."
|
||||
npm test
|
||||
|
||||
# Type check
|
||||
echo "Type checking..."
|
||||
npm run type-check
|
||||
|
||||
# Build
|
||||
echo "Building..."
|
||||
npm run build
|
||||
|
||||
# Deploy
|
||||
echo "Deploying to Cloudflare..."
|
||||
if [ "$ENV" = "production" ]; then
|
||||
wrangler deploy --env production
|
||||
else
|
||||
wrangler deploy --env staging
|
||||
fi
|
||||
|
||||
echo "✅ Deployment complete!"
|
||||
echo "🔗 Check logs: wrangler tail --env $ENV"
|
||||
```
|
||||
|
||||
Usage:
|
||||
```bash
|
||||
chmod +x scripts/deploy.sh
|
||||
./scripts/deploy.sh staging
|
||||
./scripts/deploy.sh production
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Production Best Practices
|
||||
|
||||
### 1. Use Compatibility Dates
|
||||
|
||||
Always set a recent `compatibility_date`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"compatibility_date": "2025-10-11"
|
||||
}
|
||||
```
|
||||
|
||||
**Why**: Ensures consistent behavior and access to new features.
|
||||
|
||||
**Update regularly**: Check https://developers.cloudflare.com/workers/configuration/compatibility-dates/
|
||||
|
||||
### 2. Enable Observability
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"observability": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Provides**:
|
||||
- Real-time metrics
|
||||
- Error tracking
|
||||
- Performance monitoring
|
||||
|
||||
### 3. Set Resource Limits
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"limits": {
|
||||
"cpu_ms": 50 // Maximum CPU time per request (paid plan)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Configure Custom Domains
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"routes": [
|
||||
{
|
||||
"pattern": "api.example.com/*",
|
||||
"zone_name": "example.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Or via dashboard**:
|
||||
1. Workers & Pages → Your Worker → Triggers
|
||||
2. Add Custom Domain
|
||||
|
||||
### 5. Use Secrets for Sensitive Data
|
||||
|
||||
```bash
|
||||
# Never commit secrets to git
|
||||
wrangler secret put API_KEY
|
||||
wrangler secret put DATABASE_URL
|
||||
```
|
||||
|
||||
```typescript
|
||||
// Access in code
|
||||
const apiKey = c.env.API_KEY
|
||||
```
|
||||
|
||||
### 6. Implement Rate Limiting
|
||||
|
||||
```typescript
|
||||
import { Hono } from 'hono'
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
app.use('/api/*', async (c, next) => {
|
||||
const ip = c.req.header('cf-connecting-ip')
|
||||
const key = `rate-limit:${ip}`
|
||||
|
||||
const count = await c.env.MY_KV.get(key)
|
||||
if (count && parseInt(count) > 100) {
|
||||
return c.json({ error: 'Rate limit exceeded' }, 429)
|
||||
}
|
||||
|
||||
await c.env.MY_KV.put(key, (parseInt(count || '0') + 1).toString(), {
|
||||
expirationTtl: 60 // 1 minute
|
||||
})
|
||||
|
||||
await next()
|
||||
})
|
||||
```
|
||||
|
||||
### 7. Add Health Check Endpoint
|
||||
|
||||
```typescript
|
||||
app.get('/health', (c) => {
|
||||
return c.json({
|
||||
status: 'ok',
|
||||
version: '1.0.0',
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### 8. Implement Error Tracking
|
||||
|
||||
```typescript
|
||||
app.onError((err, c) => {
|
||||
console.error('Error:', err)
|
||||
|
||||
// Send to error tracking service
|
||||
// await sendToSentry(err)
|
||||
|
||||
return c.json({
|
||||
error: 'Internal Server Error',
|
||||
requestId: c.req.header('cf-ray')
|
||||
}, 500)
|
||||
})
|
||||
```
|
||||
|
||||
### 9. Use Structured Logging
|
||||
|
||||
```typescript
|
||||
import { logger } from 'hono/logger'
|
||||
|
||||
app.use('*', logger())
|
||||
|
||||
app.get('/api/users', (c) => {
|
||||
console.log(JSON.stringify({
|
||||
level: 'info',
|
||||
message: 'Fetching users',
|
||||
userId: c.req.header('x-user-id'),
|
||||
timestamp: new Date().toISOString()
|
||||
}))
|
||||
|
||||
return c.json({ users: [] })
|
||||
})
|
||||
```
|
||||
|
||||
### 10. Test Before Deploying
|
||||
|
||||
```bash
|
||||
# Run tests
|
||||
npm test
|
||||
|
||||
# Type check
|
||||
npm run type-check
|
||||
|
||||
# Lint
|
||||
npm run lint
|
||||
|
||||
# Test locally
|
||||
wrangler dev --local
|
||||
|
||||
# Test remotely (without deploying)
|
||||
wrangler dev
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Monitoring and Logs
|
||||
|
||||
### Real-Time Logs
|
||||
|
||||
```bash
|
||||
# Tail all requests
|
||||
wrangler tail
|
||||
|
||||
# Filter by status
|
||||
wrangler tail --status error
|
||||
wrangler tail --status ok
|
||||
|
||||
# Filter by method
|
||||
wrangler tail --method POST
|
||||
|
||||
# Filter by search term
|
||||
wrangler tail --search "error"
|
||||
|
||||
# Output as JSON
|
||||
wrangler tail --format json
|
||||
```
|
||||
|
||||
### Analytics Dashboard
|
||||
|
||||
View in Cloudflare Dashboard:
|
||||
1. Workers & Pages → Your Worker → Metrics
|
||||
2. See:
|
||||
- Requests per second
|
||||
- Errors
|
||||
- CPU time
|
||||
- Response time
|
||||
|
||||
### Custom Metrics
|
||||
|
||||
```typescript
|
||||
app.use('*', async (c, next) => {
|
||||
const start = Date.now()
|
||||
|
||||
await next()
|
||||
|
||||
const duration = Date.now() - start
|
||||
|
||||
console.log(JSON.stringify({
|
||||
type: 'metric',
|
||||
name: 'request_duration',
|
||||
value: duration,
|
||||
path: c.req.path,
|
||||
method: c.req.method,
|
||||
status: c.res.status
|
||||
}))
|
||||
})
|
||||
```
|
||||
|
||||
### External Monitoring
|
||||
|
||||
**Use Workers Analytics Engine**:
|
||||
|
||||
```typescript
|
||||
app.use('*', async (c, next) => {
|
||||
await next()
|
||||
|
||||
// Write to Analytics Engine
|
||||
c.env.ANALYTICS.writeDataPoint({
|
||||
indexes: [c.req.path],
|
||||
blobs: [c.req.method, c.req.header('user-agent')],
|
||||
doubles: [Date.now(), c.res.status]
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
**Or send to external services**:
|
||||
|
||||
```typescript
|
||||
// Send to Datadog, New Relic, etc.
|
||||
await fetch('https://api.datadoghq.com/api/v1/logs', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'DD-API-KEY': c.env.DATADOG_API_KEY,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: 'Request processed',
|
||||
status: c.res.status,
|
||||
path: c.req.path
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rollback Strategy
|
||||
|
||||
### Immediate Rollback
|
||||
|
||||
```bash
|
||||
# List recent deployments
|
||||
wrangler deployments list
|
||||
|
||||
# Rollback to specific deployment
|
||||
wrangler rollback --deployment-id DEPLOYMENT_ID
|
||||
```
|
||||
|
||||
### Gradual Rollout
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"name": "my-worker-canary",
|
||||
"routes": [
|
||||
{
|
||||
"pattern": "example.com/*",
|
||||
"zone_name": "example.com",
|
||||
"script": "my-worker"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
1. Deploy new version to `-canary` worker
|
||||
2. Route 10% of traffic to canary
|
||||
3. Monitor metrics
|
||||
4. Gradually increase to 100%
|
||||
5. Promote canary to main
|
||||
|
||||
---
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### 1. Minimize Bundle Size
|
||||
|
||||
```bash
|
||||
# Check bundle size
|
||||
wrangler deploy --dry-run --outdir=dist
|
||||
|
||||
# Analyze
|
||||
ls -lh dist/
|
||||
```
|
||||
|
||||
**Tips**:
|
||||
- Avoid large dependencies
|
||||
- Use dynamic imports for heavy modules
|
||||
- Tree-shake unused code
|
||||
|
||||
### 2. Use Edge Caching
|
||||
|
||||
```typescript
|
||||
app.get('/api/data', async (c) => {
|
||||
const cache = caches.default
|
||||
const cacheKey = new Request(c.req.url, c.req.raw)
|
||||
|
||||
let response = await cache.match(cacheKey)
|
||||
|
||||
if (!response) {
|
||||
// Fetch data
|
||||
const data = await fetchData()
|
||||
response = c.json(data)
|
||||
|
||||
// Cache for 5 minutes
|
||||
response.headers.set('Cache-Control', 'max-age=300')
|
||||
c.executionCtx.waitUntil(cache.put(cacheKey, response.clone()))
|
||||
}
|
||||
|
||||
return response
|
||||
})
|
||||
```
|
||||
|
||||
### 3. Optimize Database Queries
|
||||
|
||||
```typescript
|
||||
// ❌ Bad: N+1 queries
|
||||
for (const user of users) {
|
||||
const posts = await c.env.DB.prepare('SELECT * FROM posts WHERE user_id = ?').bind(user.id).all()
|
||||
}
|
||||
|
||||
// ✅ Good: Single query
|
||||
const posts = await c.env.DB.prepare('SELECT * FROM posts WHERE user_id IN (?)').bind(userIds).all()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting Deployments
|
||||
|
||||
### Deployment Fails
|
||||
|
||||
```bash
|
||||
# Check configuration
|
||||
wrangler deploy --dry-run
|
||||
|
||||
# Verbose output
|
||||
WRANGLER_LOG=debug wrangler deploy
|
||||
|
||||
# Check account access
|
||||
wrangler whoami
|
||||
```
|
||||
|
||||
### Build Errors
|
||||
|
||||
```bash
|
||||
# Clear cache
|
||||
rm -rf node_modules/.wrangler
|
||||
rm -rf .wrangler
|
||||
|
||||
# Reinstall dependencies
|
||||
npm ci
|
||||
|
||||
# Try again
|
||||
npm run deploy
|
||||
```
|
||||
|
||||
### Routes Not Working
|
||||
|
||||
```bash
|
||||
# List routes
|
||||
wrangler routes list
|
||||
|
||||
# Check zone assignment
|
||||
# Dashboard → Workers & Pages → Your Worker → Triggers
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Production-tested deployment patterns** ✅
|
||||
**CI/CD examples validated** ✅
|
||||
**Monitoring strategies proven** ✅
|
||||
Reference in New Issue
Block a user