Skip to main content
Version: Next

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
info

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:

MethodDescription
#bodyReturns the raw body of the request as a string.
#cookiesReturns a hash-like object (instance of Marten::HTTP::Cookies) containing the cookies associated with the request.
#dataReturns a hash-like object (instance of Marten::HTTP::Params::Data) containing the request data.
#flashReturns a hash-like object (instance of Marten::HTTP::FlashStore) containing the flash messages available to the current request.
#headersReturns a hash-like object (instance of Marten::HTTP::Headers) containing the headers embedded in the request.
#hostReturns the host associated with the considered request.
#methodReturns the considered HTTP request method (GET, POST, PUT, etc).
#query_paramsReturns a hash-like object (instance of Marten::HTTP::Params::Query) containing the HTTP GET parameters embedded in the request.
#sessionReturns 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:

MethodDescription
#contentReturns the content of the response as a string.
#content_typeReturns the content type of the response as a string.
#cookiesReturns a hash-like object (instance of Marten::HTTP::Cookies) containing the cookies that will be sent with the response.
#headersReturns a hash-like object (instance of Marten::HTTP::Headers) containg the headers that will be used for the response.
#statusReturns 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
tip

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.

tip

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.

tip

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 allows constructing a response containing headers but without actual content. The method accepts a status code only:

head(404)
tip

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.

tip

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.

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:

config/routes.cr
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
caution

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.