Skip to main content
doc0blade47/honodocumentation home
Sign in

CSRF and Body Limit

Last updated Oct 12, 2026100% grounded
Relevant source files

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

Overview

Hono provides built-in middleware to protect web applications against Cross-Site Request Forgery (CSRF) and control inbound request payload sizesCSRF Protection Middleware for Hono, Body Limit Middleware for Hono. The CSRF middleware evaluates request security by validating Origin headers and Sec-Fetch-Site values against safe and mutating HTTP method checksCSRF Protection Middleware for Hono. Concurrently, the body limit middleware safeguards server resources by inspecting Content-Length headers or incrementally tracking streaming request bodies against configured maximum size limitsBody Limit Middleware for Hono, src/middleware/body-limit/index.ts:61-94. Together, these components enforce robust request boundaries across diverse edge and serverless environments. Sources: CSRF Protection Middleware for Hono, Body Limit Middleware for Hono, src/middleware/csrf/index.ts:32-36, src/middleware/body-limit/index.ts:18-25, src/middleware/body-limit/index.ts:61-94

Cross-Site Request Forgery Protection Architecture

Public API Surface and Method Checks

Hono exports the csrf() factory function which returns a standard MiddlewareHandlersrc/middleware/csrf/index.ts:94-150. The middleware evaluates incoming requests against safe and mutating HTTP method classificationssrc/middleware/csrf/index.ts:139-144. Safe methods are matched using the regular expression /^(GET|HEAD)$/, exempting read-only requests from validationsrc/middleware/csrf/index.ts:28|28, src/middleware/csrf/index.ts:139-140. Mutating requests undergo inspection only if their content-type header matches form element submission patterns defined by /^\b(application\/x-www-form-urlencoded|multipart\/form-data|text\/plain)\b/isrc/middleware/csrf/index.ts:29-30, src/middleware/csrf/index.ts:140-141. If both sec-fetch-site and origin validations fail on a matching mutating request, the middleware instantiates a Response with body 'Forbidden' and status 403, throwing an HTTPException(403)src/middleware/csrf/index.ts:142-147. Otherwise, control advances via await next()src/middleware/csrf/index.ts:149-149.

const isSafeMethodRe = /^(GET|HEAD)$/
const isRequestedByFormElementRe =
  /^\b(application\/x-www-form-urlencoded|multipart\/form-data|text\/plain)\b/i

Sources: src/middleware/csrf/index.ts:28-30, src/middleware/csrf/index.ts:94-150

return async function csrf(c, next) {
  if (
    !isSafeMethodRe.test(c.req.method) &&
    isRequestedByFormElementRe.test(c.req.header('content-type') || 'text/plain') &&
    !(await isAllowedSecFetchSite(c.req.header('sec-fetch-site'), c)) &&
    !(await isAllowedOrigin(c.req.header('origin'), c))
  ) {
    const res = new Response('Forbidden', { status: 403 })
    throw new HTTPException(403, { res })
  }
 
  await next()
}

Sources: src/middleware/csrf/index.ts:138-150

Warning

Requests lacking both sec-fetch-site and origin headers during mutating form submissions will fail validation because both isAllowedSecFetchSite and isAllowedOrigin return false when their respective headers are undefined. Sources: src/middleware/csrf/index.ts:106-112, src/middleware/csrf/index.ts:126-136

Verification Call Chain

The execution trace for Sec-Fetch-Site validation flows through distinct functions defined across the module.

  1. csrf — Intercepts the request inside the returned middleware handler and invokes isAllowedSecFetchSite with the sec-fetch-site headersrc/middleware/csrf/index.ts:138-142.
  2. isAllowedSecFetchSite — Checks whether the header value is defined and passes the type guard isSecFetchSite before calling the configured secFetchSiteHandlersrc/middleware/csrf/index.ts:126-136.
  3. isSecFetchSite — Validates that the string matches one of the permitted union constantssrc/middleware/csrf/index.ts:15-16.

