Skip to content
Merged
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions docs/guides/best-practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,125 @@ app.route('/books', books)
export default app
```

## HEAD Request Best Practices

### Understanding Hono's HEAD Handling

Hono automatically handles HEAD requests by converting them to GET requests and stripping the response body [1](#2-0) . This behavior is built into the framework's dispatch layer and happens before route matching occurs.

### ✅ Do: Use GET Routes for HEAD Requests

```typescript
// GOOD: This GET route automatically handles HEAD requests
app.get('/api/users', async (c) => {
const users = await getUsers()
c.header('X-Total-Count', users.length.toString())
return c.json(users)
})

// HEAD /api/users will return:
// - Same headers as GET (including X-Total-Count)
// - Status 200
// - No body (null)
```

### ✅ Do: Use Middleware for HEAD-Specific Logic

```typescript
// GOOD: Use middleware when HEAD needs different behavior
app.use('/api/resource', async (c, next) => {
await next()

// Add HEAD-specific headers after the handler
if (c.req.method === 'HEAD') {
c.header('X-HEAD-Processed', 'true')
// Don't compute expensive body content for HEAD
c.res = new Response(null, c.res)
}
})
```

### ❌ Don't: Try to Create Dedicated HEAD Handlers

```typescript
// BAD: This won't work as expected
app.head('/api/users', (c) => {
// This handler will NEVER be called
c.header('X-Custom', 'value')
return c.text('ignored')
})

// BAD: Using on() also won't work
app.on('HEAD', '/api/users', (c) => {
// Still converted to GET before route matching
})
```

### Performance Considerations

- **Avoid expensive operations in GET handlers if you expect many HEAD requests**: Use middleware to detect HEAD and skip body generation
- **Cache headers work identically**: HEAD responses respect the same caching rules as GET
- **Middleware compatibility**: Most middleware works with HEAD, but body-processing middleware (like compression) automatically skips HEAD requests [2](#2-1)

### Testing HEAD Requests

```typescript
// Always test both GET and HEAD responses
it('handles HEAD requests correctly', async () => {
const getRes = await app.request('/api/users')
const headRes = await app.request('/api/users', { method: 'HEAD' })

expect(headRes.status).toBe(getRes.status)
expect(headRes.headers.get('X-Total-Count')).toBe(
getRes.headers.get('X-Total-Count')
)
expect(headRes.body).toBe(null)
})
```

### Migration Note

If you're upgrading from Hono v3, remove any `app.head()` routes as they're no longer needed [3](#2-2) . Your existing GET routes will automatically handle HEAD requests.

---

## Notes

- The automatic HEAD conversion ensures consistent headers between GET and HEAD responses
- This behavior is consistent across all Hono runtimes (Cloudflare Workers, Deno, Bun, Node.js)
- If you need completely different logic for HEAD vs GET, consider using different endpoints rather than trying to override the framework's HEAD handling

Wiki pages you might want to explore:

- [Hono Application Class and HonoBase (honojs/hono)](/wiki/honojs/hono#2.1)

### Citations
Comment thread
yusukebe marked this conversation as resolved.
Outdated

**File:** src/hono-base.ts (L406-410)

```typescript
// Handle HEAD method
if (method === 'HEAD') {
return (async () =>
new Response(
null,
await this.#dispatch(request, executionCtx, env, 'GET')
))()
}
```

**File:** src/middleware/compress/index.ts (L46-46)

```typescript
ctx.req.method === 'HEAD' || // HEAD request
```

**File:** docs/MIGRATION.md (L34-34)

```markdown
- Hono - `app.head()` is no longer used. `app.get()` implicitly handles the HEAD method.
```

### If you want to use RPC features

The code above works well for normal use cases.
Expand Down
Loading