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
Note that you can use either strings or symbols when interacting with the routing parameters returned by the #params
method.
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
.
You can also express the status
of the response as a symbol that must comply with the values of the HTTP::Status
enum. For example:
respond("Response content", content_type: "text/html", status: :ok)
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
.
You can also express the status
of the response as a symbol that must comply with the values of the HTTP::Status
enum. For example:
render("path/to/template.html", context: { foo: "bar" }, content_type: "text/html", status: :ok)
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)
You can also express the status
of the response as a symbol that must comply with the values of the HTTP::Status
enum. For example:
head :not_found
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
.
You can also express the status
of the response as a symbol that must comply with the values of the HTTP::Status
enum. For example:
json({ foo: "bar" }, status: :ok)
Callbacks
It is possible to define callbacks in order to bind methods and logics to specific events in the lifecycle of your handlers. For example, it is possible to define callbacks that run before a handler's #dispatch
method gets executed, or after it!
Please head over to the Handler callbacks guide in order to learn more about handler callbacks.
Generic handlers
Marten provides a set of generic handlers that can be used to perform common application tasks such as displaying lists of records, deleting entries, or rendering templates. This saves developers from reinventing common patterns.
Please head over to the Generic handlers guide in order to learn more about available generic handlers.
Global template context
All handlers have access to a #context
method that returns a template context object. This "global" context object is available for the lifetime of the considered handler and can be mutated in order to define which variables are made available to the template runtime when rendering templates through the use of the #render
helper method or when rendering templates as part of subclasses of the Marten::Handlers::Template
generic handler.
To modify this context object effectively, it's recommended to utilize before_render
callbacks, which are invoked just before rendering a template within a handler. For example, this can be achieved as follows when using a Marten::Handlers::Template
subclass:
class MyHandler < Marten::Handlers::Template
template_name "app/my_template.html"
before_render :add_variable_to_context
private def add_variable_to_context : Nil
context["foo"] = "bar"
end
end
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.
Exceptions handling
Marten lets you define callback methods that are invoked when certain exceptions are encountered during the execution of your handler's #dispatch
method. These exception handling callbacks can be defined by using the #rescue_from
macro, which accepts one or more exception classes and an exception handler that can be specified by a trailing :with
option containing the name of a method to invoke or a block containing the exception handling logic.
For example, the following handler will react to possible Auth::UnauthorizedUser
exceptions by calling the #handle_unauthorized_user
private method:
class ProfileHandler < Marten::Handlers::Template
include RequireSignedInUser
template_name "auth/profile.html"
rescue_from Auth::UnauthorizedUser, with: :handle_unauthorized_user
private def handle_unauthorized_user
head :forbidden
end
end
And the following handler will do exactly the same by invoking the specified block:
class ProfileHandler < Marten::Handlers::Template
include RequireSignedInUser
template_name "auth/profile.html"
rescue_from Auth::UnauthorizedUser do
head :forbidden
end
end
It is worth mentioning that exception handling callbacks are inherited and that they are searched bottom-up in the inheritance hierarchy.
Your exception handling callbacks should return Marten::HTTP::Response
objects. If that's not the case, then your exception handling callback logic will be executed but the original exception will be allowed to "bubble up" (which will likely result in a server error).