Sources: src/middleware/csrf/index.ts:15-16, src/middleware/csrf/index.ts:126-136, src/middleware/csrf/index.ts:138-142

Permitted Sec-Fetch-Site Constants

The module defines explicit runtime values and type constraints for browser-supplied Sec-Fetch-Site headers.

Constant Array / TypeValueMeaning / Role
secFetchSiteValues'same-origin'Request originated from the same site and origin.
secFetchSiteValues'same-site'Request originated from a different path on the same site.
secFetchSiteValues'none'Request was triggered by a direct user interaction like bookmark navigation.
secFetchSiteValues'cross-site'Request originated from a completely external domain.
const secFetchSiteValues = ['same-origin', 'same-site', 'none', 'cross-site'] as const
type SecFetchSite = (typeof secFetchSiteValues)[number]
 
const isSecFetchSite = (value: string): value is SecFetchSite =>
  (secFetchSiteValues as readonly string[]).includes(value)

Sources: src/middleware/csrf/index.ts:12-16

CSRF Origin Resolution and Custom Matchers

Origin and Sec-Fetch-Site Matcher Resolution

The CSRF middleware normalizes both origin and secFetchSite options during initialization through higher-order function factories. When options.origin is omitted, the default handler extracts the origin from the request URL using new URL(c.req.url).origin. If options.origin is provided as a string, it constructs an exact equality matcher. If it is an array, it evaluates membership using optsOrigin.includes(origin). When supplied as a custom function, the middleware delegates directly to the user-defined handler callback.

const originHandler: IsAllowedOriginHandler = ((optsOrigin) => {
  if (!optsOrigin) {
    return (origin, c) => origin === new URL(c.req.url).origin
  } else if (typeof optsOrigin === 'string') {
    return (origin) => origin === optsOrigin
  } else if (typeof optsOrigin === 'function') {
    return optsOrigin
  } else {
    return (origin) => optsOrigin.includes(origin)
  }
})(options?.origin)

Sources: src/middleware/csrf/index.ts:95-105

Similarly, secFetchSiteHandler builds its validation logic based on the input type of options.secFetchSite. If undefined, it restricts permitted traffic exclusively to 'same-origin'. String, array, and function options follow identical resolution branches to evaluate browser metadata against configured rules.

const secFetchSiteHandler: IsAllowedSecFetchSiteHandler = ((optsSecFetchSite) => {
  if (!optsSecFetchSite) {
    // Default: only allow same-origin
    return (secFetchSite) => secFetchSite === 'same-origin'
  } else if (typeof optsSecFetchSite === 'string') {
    return (secFetchSite) => secFetchSite === optsSecFetchSite
  } else if (typeof optsSecFetchSite === 'function') {
    return optsSecFetchSite
  } else {
    return (secFetchSite) => optsSecFetchSite.includes(secFetchSite)
  }
})(options?.secFetchSite)

Sources: src/middleware/csrf/index.ts:114-125

Note

If the origin header is entirely absent from the incoming request, isAllowedOrigin immediately short-circuits and returns false without invoking the underlying handler. Sources: src/middleware/csrf/index.ts:106-112

Custom Matcher Signatures and Handlers

Both origin and secFetchSite support asynchronous or synchronous custom handler functions that receive the header value alongside the active execution Context. These hooks allow developers to implement granular policies such as validating specific subdomain patterns or permitting cross-site requests exclusively for designated webhook endpoints.

export type IsAllowedOriginHandler = (origin: string, context: Context) => boolean | Promise<boolean>
 
export type IsAllowedSecFetchSiteHandler = (
  secFetchSite: SecFetchSite,
  context: Context
) => boolean | Promise<boolean>
type IsAllowedOriginHandler = (origin: string, context: Context) => boolean | Promise<boolean>
 
