Introduction to handlers
Handlers are classes whose responsibility is to process web requests and to return responses. They implement the necessary logic allowing to return this response, which can involve processing form data through the use of schemas for example, retrieving model records from the database, etc. They can return responses corresponding to HTML pages, JSON objects, redirects, ...
Writing handlers
At their core, handlers are subclasses of the Marten::Handler
class. These classes are usually defined under a handlers
folder, at the root of a Marten project or application. Here is an example of a very simple handler:
class SimpleHandler < Marten::Handler
def dispatch
respond "Hello World!"
end
end
The above handler returns a 200 OK
response containing a short text, regardless of the incoming HTTP request method.
Handlers are initialized from a Marten::HTTP::Request
object and an optional set of routing parameters. Their inner logic is executed when calling the #dispatch
method, which must return a Marten::HTTP::Response
object.
When the #dispatch
method is explicitly overridden, it is responsible for applying different logics in order to handle the various incoming HTTP request methods. For example, a handler might display an HTML page containing a form when handling a GET
request, and it might process possible form data when handling a POST
request:
class FormHandler < Marten::Handler
def dispatch
if request.method == 'POST'
# process form data
else
# return HTML page
end
end
end
It should be noted that this "dispatching" logic based on the incoming request method does not have to live inside an overridden #dispatch
method. By default, each handler provides methods whose names match HTTP method verbs. This allows writing the logic allowing to process GET
requests by overriding the #get
method for example, or to process POST
requests by overriding the #post
method:
class FormHandler < Marten::Handler
def get
# return HTML page
end
def post
# process form data
end
end
If a handler's logic is defined like in the above example, trying to access such handler via another HTTP verb (eg. DELETE
) will automatically result in a "Not allowed" response (405).
The request
and response
objects
As mentioned previously, a handler is always initialized from an incoming HTTP request object (instance of Marten::HTTP::Request
) and is required to return an HTTP response object (instance of Marten::HTTP::Response
) as part of its #dispatch
method.
The request
object gives access to a set of useful information and attributes associated with the incoming request. Things like the HTTP request verb, headers, or query parameters can be accessed through this object. The most common methods that you can use are listed below:
Method | Description |
---|---|
#body | Returns the raw body of the request as a string. |
#cookies | Returns a hash-like object (instance of Marten::HTTP::Cookies ) containing the cookies associated with the request. |
#data | Returns a hash-like object (instance of Marten::HTTP::Params::Data ) containing the request data. |
#flash | Returns a hash-like object (instance of Marten::HTTP::FlashStore ) containing the flash messages available to the current request. |
#headers | Returns a hash-like object (instance of Marten::HTTP::Headers ) containing the headers embedded in the request. |
#host | Returns the host associated with the considered request. |
#method | Returns the considered HTTP request method (GET , POST , PUT , etc). |
#query_params | Returns a hash-like object (instance of Marten::HTTP::Params::Query ) containing the HTTP GET parameters embedded in the request. |
#session | Returns a hash-like object (instance of Marten::HTTP::Session::Store::Base ) corresponding to the session store for the current request. |
The response
object corresponds to the HTTP response that is returned to the client. Response objects can be created by initializing the Marten::HTTP::Response
class directly (or one of its subclasses) or by using response helper methods. Once initialized, these objects can be mutated to further configure what is sent back to the browser. The most common methods that you can use in this regard are listed below:
Method | Description |
---|---|
#content | Returns the content of the response as a string. |
#content_type | Returns the content type of the response as a string. |
#cookies | Returns a hash-like object (instance of Marten::HTTP::Cookies ) containing the cookies that will be sent with the response. |
#headers | Returns a hash-like object (instance of Marten::HTTP::Headers ) containg the headers that will be used for the response. |
#status | Returns the status of the response (eg. 200 or 404). |
Parameters
Handlers are mapped to URLs through a routing configuration. Some routes require parameters that are used by the handler to retrieve objects or perform any arbirtary logic. These parameters can be accessed by using the #params
method, which returns a hash of all the parameters that were used to initialize the considered handler.
For example such parameters can be used to retrieve a specific model instance:
class FormHandler < Marten::Handler
def get
if (record = MyModel.get(id: params["id"]))
respond "Record found: #{record}"
else
respond "Record not found!", status: 404
end
end
end
Response helper methods
Technically, it is possible to forge HTTP responses by instantiating the Marten::HTTP::Response
class directly (or one of its subclasses such as Marten::HTTP::Response::Found
for example). That being said, Marten provides a set of helper methods that can be used to conveniently forge responses for various use cases:
respond
You already saw #respond
in action in the first example. Basically, #respond
allows forging an HTTP response by specifying a content, a content type, and a status code:
respond("Response content", content_type: "text/html", status: 200)
Unless specified, the content_type
is set to text/html
and the status
is set to 200
.
render
render
allows returning an HTTP response whose content is generated by rendering a specific template. The template can be rendered by specifying a context hash or named tuple. For example:
render("path/to/template.html", context: { foo: "bar" }, content_type: "text/html", status: 200)
Unless specified, the content_type
is set to text/html
and the status
is set to 200
.
redirect
#redirect
allows forging a redirect HTTP response. It requires a url
and accepts an optional permanent
argument in order to define whether a permanent redirect is returned (301 Moved Permanently) or a temporary one (302 Found):
redirect("https://example.com", permanent: true)
Unless explicitly specified, permanent
will automatically be set to false
.
#head
#head
allows constructing a response containing headers but without actual content. The method accepts a status code only:
head(404)
json
json
allows forging an HTTP response with the application/json
content type. It can be used with a raw JSON string, or any serializable object:
json({ foo: "bar" }, status: 200)
Unless specified, the status
is set to 200
.
Callbacks
Callbacks let you define logics that are triggered before or after a handler's dispatch flow. This allows you to easily intercept the incoming request and completely bypass the execution of the regular #dispatch
method for example. Two callbacks are supported: before_dispatch
and after_dispatch
.
before_dispatch
before_dispatch
callbacks are executed before a request is processed as part of the handler's #dispatch
method. For example, this capability can be leveraged to inspect the incoming request and verify that a user is logged in:
class MyHandler < Marten::Handler
before_dispatch :require_authenticated_user
def get
respond "Hello, authenticated user!"
end
private def require_authenticated_user
redirect(login_url) unless user_authenticated?(request)
end
end
When one of the defined before_dispatch
callbacks returns a Marten::HTTP::Response
object, this response is always used instead of calling the handler's #dispatch
method (the latest is thus completely bypassed).
after_dispatch
after_dispatch
callbacks are executed after a request is processed as part of the handler's #dispatch
method. For example, such a callback can be leveraged to automatically add headers or cookies to the returned response.
class MyHandler < Marten::Handler
after_dispatch :add_required_header
def get
respond "Hello, authenticated user!"
end
private def add_required_header : Nil
response!.headers["X-Foo"] = "Bar"
end
end
Similarly to #before_dispatch
callbacks, #after_dispatch
callbacks can return a brand new Marten::HTTP::Response
object. When this is the case, this response is always used instead of the one that was returned by the handler's #dispatch
method.
Returning errors
It is easy to forge any error response by leveraging the #respond
or #head
helpers that were mentioned previously. Using these helpers, it is possible to forge HTTP responses that are associated with specific error status codes and specific contents. For example:
class MyHandler < Marten::Handler
def get
respond "Content not found", status: 404
end
end
It should be noted that Marten also support a couple of exceptions that can be raised to automatically trigger default error handlers. For example Marten::HTTP::Errors::NotFound
can be raised from any handler to force a 404 Not Found response to be returned. Default error handlers can be returned automatically by the framework in many situations (eg. a record is not found, or an unhandled exception is raised); you can learn more about them in Error handlers.
Mapping handlers to URLs
Handlers define the logic allowing to handle incoming HTTP requests and return corresponding HTTP responses. In order to define which handler gets called for a specific URL (and what are the expected URL parameters), handlers need to be associated with a specific route. This configuration usually takes place in the config/routes.rb
configuration file, where you can define "paths" and associate them to your handler classes:
Marten.routes.draw do
path "/", HomeHandler, name: "home"
path "/articles", ArticlesHandler, name: "articles"
path "/articles/<pk:int>", ArticleDetailHandler, name: "article_detail"
end
Please refer to Routing for more information regarding routes configuration.
Using cookies
Handlers are able to interact with a cookies store, that you can use to store small amounts of data on the client. This data will be persisted across requests, and will be made accessible with every incoming request.
The cookies store is an instance of Marten::HTTP::Cookies
and provides a hash-like interface allowing to retrieve and store data. Handlers can access it through the use of the #cookies
method. Here is a very simple example of how to interact with cookies:
class MyHandler < Marten::Handler
def get
cookies[:foo] = "bar"
respond "Hello World!"
end
end
It should be noted that the cookies store gives access to two sub stores: an encrypted one and a signed one.
cookies.encrypted
allows defining cookies that will be signed and encrypted. Whenever a cookie is requested from this store, the raw value of the cookie will be decrypted. This is useful to create cookies whose values can't be read nor tampered by users:
cookies.encrypted[:secret_message] = "Hello!"
cookies.signed
allows defining cookies that will be signed but not encrypted. This means that whenever a cookie is requested from this store, the signed representation of the corresponding value will be verified. This is useful to create cookies that can't be tampered by users, but it should be noted that the actual data can still be read by the client.
cookies.signed[:signed_message] = "Hello!"
Please refer to Cookies for more information around using cookies.
Using sessions
Handlers can interact with a session store, which you can use to store small amounts of data that will be persisted between requests. How much data you can persist in this store depends on the session backend being used. The default backend persists session data using an encrypted cookie. Cookies have a 4K size limit, which is usually sufficient in order to persist things like a user ID and flash messages.
The session store is an instance of Marten::HTTP::Session::Store::Base
and provides a hash-like interface. Handlers can access it through the use of the #session
method. For example:
class MyHandler < Marten::Handler
def get
session[:foo] = "bar"
respond "Hello World!"
end
end
Please refer to Sessions for more information regarding configuring sessions and the available backends.
Using the flash store
The flash store provides a way to pass basic string messages from one handler to the next one. Any string value that is set in this store will be available to the next handler processing the next request, and then it will be cleared out. Such mechanism provides a convenient way of creating one-time notification messages (such as alerts or notices).
The flash store is an instance Marten::HTTP::FlashStore
and provides a hash-like interface. Handlers can access it through the use of the #flash
method. For example:
class MyHandler < Marten::Handler
def post
flash[:notice] = "Article successfully created!"
redirect("/success")
end
end
In the above example, the handler creates a flash message before returning a redirect response to another URL. It is up to the handler processing this URL to decide what to do with the flash message; this can involve rendering it as part of a base template for example.
Note that it is possible to explicitly keep the current flash messages so that they remain all accessible to the next handler processing the next request. This can be done by using the flash.keep
method, which can take an optional argument in order to keep the message associated with a specific key only.
flash.keep # keeps all the flash messages for the next request
flash.keep(:foo) # keeps the message associated with the "foo" key only
The reverse operation is also possible: you can decide to discard all the current flash messages so that none of them will remain accessible to the next handler processing the next request. This can be done by using the flash.discard
method, which can take an optional argument in order to discard the message associated with a specific key only.
flash.discard # discards all the flash messages
flash.discard(:foo) # discards the message associated with the "foo" key only
Streaming responses
The Marten::HTTP::Response::Streaming
response class gives you the ability to stream a response from Marten to the browser. However, unlike a standard response, this specialized class requires initialization from an iterator of strings instead of a content string. This approach proves to be beneficial if you intend to generate lengthy responses or responses that consume excessive memory (a classic example of this is the generation of large CSV files).
Compared to a regular Marten::HTTP::Response
object, the Marten::HTTP::Response::Streaming
class operates differently in two ways:
- Instead of initializing it with a content string, it requires initialization from an iterator of strings.
- The response content is not directly accessible. The only way to obtain the actual response content is by iterating through the streamed content iterator, which can be accessed through the
Marten::HTTP::Response::Streaming#streamed_content
method. However, this is handled by Marten itself when sending the response to the browser, so you shouldn't need to worry about it.
To generate streaming responses, you can either instantiate Marten::HTTP::Response::Streaming
objects directly, or you can also leverage the #respond
helper method, which works similarly to the #respond
variant for response content strings.
For example, the following handler generates a CSV and streams its content by leveraging the #respond
helper method:
require "csv"
class StreamingTestHandler < Marten::Handler
def get
respond(streaming_iterator, content_type: "text/csv")
end
private def streaming_iterator
csv_io = IO::Memory.new
csv_builder = CSV::Builder.new(io: csv_io)
(1..1000000).each.map do |idx|
csv_builder.row("Row #{idx}", "Val #{idx}")
row_content = csv_io.to_s
csv_io.rewind
csv_io.flush
row_content
end
end
end
When considering streaming responses, it is crucial to understand that the process of streaming ties up a worker process for the entire response duration. This can significantly impact your worker's performance, so it's essential to use this approach only when necessary. Generally, it's better to carry out expensive content generation tasks outside the request-response cycle to avoid any negative impact on your worker's performance.