Handlers

Handlers are the building blocks for serving content in Bonito. Any type that implements the apply_handler interface can be used with route! to respond to HTTP requests. This makes Bonito's routing system highly extensible - you can wrap existing handlers to add functionality like authentication, rate limiting, or logging.

Understanding Handlers

A handler is any type that implements the apply_handler method:

function Bonito.HTTPServer.apply_handler(handler::YourHandlerType, context)
    request = context.request
    # ... process the request and return HTTP.Response
    return HTTP.Response(200, "Hello World")
end

The context parameter provides access to:

  • context.request - The HTTP request object
  • context.application - The server instance
  • context.routes - The routes table
  • context.match - The pattern match result (string or regex match)

Bonito's built-in App type is itself a handler, which is why you can use route!(server, "/" => app) directly.

Creating Custom Handlers

Here's a simple example of a custom handler that logs requests:

using Bonito, Dates

struct LoggingHandler{T}
    handler::T
    log_file::String
end

function Bonito.HTTPServer.apply_handler(logger::LoggingHandler, context)
    request = context.request
    # Log the request
    open(logger.log_file, "a") do io
        println(io, "$(now()): $(request.method) $(request.target)")
    end
    # Delegate to wrapped handler
    return Bonito.HTTPServer.apply_handler(logger.handler, context)
end

# Usage
app = App(DOM.div("Hello World"))
logged_app = LoggingHandler(app, "requests.log")
# route!(server, "/" => logged_app)
Main.LoggingHandler{App}(App(Bonito.var"#14#20"{Hyperscript.Node{Hyperscript.HTMLSVG}}(<div>Hello World</div>), Base.RefValue{Union{Nothing, Session}}(nothing), "Bonito App", false), "requests.log")

This pattern of wrapping handlers is similar to middleware in other web frameworks. You can chain multiple wrappers together:

protected_logged_app = ProtectedRoute(logged_app, password_store)

Built-in Handlers

Bonito provides several ready-to-use handlers:

FolderServer

Bonito.FolderServerType
FolderServer(folder::String)

A simple file server that serves files from the specified folder. Example with a static site generated with Bonito:

# Build a static site
app = App(()-> DOM.div("Hello World"));
routes = Routes(
    "/" => app
    # Add more routes as needed
)
export_static("build", routes)
# Serve the static site from the build folder
server = Bonito.Server("0.0.0.0", 8982)
route!(server, r".*" => FolderServer("build"))
source

ProtectedRoute

Bonito.ProtectedRouteType
ProtectedRoute{T, PS <: AbstractPasswordStore}

A wrapper that adds HTTP Basic Authentication to any type that implements apply_handler. Can wrap Bonito.App, FolderServer, or any other handler type.

Security Notes

  • REQUIRES HTTPS: HTTP Basic Auth sends credentials with every request. Without HTTPS, credentials are transmitted in plaintext (Base64 is NOT encryption).
  • Experimental: This is a simple implementation for basic use cases with no security guarantees.
  • Rate Limiting: Built-in protection against brute force attacks (max 5 attempts per IP per minute).
  • PBKDF2 Password Hashing: Uses PBKDF2-HMAC-SHA256 with 10,000 iterations and random salt.
  • Extensible: Use custom AbstractPasswordStore implementations for multi-user or database-backed auth.

Fields

  • handler::T: The wrapped handler (e.g., Bonito.App, FolderServer, etc.)
  • password_store::PS: Password store implementing AbstractPasswordStore interface
  • realm::String: Authentication realm name (default: "Protected Area")
  • failed_attempts::Dict{String, Vector{Float64}}: IP -> timestamps of failed attempts
  • max_attempts::Int: Maximum failed attempts allowed (default: 5)
  • lockout_window::Float64: Time window in seconds for rate limiting (default: 60.0)
  • auth_required_handler: Handler for 401 responses (default: built-in HTML page)
  • rate_limited_handler: Handler for 429 responses (default: built-in HTML page)

Example

# Create app
app = App() do
    return DOM.h1("Hello World")
end

# Create password store
store = SingleUser("admin", "secret123")

# Create protected route
protected_app = ProtectedRoute(app, store)

# With custom error pages
auth_page = App() do
    return DOM.div(
        DOM.h1("Authentication Required"),
        DOM.p("Please log in to access this resource")
    )
