Output modules ¶
Once the service identification mechanism identifies the service responsible for a given URL, it executes its service handler. The service handler is a function taking the GET and POST parameters as arguments and returning the content to be sent to the client. The return type of the service handler depends on the function used to register it. The most common case is HTML content build with the TyXML library, but Eliom additionally provides a lot of output modules to ease the implementation of common Web interaction. See section Predefined output modules for a comprehensive list.
List of predefined output modules
Services can send several types of data, using a variety of predefined modules. It is also possible to create your own output modules. The main predefined output modules are:
Generating content for the browser
- Registration of functions that generate HTML pages statically checked using polymorphic variant types. You may use constructor functions from Eliom_content.Html.D or a syntax extension close to the standard HTML syntax.
- Registration of functions that generate a portion of page using Eliom_content.Html.F or the syntax extension (useful for XMLHttpRequest requests for example). Do not use with Eliom applications: you can instead use to call server functions that produce HTML nodes.
- Registration of functions that generate text HTML pages, without any validation of the content. The content type sent by the server is "text/html".
- Registration of functions that generate CSS pages, without any validation of the content. The content type sent by the server is "text/css".
- Registration of functions that generate text pages, without any validation of the content. The services return a pair of strings. The first string is the content of the page, while the second string is the content type.
- Registration of services that send files. See here for an example of use.
- Registration of services that send "byte" contents. It is used when big content (that does not fit in memory) is generated.
Generating content for client-server applications
- Functor that allows creation of services belonging to a client-server Eliom application (see chapter client-server applications).
Special browser interraction
- Registration of actions (functions that do not generate any page. See Action). The page corresponding to the URL (without the special parameter identifying the action) is reloaded after the action by default if possible.
- Like Eliom_registration.Action, but the URL is not reloaded after the action. (Same as Eliom_registration.Action with [`NoReload] option).
- Registration of HTTP redirections. The handler returns the service (without parameter) of the page you want to redirect to. The browser will get a 301 or 307 code in answer and redo the request to the new URL. To specify whether you want temporary (307) or permanent (301) redirections, use the ?options parameter of registration functions. For example: register options:`Permanent ... or register options:`Temporary ....
- Same but the ouput type is a string. Use with care! Warning: According to the RFC of the HTTP protocol, the URL must be absolute!
Customization of other outputs
- Specialization of service registration functions by customizing the page type.
Sending caml values to client side code
- Registration of services sending marshalled OCaml values. See the section on communications in the chapter about client-server applications.
Runtime choice of content
- Registration of services that can choose what they send, for example an HTML page or a file, depending on some situation (parameter, user logged or not, page present in a cache ...). It is also possible to create your own modules for other types of pages. See here for an example of use.
Advanced output modules
Sending files ¶
You may want to register a service that sends files. To do so, use the Eliom_registration.File module. Example:
let sendfile = Eliom_registration.File.create ~path:(Eliom_service.Path ["sendfile"]) ~meth:(Eliom_service.Get Eliom_parameter.unit) (fun () () -> return "filename")
Other example, with "suffix" services (see here):
let sendfile2 = Eliom_registration.File.create ~path:(Eliom_service.Path ["files"]) ~meth: (Eliom_service.Get Eliom_parameter.(suffix (all_suffix "filename"))) (fun s () -> Lwt.return @@ "//path//" ^ Ocsigen_lib.Url.string_of_url_path ~encode:false s)
The extension Staticmod is another way to handle static files.
Sending portions of pages
let divpage = Eliom_registration.Flow5.create ~path:(Eliom_service.Path ["div"]) ~meth:(Eliom_service.Get Eliom_parameter.unit) (fun () () -> Lwt.return [div [h2 [txt "Hallo"]; p [txt "Blablablabla"] ]])
module Flow5 = Make_typed_xml_registration(Xml)(Html.F)(struct type content = Html_types.body_content end)
Redirections to Eliom services
The Eliom_registration.Redirection module allows one to register HTTP redirections.
If a request is made for such a service, the server asks the browser to retry with another URL.
Redirection services need to return a GET service without parameter at all. Example:
let redir1 = Eliom_registration.Redirection.create ~options:`TemporaryRedirect ~path:(Eliom_service.Path ["redir"]) ~meth:(Eliom_service.Get Eliom_parameter.uni) (fun () () -> Lwt.return someservice)
let redir = Eliom_registration.Redirection.create ~options:`TemporaryRedirect ~path:(Eliom_service.Path ["redir"]) ~meth:(Eliom_service.Get Eliom_parameter.(int "o")) (fun o () -> Lwt.return (Eliom_service.preapply service_with_params (o,(22,"ee"))))
The options parameter may be either `Temporary or `Permanent.
Note that the cost of a redirection is one more request and one more response.
Redirections to generated URLs
The Eliom_registration.String_redirection module allows one to register HTTP redirections to custom URLs provided as strings. In most cases, it is a better idea to use Eliom_registration.Redirection, even for external redirections (using Eliom_service.extern). Use Eliom_registration.String_redirection only when it is not possible to have a service corresponding to a URL.
Notice that the supplied URL must be absolute.
Actions are used to perform side effects before generating the fallback of a service. When an action is called, the service handler is executed, then the service handler of the fallback service is executed.
Eliom references of scope Eliom_common.request_scope set in an action handler are still available in the service handler of the fallback.
A common use of actions and pathless services working synergistically is the implementation of login/logout forms. Actions are well-suited for the following reasons:
- Connection and disconnection can happen as the side-effect of the action, after which we stay on the same page; and
- The connection/disconnection form generally needs to be present on all pages. An action implemented as a pathless service will respond no matter what page it is called from, so there is no need to create a version with POST parameters of each service. The implementation of the same behavior with standard Web programming techniques is usually much more complicated.
Registering services that decide what they want to send ¶
You may want to register a service that will send, for instance, sometimes an HTML page, sometimes a file, sometimes something else. To do that, use the Eliom_registration.Any module, together with the send function of the module you want to use. Example:
let send_any = Eliom_registration.Any.create ~path:(Eliom_service.Path ["sendany"]) ~meth:(Eliom_service.Get Eliom_parameter.(string "type")) (fun s () -> if s = "valid" then Eliom_registration.Html.send (html (head (title (txt "")) ) (body [p [txt "This page has been statically checked. "; txt "If you change the parameter in the URL "; txt "will get an unchecked text page"]])) else Eliom_registration.Html_text.send "<p>Not a valid page. Try with type=\"valid\" in the URL.</p>")
Dynamically modifying register options using Any
You may also use Eliom_registration.Any to dynamically modify the parameters usually set on the register function. You can set the HTTP code, the charset, the content_type, the HTTP headers and the specific option of the output module.
let change_option = Eliom_registration.Any.create ~path:(Eliom_service.Path ["change_option"]) ~meth:(Eliom_service.Get Eliom_parameter.unit) (fun () () -> Eliom_registration.Html.send ~code:403 (html (head (title (txt "forbidden")) ) (body [p [txt "forbidden"]])))
About kind type and how to serve application and other content with the same service
In Eliom applications, changing the current page does not always do the same thing. When going to a page inside the same application by clicking on a link (or calling Eliom_client.change_page) the client application performs an XmlHttpRequest and modifies the displayed page according to the result. When going to content outside the application (another site, a static file, etc.) the client leaves the application by changing the browser URL.
When using Eliom_registration.Any, there is no way to know before the request whether the content is from the same application or not. To this end, there are phantom type annotations to the type of the send functions: Eliom_registration.kind. The Eliom_registration.Any.register takes a service handler that can server only one kind of content: that way it is not possible to inadvertently mix kinds. The different kinds of content are:
- Browser content: everything that can't be handled by application directly, e.g., HTML pages, files
- Block content: subparts of pages sent as XML: e.g., Flow5, Block.
- Application content: pages of application.
- Ocaml content: marshalled OCaml values.
- Unknown content: content generated as text.
Yet sometimes you may want to mix the kinds of contents a service can return. The function Eliom_registration.appl_self_redirect allows one to cast browser content to application content. When an application requests some content cast through that function, the server sends some information asking the client to exit to a specific address. You should not use that on POST services: leaving the application sending POST parameters is not always possible and the request will be performed two times.
For instance, you may want to serve a file if it exists, and generate some error message with client-side code otherwise. You can achieve this as follows.
let file_or_application = Eliom_registration.Any.create ~path:(Eliom_service.Path ["file_or_application"]) ~meth:(Eliom_service.Get Eliom_parameter.(string "filename")) (fun filename () -> if Eliom_registration.File.check_file filename then Eliom_registration.appl_self_redirect Eliom_registration.File.send filename else My_application.send ~code:404 (html (head (title (txt "no page")) ) (body [p [txt "the file does not exist"]])))
Unknown content can be cast to browser content using Eliom_registration.cast_unknown_content_kind.
Creating your own output modules ¶
By customizing an existing registration module ¶
Using Eliom_registration.Customize, you can specialize a registration function to avoid code duplication. You can for instance add parameters before calling the service handler, or modify the answer.
Importantly, you can use this customization mechanism to check whether a user is logged-in before serving the content of a page. That way, we don't need to do the check in every service handler, and we can get the user information directly.
In this example, we check if we are in a session group telling that the user is connected. If this not the case, we generate a login page. When we are in a group, we retrieve the user information and pass it to the service handler. It returns a title and the content of the body. That way, it also always generate the same CSS without code duplication.
module Connected_param = struct type page = user_info -> Eliom_registration.Html.page Lwt.t let translate page = match Eliom_state.get_volatile_data_session_group ~scope:Eliom_common.default_session_scope () with | None -> login_page () | Some group_name -> let%lwt user_info = get_user_info group_name in let%lwt page_title, content = page user_info in Lwt.return Eliom_content.Html.D.( html (head (title (txt page_title)) ) (body content) ) end module Connected = Eliom_registration.Customize (Eliom_registration.Html) (Connected_param)
By building the HTTP frame
For defining more sophisticated kinds of custom services, you may need to create your own registration module via the Eliom_mkreg.Make functor. As an example, we define a module serving integers as text. Notice that for this particular example you can (and should) use Eliom_registration.Customize instead.
module Int_reg_base = struct type page = int type options = unit type return = Eliom_service.non_ocaml type result = Eliom_registration.browser_content Eliom_registration.kind let result_of_http_result = Eliom_registration.cast_http_result let send_appl_content = Eliom_service.XNever let send ?options ?charset ?code ?content_type ?headers content = let content = string_of_int content in let%lwt r = Ocsigen_senders.Text_content.result_of_content (content, "text/plain") in Lwt.return @@ Ocsigen_http_frame.Result.update r () ~code:(match code with Some c -> c | None -> 200) ~charset: (match charset with | None -> Some (Eliom_config.get_config_default_charset ()) | _ -> charset) ~content_type: (match content_type with | None -> Ocsigen_http_frame.Result.content_type r | _ -> content_type) ~headers: (match headers with | None -> Ocsigen_http_frame.Result.headers r | Some headers -> Http_headers.with_defaults headers (Ocsigen_http_frame.Result.headers r)) end module Int = Eliom_mkreg.Make(Int_reg_base)
If your page type has parameters you should use Eliom_mkreg.Make_poly instead.
Considerations for implementing services
Implementing cooperative service handlers with Lwt ¶
Remember that a Web site written with Eliom is an OCaml application. This application must be able to handle several requests at the same time, in order to prevent a single request from making the whole server hang. To make this possible, Ocsigen uses cooperative threads. The monadic style followed by Lwt makes such threads easy to use.
Below is an example of a page written in a non-cooperative way, that has the effect of stopping the entire server for 5 seconds. No one will be able to query the server during this period:
let looong = Eliom_registration.Html.create ~path:(Eliom_service.Path ["looong"]) ~meth:(Eliom_service.Get Eliom_parameter.unit) (fun () () -> Unix.sleep 5; Lwt.return Eliom_content.Html.D.( html (head (title (txt "")) ) (body [h1 [txt "Ok now, you can read the page."]]) ))
To solve this problem, use a cooperative version of Unix.sleep: Lwt_unix.sleep:
open Eliom_content.Html.D let looong = Eliom_registration.Html.create ~path:(Eliom_service.Path ["looong"]) ~meth:(Eliom_service.Get Eliom_parameter.unit) (fun () () -> let%lwt () = Lwt_unix.sleep 5.0 in Lwt.return Eliom_content.Html.D.( html (head (title (txt "")) ) (body [h1 [txt "Ok now, you can read the page."]]) ))
If you want to use, say, a database library that is not written in a cooperative way, but which is thread-safe for preemptive threads, use the Lwt_preemptive module to detach the computation. In the following example, we simulate the request by making a call to Unix.sleep:
let looong2 = Eliom_registration.Html.create ~path:(Eliom_service.Path ["looong2"]) ~meth:(Eliom_service.Get Eliom_parameter.unit) (fun () () -> let%lwt () = Lwt_preemptive.detach Unix.sleep 5 in Lwt.return Eliom_content.Html.D.( html (head (title (txt "")) ) (body [h1 [txt "Ok now, you can read the page."]]) ))
Error handling ¶
You can catch exceptions raised during page generation in two places:
- add an exception handler to services using the ?error_handler parameter of the registration functions.
- add a global exception handler using Eliom_registration.set_exn_handler
You can use it to catch exception Eliom_common.Eliom_404 and generate a custom 404 page.
let () = Eliom_registration.set_exn_handler @@ function | Eliom_common.Eliom_404 -> Eliom_registration.Html.send ~code:404 Eliom_content.Html.D.( html (head (title (txt "")) ) (body [h1 [txt "Eliom tutorial"]; p [txt "Page not found"]]) ) | Eliom_common.Eliom_Wrong_parameter -> Eliom_registration.Html.send Eliom_content.Html.D.( html (head (title (txt "")) ) (body [h1 [txt "Eliom tutorial"]; p [txt "Wrong parameters"]]) ) | e -> Lwt.fail e
You can check whether a service was directly called or if it was used as a fallback using the Eliom_request_info.get_link_too_old function. In case of services registered with a restricted scope, you can find out which state was closed using Eliom_request_info.get_expired_service_sessions
Error in service handlers of actions
If something wrong happens during an action, it is possible to inform the service generating the page. For instance, you may want to display a "wrong password" message after an aborted connection. To transmit this kind of information, use Eliom references (see module Eliom_reference) created using scope Eliom_common.request_scope. The value will be available to the service generating the page.
Other example: creating user accounts using actions. If the creation fails, you may want to display some message to the user, like "password too weak" or "name already used".