Relevant source files
The following files were used as context for generating this wiki page:
- src/middleware/serve-static/index.ts
- src/adapter/bun/serve-static.ts
- src/middleware/secure-headers/secure-headers.ts
- src/adapter/deno/serve-static.ts
- src/helper/streaming/sse.ts
- src/adapter/cloudflare-workers/serve-static-module.ts
- src/helper/ssg/ssg.ts
- src/middleware/cache/index.ts
- src/helper/streaming/stream.ts
- src/adapter/cloudflare-pages/handler.ts
- src/adapter/cloudflare-workers/serve-static.ts
- src/middleware/etag/index.ts
- src/helper/streaming/text.ts
- src/middleware/ip-restriction/index.ts
- src/context.ts
- src/middleware/serve-static/path.ts
- src/adapter/lambda-edge/handler.ts
- src/helper/ssg/middleware.ts
- src/middleware/language/language.ts
- src/adapter/cloudflare-workers/utils.ts
- src/hono-base.ts
- src/adapter/cloudflare-workers/index.ts
- src/adapter/deno/index.ts
- src/router/reg-exp-router/router.ts
- src/adapter/cloudflare-pages/index.ts
- src/utils/mime.ts
- src/middleware/etag/digest.ts
- src/adapter/netlify/handler.ts
- src/router/linear-router/router.ts
- package.json
Overview
Hono provides a unified static file serving middleware that bridges web standards with runtime-specific file system and asset APIs across different JavaScript runtimes. By abstracting lower-level file retrieval functions, the middleware processes incoming requests to safely deliver assets, handle precompressed encodings, and integrate with environment adapters such as Bun Adapter and Deno Adapter.
Sources: src/middleware/serve-static/index.ts:32-49, src/adapter/bun/serve-static.ts:8-31, src/adapter/deno/serve-static.ts:8-41
Middleware Architecture and Core Interfaces
Public Options and Runtime Abstractions
The ServeStaticOptions interface defines the configuration properties accepted by the static file serving middleware. It supports setting a root directory, a fixed target path, custom MIME type mappings, precompression enablement, request path rewriting, and lifecycle hooks for file resolution outcomes.
export type ServeStaticOptions<E extends Env = Env> = {
root?: string
path?: string
precompressed?: boolean
mimes?: Record<string, string>
rewriteRequestPath?: (path: string) => string
onFound?: (path: string, c: Context<E>) => void | Promise<void>
onNotFound?: (path: string, c: Context<E>) => void | Promise<void>
}Sources: src/middleware/serve-static/index.ts:13-21
The base serveStatic function wraps runtime-specific asset fetchers into a standard MiddlewareHandler. Because underlying engines vary, the function requires a runtime-provided getContent implementation alongside optional path-joining and directory-checking utilities.
| Option | Type | Default | Purpose |
|---|---|---|---|
root | string | './' | Base directory prefix for resolving asset paths |
path | string | undefined | Static file path override bypassing request path inspection |
precompressed | boolean | undefined | Enables checking for precompressed variants (.br, .zst, .gz) |
mimes | Record<string, string> | undefined | Custom file extension to MIME type overrides |
rewriteRequestPath | (path: string) => string | undefined | Transforms the incoming request path prior to lookup |
onFound | (path: string, c: Context<E>) => void | Promise<void> | undefined | Lifecycle callback invoked when an asset is successfully retrieved |
onNotFound | (path: string, c: Context<E>) => void | Promise<void> | undefined | Lifecycle callback invoked when asset retrieval fails |
Sources: src/middleware/serve-static/index.ts:35-48, src/middleware/serve-static/index.ts:50-52
Note
The serveStatic middleware is not called directly by application code under normal circumstances; runtime-specific wrappers such as those in Bun Adapter and Deno Adapter supply the appropriate getContent callback and runtime binding.
Sources: src/middleware/serve-static/index.ts:32-34
Supported Encoding Constants
When precompressed is enabled, the middleware evaluates client support against three compression algorithms defined by the ENCODINGS constant map, checking file extensions in strict order.
const ENCODINGS = {
br: '.br',
zstd: '.zst',
gzip: '.gz',
} as const
const ENCODINGS_ORDERED_KEYS = Object.keys(ENCODINGS) as (keyof typeof ENCODINGS)[]Sources: src/middleware/serve-static/index.ts:23-28
Important
ENCODINGS_ORDERED_KEYS iterates through Brotli (br), Zstd (zstd), and Gzip (gzip) sequentially, meaning the first matching precompressed file extension found on disk takes precedence over subsequent alternatives.
Sources: src/middleware/serve-static/index.ts:23-28, src/middleware/serve-static/index.ts:104-116
Path Normalization and Route Rewriting
URI Decoding and Security Validation
When a static file path is not explicitly configured via options.path, the middleware extracts the filename from the request path using tryDecodeURI(c.req.path). To prevent directory traversal attacks, the decoded string is immediately tested against a security regular expression that detects relative path components and illegal slash sequences.
filename = tryDecodeURI(c.req.path)
if (/(?:^|[\/\\])\.{1,2}(?:$|[\/\\])|[\/\\]{2,}|\\/.test(filename)) {
throw new Error()
}Sources: src/middleware/serve-static/index.ts:65-69
Warning
If URI decoding fails or the validation regex matches dot-segments (. or ..) or backslashes (\), the middleware triggers onNotFound and delegates control to the next handler rather than leaking files outside the root directory.
Sources: src/middleware/serve-static/index.ts:70-73
Request Path Rewriting and Joining
Once validated, the filename can be transformed by an optional rewriteRequestPath function provided in ServeStaticOptions. The resulting path is then combined with the root directory using the join function, which defaults to defaultJoin.
let path = join(
root,
!optionPath && options.rewriteRequestPath ? options.rewriteRequestPath(filename) : filename
)Sources: src/middleware/serve-static/index.ts:76-79
The defaultJoin implementation filters out empty path segments, normalizes multiple consecutive forward slashes, and resolves relative . and .. segments sequentially.
export const defaultJoin = (...paths: string[]): string => {
let result = paths.filter((p) => p !== '').join('/')
result = result.replace(/(?<=\/)\/+/g, '')
const segments = result.split('/')
const resolved = []
for (const segment of segments) {
if (segment === '..' && resolved.length > 0 && resolved.at(-1) !== '..') {
resolved.pop()
} else if (segment !== '.') {
resolved.push(segment)
}
}
return resolved.join('/') || '.'
}Sources: src/middleware/serve-static/path.ts:5-24
Note
defaultJoin is designed exclusively for POSIX-style paths and does not support Windows drive letters or backslash separators. For Windows path compatibility, developers must supply a platform-aware join helper such as join from node:path.
Sources: src/middleware/serve-static/path.ts:1-4
MIME Type Resolution and Overrides
File Extension Mapping and Lookup Mechanics
When static content is successfully retrieved from disk or key-value storage, the middleware determines its Internet media type by invoking getMimeType(path, options.mimes) with any custom mime mappings supplied via options.mimes, falling back to the default baseMime collection. The utility extracts file extensions using a regular expression that matches word characters following the final dot in the filename, converting the result to lowercase for case-insensitive lookup. If no custom or default extension match is found, the header falls back to application/octet-stream.
export const getMimeType = (
filename: string,
mimes: Record<string, string> = baseMimes
): string | undefined => {
const regexp = /\.([a-zA-Z0-9]+?)$/
const match = filename.match(regexp)
if (!match) {
return
}
return mimes[match[1].toLowerCase()]
}Sources: src/utils/mime.ts:6-16
Once the MIME type string is resolved, it is applied directly to the outgoing response headers under the Content-Type key.
const mimeType = (options.mimes && getMimeType(path, options.mimes)) || getMimeType(path)
c.header('Content-Type', mimeType || 'application/octet-stream')Sources: src/middleware/serve-static/index.ts:93-94
Note
The reverse lookup helper getExtension(mimeType) strips media type parameters and charset indicators before scanning the built-in MIME dictionary to locate a matching file extension.
Sources: src/utils/mime.ts:18-26
Default MIME Type Registry
Hono embeds an extensive collection of common web MIME types in _baseMimes, aliased publicly as mimes.
| Extension | Content-Type |
|---|---|
aac | audio/aac |
avif | image/avif |
css | text/css; charset=utf-8 |
csv | text/csv; charset=utf-8 |
html, htm | text/html; charset=utf-8 |
ico | image/x-icon |
jpeg, jpg | image/jpeg |
js, mjs | text/javascript; charset=utf-8 |
json | application/json |
jsonld | application/ld+json |
png | image/png |
svg | image/svg+xml; charset=utf-8 |
txt | text/plain; charset=utf-8 |
wasm | application/wasm |
webp | image/webp |
woff2 | font/woff2 |
xml | application/xml; charset=utf-8 |
zip | application/zip |
Sources: src/utils/mime.ts:35-92, src/utils/mime.ts:28
MIME Resolution Call-Chain Walkthrough
The verified call-chain execution flow Module -> GetMimeType traces the exact sequence of function invocations from the ES module entry point down to MIME type resolution:
module(src/adapter/cloudflare-workers/serve-static-module.ts:6-10) initiates ES module mode handling by invoking the Cloudflare Workers adapter'sserveStatic.serveStatic(src/adapter/cloudflare-workers/serve-static.ts:21-42) constructs the environment'sgetContentcallback and delegates execution tobaseServeStatic.serveStatic(src/middleware/serve-static/index.ts:35-126) normalizes request paths, checks directory status, retrieves asset content streams, and callsgetMimeType.getMimeType(src/utils/mime.ts:6-16) inspects file extension matches against custom or default dictionaries to assign the correctContent-Type.
Sources: src/adapter/cloudflare-workers/serve-static-module.ts:6-10, src/adapter/cloudflare-workers/serve-static.ts:21-42, src/middleware/serve-static/index.ts:35-126, src/utils/mime.ts:6-16
Directory Index and Fallback Handling
Missing Asset Propagation and Lifecycle Hooks
When a requested resource resolves to a directory path on disk or storage, Hono intercepts the path check via the optional isDir function. If options.isDir evaluates to a truthy value for the target path, the middleware automatically appends the default document constant to resolve index assets.
const DEFAULT_DOCUMENT = 'index.html'Sources: src/middleware/serve-static/index.ts:30, src/middleware/serve-static/index.ts:81-83
if (options.isDir && (await options.isDir(path))) {
path = join(path, DEFAULT_DOCUMENT)
}Sources: src/middleware/serve-static/index.ts:81-83
Tip
The isDir check is entirely optional; runtimes that lack asynchronous directory checks can omit the function property, causing the middleware to skip directory index resolution and attempt direct file retrieval.
Sources: src/middleware/serve-static/index.ts:47-48, src/middleware/serve-static/index.ts:81-83
When asset lookup fails to yield valid content—either because the file does not exist, the requested path triggered path traversal security checks, or directory fallback discovery came up empty—the middleware invokes the onNotFound lifecycle hook before passing control to downstream handlers via next().
await options.onNotFound?.(path, c)
await next()
returnSources: src/middleware/serve-static/index.ts:122-124
Warning
If onNotFound is not provided in ServeStaticOptions, the middleware gracefully falls through to the next handler in the Hono application stack without emitting a terminal 404 response directly.
Sources: src/middleware/serve-static/index.ts:19-21, src/middleware/serve-static/index.ts:122-123
Response Creation and Asset Streaming
Precompression and Content Negotiation
When a static asset is successfully retrieved, Hono checks whether precompression is enabled via options.precompressed and whether the content type is compressible using COMPRESSIBLE_CONTENT_TYPE_REGEX. If these conditions are met, the middleware inspects the Accept-Encoding request header to negotiate a compressed representation.
const ENCODINGS = {
br: '.br',
zstd: '.zst',
gzip: '.gz',
} as const
const ENCODINGS_ORDERED_KEYS = Object.keys(ENCODINGS) as (keyof typeof ENCODINGS)[]Sources: src/middleware/serve-static/index.ts:23-28
if (options.precompressed && (!mimeType || COMPRESSIBLE_CONTENT_TYPE_REGEX.test(mimeType))) {
const acceptEncodingSet = new Set(
c.req
.header('Accept-Encoding')
?.split(',')
.map((encoding) => encoding.trim())
)
for (const encoding of ENCODINGS_ORDERED_KEYS) {
if (!acceptEncodingSet.has(encoding)) {
continue
}
const compressedContent = (await getContent(path + ENCODINGS[encoding], c)) as Data | null
if (compressedContent) {
content = compressedContent
c.header('Content-Encoding', encoding)
c.header('Vary', 'Accept-Encoding', { append: true })
break
}
}
}Sources: src/middleware/serve-static/index.ts:96-116
Note
The precompression routine iterates through encodings in a strict evaluation order (br, zstd, gzip), appending the corresponding file extension (.br, .zst, .gz) to locate prebuilt compressed variants on storage.
Sources: src/middleware/serve-static/index.ts:23-28, src/middleware/serve-static/index.ts:104-108
Lifecycle Delegation and Payload Streaming
Once content and encoding headers are finalized, the middleware invokes the optional onFound lifecycle callback before delegating payload delivery to the context response builder.
await options.onFound?.(path, c)
return c.body(content)Sources: src/middleware/serve-static/index.ts:118-119
| Return Type / Condition | Handling Mechanism | Sources |
|---|---|---|
content instanceof Response | Clones or delegates using c.newResponse(content.body, content) | src/middleware/serve-static/index.ts:88-90 |
content (Truthy Data) | Assigns Content-Type, negotiates compression, calls onFound, and returns c.body(content) | src/middleware/serve-static/index.ts:92-120 |
| Falsy / Missing Content | Invokes onNotFound, calls next(), and falls through | src/middleware/serve-static/index.ts:122-124 |
Sources: src/middleware/serve-static/index.ts:88-124
Related
On This Page
OverviewMiddleware Architecture and Core InterfacesPublic Options and Runtime AbstractionsSupported Encoding ConstantsPath Normalization and Route RewritingURI Decoding and Security ValidationRequest Path Rewriting and JoiningMIME Type Resolution and OverridesFile Extension Mapping and Lookup MechanicsDefault MIME Type RegistryMIME Resolution Call-Chain WalkthroughDirectory Index and Fallback HandlingMissing Asset Propagation and Lifecycle HooksResponse Creation and Asset StreamingPrecompression and Content NegotiationLifecycle Delegation and Payload StreamingRelated