const secFetchSiteValues = ['same-origin', 'same-site', 'none', 'cross-site'] as const
type SecFetchSite = (typeof secFetchSiteValues)[number]
 
const isSecFetchSite = (value: string): value is SecFetchSite =>
  (secFetchSiteValues as readonly string[]).includes(value)
 
type IsAllowedSecFetchSiteHandler = (
  secFetchSite: SecFetchSite,
  context: Context
) => boolean | Promise<boolean>

Sources: src/middleware/csrf/index.ts:10-21

Warning

When sec-fetch-site contains an unrecognised string value not present in the runtime secFetchSiteValues tuple, isSecFetchSite evaluates to false and causes validation to fail before the custom secFetchSiteHandler can execute. Sources: src/middleware/csrf/index.ts:12-16, src/middleware/csrf/index.ts:131-135

Body Limit Middleware and Stream Wrapping

Public API Surface and Configuration Options

The body limit middleware restricts incoming request payload sizes to protect applications against denial-of-service attacks Execution Context caused by oversized payloads. It exports a bodyLimit factory function that accepts a BodyLimitOptions configuration object specifying the maxSize threshold in bytes and an optional custom error handler function onError.

Option PropertyTypeDefault BehaviorPurpose
maxSizenumberRequiredMaximum allowable body size in bytes.
onErrorOnErrorThrows HTTPException(413) with text 'Payload Too Large'Handler invoked when maxSize is exceeded.
type OnError = (c: Context) => Response | Promise<Response>
type BodyLimitOptions = {
  maxSize: number
  onError?: OnError
}

Sources: src/middleware/body-limit/index.ts:12-16

