Kemal Middleware & Handlers
This skill provides expert guidance on creating and using custom middleware in Kemal, leveraging modern use registration and filter macros, strictly following patterns from kemal-by-example/webhook-inbox.
Version Notes
use registration (global and path-scoped): since Kemal 1.10.
only / exclude with a single method and exact paths: all supported versions.
only / exclude with "*" methods and "/*" path globs: since Kemal 1.13.0 — do not use the glob syntax on 1.12.0 or earlier, where it matches every path (see the warning below).
HEAD fallback route-scoped dispatch: since Kemal 1.13.0. On Kemal 1.12.0 and earlier, HEAD requests falling back to GET routes bypassed GET-scoped filters (before_get) and GET-scoped only / exclude middleware because dispatch checked the literal request method (HEAD) rather than the serving route method, creating an authentication bypass risk (GHSA-jf9q-62h3-924j). Kemal 1.13.0+ evaluates both the request method and the serving route's effective method.
Kemal::Router filters registered with trailing /* match path subtrees: since Kemal 1.13.0 (on 1.12.0 and earlier, trailing * was treated as a literal path segment and silently failed to register/match routes).
Core Mandates
Middleware Registration (use Keyword in Kemal 1.10+): Prefer use for clean global or path-scoped middleware registration:
# Path-scoped middleware (applies only to /api routes)
use "/api", [CORSHandler.new, AuthHandler.new]
# Global middleware (applies to all routes)
use CustomHandler.new
Configuration-based Registration: Alternatively, use Kemal.config.add_handler(CustomHandler.new). (The bare top-level add_handler is deprecated in favor of use and Kemal.config.add_handler).
Handlers: Create custom middleware by inheriting from Kemal::Handler:
class MyHandler < Kemal::Handler
def call(context)
# Execution prior to next handler
call_next(context)
# Execution after next handler returns
end
end
Selective Filtering in Handlers: Use only or exclude macros to restrict execution within the handler class. On Kemal 1.12.0 they support a single HTTP method and exact paths only:
class MyHandler < Kemal::Handler
only %w[/admin], "POST"
def call(context)
return call_next(context) unless only_match?(context)
# Custom logic
call_next(context)
end
end
On Kemal 1.13.0+, "*" matches all methods and paths ending in "/*" match a prefix:
# Kemal 1.13.0+:
class MyHandler < Kemal::Handler
# Matches all HTTP methods on /admin and sub-paths
only %w[/admin/*], "*"
# ...
end
WARNING: Do not use "*" or "/*" globs on Kemal 1.12.0. The 1.12.0 route matcher treats * as a radix glob, so a rule like only %w[/admin/*], "*" matches every path on every method — an auth handler scoped this way locks the whole site. For middleware that should cover an entire path subtree on any version, prefer use "/admin", MyHandler.new instead.
Legacy Registration (add_handler): add_handler MyHandler.new remains supported for backward compatibility.
Specialized Handlers (HMAC): For HMAC signatures, require "kemal-hmac" and inherit from Kemal::Hmac::Handler:
class InboxHmacHandler < Kemal::Hmac::Handler
only %w[/hooks/inbox], "POST"
def call(context)
return call_next(context) unless only_match?(context)
super
end
end
Patterns from Source Code
HMAC Handler (webhook-inbox/src/middleware/inbox_hmac_handler.cr)
require "kemal-hmac"
# Protects only `POST /hooks/inbox` with kemal-hmac.
class WebhookInbox::InboxHmacHandler < Kemal::Hmac::Handler
only %w[/hooks/inbox], "POST"
def call(context)
return call_next(context) unless only_match?(context)
super
end
end
Main Application Setup (webhook-inbox/src/webhook_inbox.cr)
require "kemal"
require "kemal-hmac"
require "db"
require "sqlite3"
require "./config/app"
require "./config/database"
require "./config/schema"
require "./helpers/headers_json"
require "./middleware/inbox_hmac_handler"
require "./models/webhook_event"
require "./routes/home"
require "./routes/inbox"
require "./routes/events"
# Configure HMAC handler with client/secret mapping and use keyword
client = WebhookInbox.webhook_client
use WebhookInbox::InboxHmacHandler.new({client => [WebhookInbox.webhook_secret]})
WebhookInbox::Schema.setup
Kemal.run
Best Practices
- Use Keyword: Prefer
use "/path", Handler.new for path-scoped middleware instead of checking path conditions inside route blocks.
- Minimalist Design: Keep handlers focused on a single responsibility (e.g., CORS, logging, authorization).
- Execution Order: Order middleware intentionally (e.g., authentication handlers must run before route handlers that depend on auth context).
- HMAC Setup: For
Kemal::Hmac::Handler, pass key/secret maps as { "client_id" => ["secret_key"] }.
When to Use
- When implementing cross-cutting concerns (authentication, CORS, security headers, logging).
- When protecting specific API routes using HMAC or token authorization.
- When organizing global or path-scoped request processing stacks.
1---2name: kemal-middleware3description: Creating and using custom middleware in Kemal using modern `use` keyword and handler classes.4license: MIT5---67# Kemal Middleware & Handlers89This skill provides expert guidance on creating and using custom middleware in Kemal, leveraging modern `use` registration and filter macros, strictly following patterns from [`kemal-by-example/webhook-inbox`](https://github.com/sdogruyol/kemal-by-example/tree/master/webhook-inbox).1011## Version Notes1213- `use` registration (global and path-scoped): since Kemal 1.10.14- `only` / `exclude` with a single method and exact paths: all supported versions.15- `only` / `exclude` with `"*"` methods and `"/*"` path globs: since Kemal 1.13.0 — do **not** use the glob syntax on 1.12.0 or earlier, where it matches every path (see the warning below).16- `HEAD` fallback route-scoped dispatch: since Kemal 1.13.0. On Kemal 1.12.0 and earlier, `HEAD` requests falling back to `GET` routes bypassed `GET`-scoped filters (`before_get`) and `GET`-scoped `only` / `exclude` middleware because dispatch checked the literal request method (`HEAD`) rather than the serving route method, creating an authentication bypass risk ([GHSA-jf9q-62h3-924j](https://github.com/kemalcr/kemal/security/advisories/GHSA-jf9q-62h3-924j)). Kemal 1.13.0+ evaluates both the request method and the serving route's effective method.17- `Kemal::Router` filters registered with trailing `/*` match path subtrees: since Kemal 1.13.0 (on 1.12.0 and earlier, trailing `*` was treated as a literal path segment and silently failed to register/match routes).1819## Core Mandates2021- **Middleware Registration (`use` Keyword in Kemal 1.10+):** Prefer `use` for clean global or path-scoped middleware registration:2223 ```crystal24 # Path-scoped middleware (applies only to /api routes)25 use "/api", [CORSHandler.new, AuthHandler.new]2627 # Global middleware (applies to all routes)28 use CustomHandler.new29 ```3031- **Configuration-based Registration:** Alternatively, use `Kemal.config.add_handler(CustomHandler.new)`. (The bare top-level `add_handler` is deprecated in favor of `use` and `Kemal.config.add_handler`).3233- **Handlers:** Create custom middleware by inheriting from `Kemal::Handler`:3435 ```crystal36 class MyHandler < Kemal::Handler37 def call(context)38 # Execution prior to next handler39 call_next(context)40 # Execution after next handler returns41 end42 end43 ```4445- **Selective Filtering in Handlers:** Use `only` or `exclude` macros to restrict execution within the handler class. On Kemal 1.12.0 they support a single HTTP method and exact paths only:4647 ```crystal48 class MyHandler < Kemal::Handler49 only %w[/admin], "POST"5051 def call(context)52 return call_next(context) unless only_match?(context)53 # Custom logic54 call_next(context)55 end56 end57 ```5859 On Kemal 1.13.0+, `"*"` matches all methods and paths ending in `"/*"` match a prefix:6061 ```crystal62 # Kemal 1.13.0+:63 class MyHandler < Kemal::Handler64 # Matches all HTTP methods on /admin and sub-paths65 only %w[/admin/*], "*"66 # ...67 end68 ```6970 **WARNING:** Do not use `"*"` or `"/*"` globs on Kemal 1.12.0. The 1.12.0 route matcher treats `*` as a radix glob, so a rule like `only %w[/admin/*], "*"` matches **every path on every method** — an auth handler scoped this way locks the whole site. For middleware that should cover an entire path subtree on any version, prefer `use "/admin", MyHandler.new` instead.7172- **Legacy Registration (`add_handler`):** `add_handler MyHandler.new` remains supported for backward compatibility.7374- **Specialized Handlers (HMAC):** For HMAC signatures, `require "kemal-hmac"` and inherit from `Kemal::Hmac::Handler`:7576 ```crystal77 class InboxHmacHandler < Kemal::Hmac::Handler78 only %w[/hooks/inbox], "POST"7980 def call(context)81 return call_next(context) unless only_match?(context)82 super83 end84 end85 ```8687## Patterns from Source Code8889### HMAC Handler (webhook-inbox/src/middleware/inbox_hmac_handler.cr)9091```crystal92require "kemal-hmac"9394# Protects only `POST /hooks/inbox` with kemal-hmac.95class WebhookInbox::InboxHmacHandler < Kemal::Hmac::Handler96 only %w[/hooks/inbox], "POST"9798 def call(context)99 return call_next(context) unless only_match?(context)100 super101 end102end103```104105### Main Application Setup (webhook-inbox/src/webhook_inbox.cr)106107```crystal108require "kemal"109require "kemal-hmac"110require "db"111require "sqlite3"112113require "./config/app"114require "./config/database"115require "./config/schema"116require "./helpers/headers_json"117require "./middleware/inbox_hmac_handler"118require "./models/webhook_event"119require "./routes/home"120require "./routes/inbox"121require "./routes/events"122123# Configure HMAC handler with client/secret mapping and use keyword124client = WebhookInbox.webhook_client125use WebhookInbox::InboxHmacHandler.new({client => [WebhookInbox.webhook_secret]})126127WebhookInbox::Schema.setup128Kemal.run129```130131## Best Practices132133- **Use Keyword:** Prefer `use "/path", Handler.new` for path-scoped middleware instead of checking path conditions inside route blocks.134- **Minimalist Design:** Keep handlers focused on a single responsibility (e.g., CORS, logging, authorization).135- **Execution Order:** Order middleware intentionally (e.g., authentication handlers must run before route handlers that depend on auth context).136- **HMAC Setup:** For `Kemal::Hmac::Handler`, pass key/secret maps as `{ "client_id" => ["secret_key"] }`.137138## When to Use139140- When implementing cross-cutting concerns (authentication, CORS, security headers, logging).141- When protecting specific API routes using HMAC or token authorization.142- When organizing global or path-scoped request processing stacks.