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 objectcontext.application
- The server instancecontext.routes
- The routes tablecontext.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.FolderServer
— TypeFolderServer(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"))
ProtectedRoute
Bonito.ProtectedRoute
— TypeProtectedRoute{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 interfacerealm::String
: Authentication realm name (default: "Protected Area")failed_attempts::Dict{String, Vector{Float64}}
: IP -> timestamps of failed attemptsmax_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)
Password Storage
Bonito.AbstractPasswordStore
— TypeAbstractPasswordStore
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:
- Call
get_user()
to retrieve the user - Verify the password using the User's authenticate method
- Return true if credentials match, false otherwise
For custom authentication logic (e.g., LDAP), you can override authenticate()
.
SingleUser
Bonito.SingleUser
— TypeSingleUser <: AbstractPasswordStore
Simple password store for single-user authentication.
Fields
user::User
: The single user
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 aUser
ornothing
- The
authenticate(store, username, password)
method is automatically implemented - Only override
authenticate()
if you need custom authentication logic
Creating Users Manually:
Bonito.User
— TypeUser
Represents a user with authentication credentials.
Fields
username::String
: The usernamepassword_hash::Vector{UInt8}
: PBKDF2 derived keysalt::Vector{UInt8}
: Random salt for password hashingiterations::Int
: PBKDF2 iteration countmetadata::Dict{String, Any}
: Optional user metadata (roles, permissions, etc.)
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