end

rate_limit_page = App() do
    return DOM.div(
        DOM.h1("Too Many Attempts"),
        DOM.p("Please wait before trying again")
    )
end

protected_app = ProtectedRoute(app, store;
                               auth_required_handler=auth_page,
                               rate_limited_handler=rate_limit_page)

# Add to server (MUST use HTTPS in production!)
server = Bonito.Server("0.0.0.0", 8443)  # Use SSL/TLS
route!(server, "/" => protected_app)
source

Password Storage

Bonito.AbstractPasswordStoreType
AbstractPasswordStore

Abstract interface for password storage and authentication.

Required Methods

  • get_user(store, username::String)::Union{User, Nothing}: Retrieve user by username

Provided Methods

  • authenticate(store, username::String, password::String)::Bool: Verify credentials (implemented via get_user)

Implementation Guide

Subtypes only need to implement get_user(). The authenticate() method will automatically:

  1. Call get_user() to retrieve the user
  2. Verify the password using the User's authenticate method
  3. Return true if credentials match, false otherwise

For custom authentication logic (e.g., LDAP), you can override authenticate().

source

SingleUser

Bonito.SingleUserType
SingleUser <: AbstractPasswordStore

Simple password store for single-user authentication.

Fields

  • user::User: The single user
source

Custom Password Stores

To implement a custom password store, subtype AbstractPasswordStore and implement get_user:

using Bonito
using Bonito: User, AbstractPasswordStore

# Example: Database-backed password store
struct DatabasePasswordStore <: AbstractPasswordStore
    db_connection::DatabaseConnection
end

function Bonito.get_user(store::DatabasePasswordStore, username::String)
    # Query database for user
    result = query(store.db_connection,
                   "SELECT * FROM users WHERE username = ?", username)

    if isempty(result)
        return nothing
    end

    # Reconstruct User from database fields
    row = result[1]
    return User(
        row.username,
        row.password_hash,  # Already hashed in database
        row.salt,
        row.iterations,
        Dict("role" => row.role)  # Optional metadata
    )
end

# The authenticate() method is automatically provided

Interface Requirements:

  • Implement get_user(store, username) which returns a User or nothing
  • The authenticate(store, username, password) method is automatically implemented
  • Only override authenticate() if you need custom authentication logic

Creating Users Manually:

Bonito.UserType
User

Represents a user with authentication credentials.

Fields

  • username::String: The username
  • password_hash::Vector{UInt8}: PBKDF2 derived key
  • salt::Vector{UInt8}: Random salt for password hashing
  • iterations::Int: PBKDF2 iteration count
  • metadata::Dict{String, Any}: Optional user metadata (roles, permissions, etc.)
source

Combining Handlers

Handlers can be composed to create complex behaviors:

# Multiple layers of protection
admin_app = App(DOM.h1("Super Secret Admin"))
logged_app = LoggingHandler(admin_app, "admin.log")
protected_app = ProtectedRoute(logged_app, admin_store)

# Different protection for different routes
public_files = FolderServer("public")
protected_files = ProtectedRoute(FolderServer("private"), file_store)

server = Bonito.Server("0.0.0.0", 8080)
# Use regex route to forward all file requests to FolderServer
route!(server, r"/public/.*" => public_files)
route!(server, "/private/.*" => protected_files)
route!(server, "/admin" => protected_app)

Advanced: Handler Context

The context object passed to handlers is a NamedTuple with the following fields:

context = (
    request = HTTP.Request,      # The HTTP request
    application = Server,         # The server instance
    routes = Routes,              # The routes table
    match = Union{String, RegexMatch}  # The pattern match result
)

You can access request details like:

function Bonito.HTTPServer.apply_handler(handler::MyHandler, context)
    request = context.request

    # Request method (GET, POST, etc.)
    method = request.method

    # Request path
    path = request.target

    # Headers
    user_agent = HTTP.header(request, "User-Agent", "")

    # Query parameters
    uri = URIs.URI(request.target)
    query = URIs.queryparams(uri.query)

    # Access server info
    server = context.application
    server_url = online_url(server, "/")

    # Access regex capture groups if using regex pattern
    if context.match isa RegexMatch
        captured = context.match.captures
    end

    # ...
end