Crystal comes with a module HTTP::Handler, which is used as middleware for your HTTP Server stack.
These are classes you create that include the HTTP::Handler
module to process incoming requests and return responses.
Lucky comes with some built-in handlers that you can see in your src/app_server.cr
file under the middleware
method.
The order of this middleware stack is the order Lucky will send a web request with the first in the stack getting the initial request, and the last finishing out the stack.
By default when you generate a full application, your middleware stack will look like this:
def middleware : Array(HTTP::Handler)
[
Lucky::RequestIdHandler.new,
Lucky::ForceSSLHandler.new,
Lucky::HttpMethodOverrideHandler.new,
Lucky::LogHandler.new,
Lucky::ErrorHandler.new(action: Errors::Show),
Lucky::RemoteIpHandler.new,
Lucky::RouteHandler.new,
Lucky::StaticCompressionHandler.new("./public", file_ext: "gz", content_encoding: "gzip"),
Lucky::StaticFileHandler.new("./public", fallthrough: false, directory_listing: false),
Lucky::RouteNotFoundHandler.new,
] of HTTP::Handler
end
The request
object will start in the Lucky::RequestIdHandler
, do some processing, then move on to the Lucky::ForceSSLHandler
, and so on.
In the Lucky::RouteHandler
it will look for a Lucky::Action
that matches the request, run any pipes, then run the action. If no action is found,
we check to see if there’s a static file in Lucky::StaticFileHandler
. Finally, if no route or file matches the request, we run the Lucky::RouteNotFoundHandler
.
The remote IP, or remote address, is the IP address of the user on your site. Getting this value can be helpful for things like banning spammers, tracking password sharing with user accounts, geo locating based on IP, and many other options.
This handler sets the HTTP::Request#remote_ip
value to the value of the first IP in the X-Forwarded-For
header, or fallback to the default remote_address
.
To use a different header other than X-Forwarded-For
, for example, with a Load Balancer you may use X-Client-IP
, you can update the ip_header_name
setting for Lucky::RemoteIpHandler
.
# config/server.cr
Lucky::RemoteIpHandler.configure do |settings|
settings.ip_header_name = "X-Client-IP"
end
The remote_ip
method can be accessed in your actions from context.request.remote_ip
.
# src/actions/home/index.cr
class Home::Index < BrowserAction
get "/" do
# This method is an extension provided by Lucky
ip_address = context.request.remote_ip || "0.0.0.0"
html IndexPage, ip_address: ip_address
end
end
There may be cases where you want a handler to be included only when certain conditions are met.
For example, let’s say that we want to serve files from our /tmp
directory in development and test environments, but not in production.
This can be accomplished with a ternary statement and slight restructure of the middleware
method in app_server.cr
:
def middleware : Array(HTTP::Handler)
[
Lucky::RequestIdHandler.new,
Lucky::ForceSSLHandler.new,
Lucky::HttpMethodOverrideHandler.new,
Lucky::LogHandler.new,
Lucky::ErrorHandler.new(action: Errors::Show),
Lucky::RemoteIpHandler.new,
Lucky::RouteHandler.new,
Lucky::StaticCompressionHandler.new("./public", file_ext: "br", content_encoding: "br"),
Lucky::StaticCompressionHandler.new("./public", file_ext: "gz", content_encoding: "gzip"),
Lucky::StaticFileHandler.new("./public", fallthrough: false, directory_listing: false),
# Here is our new handler, which is `nil` when we're in production,
# and a `StaticFileHandler` when we're in other environments.
#
# To make sure that `middleware` still returns only `HTTP::Handler`s,
# we `select(HTTP::Handler)` when returning from this method.
LuckyEnv.production? ? nil : Lucky::StaticFileHandler.new("./tmp", fallthrough: false, directory_listing: false),
Lucky::RouteNotFoundHandler.new,
].select(HTTP::Handler)
end
Your application may have special requirements like routing legacy URLs, sending bug reporting, CORS, or even doing HTTP Basic auth while your app is in beta. Whatever your use case, creating a custom handler is really easy!
First we start off by creating a new directory where we can place all of our custom handlers. Create a src/handlers/
directory and be sure to require it src/app.cr
.
Next, your handler needs 3 things:
HTTP::Handler
call
method with an argument of context : HTTP::Server::Context
call_next(context)
to go to the next handler in the stack# src/handlers/legacy_redirect_handler.cr
class LegacyRedirectHandler
include HTTP::Handler
LEGACY_ROUTES = {"/old-path" => "/new-path"}
def call(context : HTTP::Server::Context)
if new_path = LEGACY_ROUTES[context.request.path]?
context.response.status_code = 301
context.response.headers["Location"] = new_path
else
# Go to the next handler in the stack
call_next(context)
end
end
end
Lastly, we need to make sure our new custom handler is in our stack. Open up src/app_server.cr
and place a new instance in the stack!
# src/app_server.cr
#...
class App < Lucky::BaseAppServer
def middleware
[
Lucky::HttpMethodOverrideHandler.new,
Lucky::LogHandler.new,
LegacyRedirectHandler.new, # Add this line
Lucky::ErrorHandler.new(action: Errors::Show),
Lucky::RouteHandler.new,
Lucky::StaticFileHandler.new("./public", false),
Lucky::RouteNotFoundHandler.new,
]
end
end
Note: The order of custom handlers will be completely up to you, but keep in mind the order Lucky placed the built-in handlers.