export const bodyLimit = (options: BodyLimitOptions): MiddlewareHandler => {
  const onError: OnError =
    options.onError ||
    (() => {
      const res = new Response(ERROR_MESSAGE, {
        status: 413,
      })
      throw new HTTPException(413, { res })
    })
  const maxSize = options.maxSize

Sources: src/middleware/body-limit/index.ts:50-59

Note

If options.onError is omitted, the default error handler constructs a standard Web Response with status code 413 and throws an HTTPException. Sources: src/middleware/body-limit/index.ts:51-58

Content-Length Pre-Checks and Chunk Tracking

When an incoming request reaches the middleware, it first evaluates whether a request body exists. If c.req.raw.body is null or undefined (such as standard GET or HEAD requests), the middleware bypasses inspection and invokes next() immediately.

return async function bodyLimit(c, next) {
  if (!c.req.raw.body) {
    // maybe GET or HEAD request
    return next()
  }

Sources: src/middleware/body-limit/index.ts:61-65

If headers contain a content-length without a transfer-encoding header, the middleware performs a fast pre-check by parsing the length directly. If content-length exceeds maxSize, it immediately invokes onError(c) without consuming the stream.

const hasTransferEncoding = c.req.raw.headers.has('transfer-encoding')
const hasContentLength = c.req.raw.headers.has('content-length')
 
if (hasContentLength && !hasTransferEncoding) {
  // Only Content-Length present - we can trust it
  const contentLength = parseInt(c.req.raw.headers.get('content-length') || '0', 10)
  return contentLength > maxSize ? onError(c) : next()
}

Sources: src/middleware/body-limit/index.ts:67-74

When transfer-encoding (chunked) is present or length headers are missing entirely, per RFC 7230, transfer-encoding takes precedence. The middleware consumes the underlying ReadableStream chunk by chunk using a ReadableStreamDefaultReader, accumulating byte lengths into a running size counter and pushing each Uint8Array chunk into an array. If accumulated size exceeds maxSize at any point during iteration, processing terminates instantly and invokes onError(c).

// Transfer-Encoding present (chunked) or no length headers.
// Per RFC 7230, when both are present Transfer-Encoding takes precedence
// and Content-Length is ignored. Read the body up-front so the size check
// is final before the handler runs, regardless of how (or whether) the
// handler reads the body.
let size = 0
const chunks: Uint8Array[] = []
const rawReader = c.req.raw.body.getReader()
for (;;) {
  const { done, value } = await rawReader.read()
  if (done) {
    break
  }
  size += value.length
  if (size > maxSize) {
    return onError(c)
  }
  chunks.push(value)
}

Sources: src/middleware/body-limit/index.ts:76-94

Warning

Because chunked or length-less streams are read entirely into memory arrays during validation, extremely large streaming payloads will consume memory proportional to their size up to the threshold before being rejected or forwarded. Sources: src/middleware/body-limit/index.ts:81-94

Streaming Body Reconstruction and Replacement

Once a chunked or un-sized stream successfully passes the byte limit verification, the accumulated chunks must be restored so downstream route handlers and parsers can read the request body normally. The middleware constructs a fresh ReadableStream that enqueues the saved Uint8Array chunks in sequence and closes the controller.

const requestInit: RequestInit & { duplex: 'half' } = {
  body: new ReadableStream({
    start(controller) {
      for (const chunk of chunks) {
        controller.enqueue(chunk)
      }
      controller.close()
    },
  }),

Sources: src/middleware/body-limit/index.ts:96-104

It then instantiates a new standard Request object wrapping the fresh readable stream with explicit duplex: 'half' options, assigns it back to c.req.raw, and proceeds down the middleware chain via next().

  duplex: 'half',
}
c.req.raw = new Request(c.req.raw, requestInit as RequestInit)
 

Sources: src/middleware/body-limit/index.ts:105-108

Payload Overflow Detection and Error Handling

Error Trigger Conditions and HTTPException 413

When the body limit middleware detects that a payload exceeds the configured maxSize, it immediately halts further stream consumption or execution and invokes the error handler. For requests governed by a trustworthy content-length header, this triggers as soon as content-length > maxSize. For chunked or un-sized streams, the check occurs incrementally during stream iteration inside the reader loop whenever the running size counter surpasses maxSize.

if (hasContentLength && !hasTransferEncoding) {
  // Only Content-Length present - we can trust it
  const contentLength = parseInt(c.req.raw.headers.get('content-length') || '0', 10)
  return contentLength > maxSize ? onError(c) : next()
}

Sources: src/middleware/body-limit/index.ts:70-74

  size += value.length
  if (size > maxSize) {
    return onError(c)
  }
  chunks.push(value)
}

Sources: src/middleware/body-limit/index.ts:89-94

Caution

When consuming chunked streams, the reader halts the moment size > maxSize is met mid-stream, preventing excess bytes from accumulating into memory while terminating the active request stream early. Sources: src/middleware/body-limit/index.ts:89-94

Custom onError Callbacks and Default Response Termination

Developers can supply a custom onError callback function within BodyLimitOptions to customize how payload overflows are formatted and returned to clients. If options.onError is omitted, the middleware falls back to a default handler that instantiates a web standard Response using the constant ERROR_MESSAGE string ('Payload Too Large') with status code 413, and immediately throws an HTTPException(413, { res }).

const ERROR_MESSAGE = 'Payload Too Large'
 
type OnError = (c: Context) => Response | Promise<Response>
type BodyLimitOptions = {
  maxSize: number
  onError?: OnError
}

Sources: src/middleware/body-limit/index.ts:10-16

export const bodyLimit = (options: BodyLimitOptions): MiddlewareHandler => {
  const onError: OnError =
    options.onError ||
    (() => {
      const res = new Response(ERROR_MESSAGE, {
        status: 413,
      })
      throw new HTTPException(413, { res })
    })

Sources: src/middleware/body-limit/index.ts:50-58

  return contentLength > maxSize ? onError(c) : next()
}

Sources: src/middleware/body-limit/index.ts:73-74

if (size > maxSize) {
  return onError(c)
}

Sources: src/middleware/body-limit/index.ts:90-92

Note

The OnError type signature specifies that custom handlers accept the active Context (c) and return either a Response or a Promise<Response>, allowing seamless integration with context response helpers like c.text(). Sources: src/middleware/body-limit/index.ts:12-16

Security Configuration and Defense in Depth

Proxy Adapters and Edge Request Initialization

When deploying security middleware like CSRF protection and body limits behind serverless runtimes or edge proxies, incoming event structures must be accurately transformed into web standard Request instances. Adapters such as aws-lambda and lambda-edge process incoming events by parsing HTTP headers, methods, query strings, and body payloads before dispatching them into Hono's app.fetch().

const createRequest = (event: CloudFrontEdgeEvent): Request => {
  const queryString = event.Records[0].cf.request.querystring
  const host =
    event.Records[0].cf.request.headers?.host?.[0]?.value ||
    event.Records[0].cf.config.distributionDomainName
  const urlPath = `https://${host}${event.Records[0].cf.request.uri}`
  const url = queryString ? `${urlPath}?${queryString}` : urlPath
 
  const headers = new Headers()
  Object.entries(event.Records[0].cf.request.headers).forEach(([k, v]) => {
    v.forEach((header) => headers.append(k, header.value))
  })
 
  const requestBody = event.Records[0].cf.request.body
  const method = event.Records[0].cf.request.method
  const rawBody = createBody(method, requestBody)
 
  let body: string | Uint8Array<ArrayBuffer> | undefined = rawBody
  if (rawBody !== undefined) {
    const bytes =
      typeof rawBody === 'string'
        ? (new TextEncoder().encode(rawBody) as Uint8Array<ArrayBuffer>)
        : rawBody
    body = bytes
    headers.set('content-length', bytes.length.toString())
  }
 
  return new Request(url, {
    headers,
    method,
    body,
  })
}

Sources: src/adapter/lambda-edge/handler.ts:152-184

Important

Edge adapters like Lambda@Edge manually assign content-length headers when request body bytes are present, ensuring downstream size validation and body limit middleware can inspect exact payload dimensions before handlers execute. Sources: src/adapter/lambda-edge/handler.ts:169-177

Combining CSRF Protection and Body Constraints

Layering both csrf() and bodyLimit() creates a multi-layered defense against malicious web requests. The CSRF middleware evaluates mutating HTTP methods against form element content-types (application/x-www-form-urlencoded, multipart/form-data, text/plain), requiring valid sec-fetch-site or origin validation headers before allowing execution to proceed.

return async function csrf(c, next) {
  if (
    !isSafeMethodRe.test(c.req.method) &&
    isRequestedByFormElementRe.test(c.req.header('content-type') || 'text/plain') &&
    !(await isAllowedSecFetchSite(c.req.header('sec-fetch-site'), c)) &&
    !(await isAllowedOrigin(c.req.header('origin'), c))
  ) {
    const res = new Response('Forbidden', { status: 403 })
    throw new HTTPException(403, { res })
  }
 
  await next()
}

Sources: src/middleware/csrf/index.ts:138-150

Once origin and site context validation pass, the body limit middleware intercepts incoming streams, enforcing strict maxSize thresholds to reject oversized payloads with an HTTPException(413) before memory exhaustion occurs.

return async function bodyLimit(c, next) {
  if (!c.req.raw.body) {
    // maybe GET or HEAD request
    return next()
  }
 
  const hasTransferEncoding = c.req.raw.headers.has('transfer-encoding')
  const hasContentLength = c.req.raw.headers.has('content-length')
 
  if (hasContentLength && !hasTransferEncoding) {
    // Only Content-Length present - we can trust it
    const contentLength = parseInt(c.req.raw.headers.get('content-length') || '0', 10)
    return contentLength > maxSize ? onError(c) : next()
  }

Sources: src/middleware/body-limit/index.ts:61-74

Was this page helpful?