Skip to main content
doc0blade47/honodocumentation home
Sign in

Static File Serving

Last updated Oct 12, 2026100% grounded
Relevant source files

The following files were used as context for generating this wiki page:

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.

OptionTypeDefaultPurpose
rootstring'./'Base directory prefix for resolving asset paths
pathstringundefinedStatic file path override bypassing request path inspection
precompressedbooleanundefinedEnables checking for precompressed variants (.br, .zst, .gz)
mimesRecord<string, string>undefinedCustom file extension to MIME type overrides
rewriteRequestPath(path: string) => stringundefinedTransforms the incoming request path prior to lookup
onFound(path: string, c: Context<E>) => void | Promise<void>undefinedLifecycle callback invoked when an asset is successfully retrieved
onNotFound(path: string, c: Context<E>) => void | Promise<void>undefinedLifecycle 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.

ExtensionContent-Type
aacaudio/aac
avifimage/avif
csstext/css; charset=utf-8
csvtext/csv; charset=utf-8
html, htmtext/html; charset=utf-8
icoimage/x-icon
jpeg, jpgimage/jpeg
js, mjstext/javascript; charset=utf-8
jsonapplication/json
jsonldapplication/ld+json
pngimage/png
svgimage/svg+xml; charset=utf-8
txttext/plain; charset=utf-8
wasmapplication/wasm
webpimage/webp
woff2font/woff2
xmlapplication/xml; charset=utf-8
zipapplication/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:

  1. module (src/adapter/cloudflare-workers/serve-static-module.ts:6-10) initiates ES module mode handling by invoking the Cloudflare Workers adapter's serveStatic.
  2. serveStatic (src/adapter/cloudflare-workers/serve-static.ts:21-42) constructs the environment's getContent callback and delegates execution to baseServeStatic.
  3. serveStatic (src/middleware/serve-static/index.ts:35-126) normalizes request paths, checks directory status, retrieves asset content streams, and calls getMimeType.
  4. getMimeType (src/utils/mime.ts:6-16) inspects file extension matches against custom or default dictionaries to assign the correct Content-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()
    return

Sources: 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 / ConditionHandling MechanismSources
content instanceof ResponseClones 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 ContentInvokes onNotFound, calls next(), and falls throughsrc/middleware/serve-static/index.ts:122-124

Sources: src/middleware/serve-static/index.ts:88-124

Was this page helpful?