Serving Web Pages

This page describes both the Stunt server functionality for reading and writing HTTP requests and responses, and the functionality in Dialog (the Improvise web server package).

Built-In Server Functionality

The Stunt server includes the built-in function read_http() that behaves very much like the existing built-in read() except that instead of returning a line of input, it returns a parsed representation of an HTTP request or response. It uses the excellent Node HTTP parser (https://github.com/joyent/http-parser), which is both fast and accurate. Almost everything that can be said about read() applies to read_http().

Here’s code for a simple Hello, World! HTTP server, which could be implemented in the do_login_command() verb on a listening object. It dumps the parsed request to the server log for inspection (but otherwise ignores it).

set_connection_option(player, "hold-input", 1);
set_connection_option(player, "disable-oob", 1);
set_connection_option(player, "binary", 1);

while (request = read_http("request", player))
  server_log(toliteral(request));

  response = "<h1>Hello, World!</h1>~0D~0A";
  length = length(decode_binary(response, 1));

  notify(player, tostr("HTTP/1.1 ", 200, " ", "Ok", "~0D~0A"));
  notify(player, tostr("Content-Type: ", "text/html", "~0D~0A"));
  notify(player, tostr("Content-Length: ", length, "~0D~0A"));
  notify(player, "~0D~0A");
  notify(player, response);
endwhile

boot_player(player);

Dialog

Dialog is a complete web server and lightweight web framework. It handles many of the details (security, session management, parsing parameters…) of serving responses to HTTP requests, and exposes a simple interface for building web applications.

Getting Started

It’s easier (and probably safer) to work with a child of the $dialog.server object, instead of the parent object itself. It’s possible to have multiple servers listening on independent ports. (It’s also possible to serve multiple hosts from a single server – that’s left as an exercise for the reader.)

; s = create($dialog.server)

The verbs start() and stop() start and stop an HTTP server. The start() verb requires a single argument – the port to listen on.

; s:start(8888) /* now listening on port 8888 */
; s:stop()

Page Creation Workflow

Creating a page is typically a two step process that comprises adding a route (routes tell the server about request patterns – usually combinations of method and path – that should be handled) and creating a handler for that route (the handler generates a response from the request).

Adding A Route

Routes are children of $dialog.route_proto with a executable match() verb. Active routes are located inside an instance of a server.

; r = create($dialog.route_proto)
; add_verb(r, {r.owner, "xd", "match"}, {"this", "none", "this"})
; move(r, s)

The match() verb examines an HTTP request and identifies the object and verb that will handle the request. Typically it looks at the request method and path, but more complicated scenarios are possible (for example, it could use the Host header to implement virtual hosts). The following simple match() verb uses the method and the path to route the request to CRUD verbs defined on itself (the route is also acting as the handler).

@program r:match as application/x-moocode
{request} = args;

method = request:method();

/* Useful helpers:
 *   If the full URI is "/foo/bar?one=1&two=2"
 *     request:uri() => "/foo/bar?one=1&two=2"
 *     request:path() => "/foo/bar"
 *     request:query() => "one=1&two=2"
 */
uri = request:uri();
path = request:path();
query = request:query();

if (r = match(path, "^%/things%(%/%([0-9]+%)%)?$"))
  id = path[r[3][2][1]..r[3][2][2]];

/* Return: {object, verb, parameters}
* object - the handler object
* verb - the handler verb
* parameters - parameters to pass to the handler
*/
if (method == "GET" && !id) return {this, "index", []}; elseif (method == "POST" && !id) return {this, "create", []}; elseif (method == "GET" && id) return {this, "show", ["id" -> id]}; elseif (method == "PUT" && id) return {this, "update", ["id" -> id]}; elseif (method == "DELETE" && id) return {this, "destroy", ["id" -> id]}; endif endif return {}; .

The request argument encapsulates information about the request and defines convenience methods for accessing that information. If a match() verb successfully matches, it should return a three element list containing the object number and verb name of the handler – the third argument is a map of values to add to the request parameters (in this case, the id from the path). If it fails to match, it should return the empty list.

Programming the Handler

The handler verbs consume an HTTP request and produce an HTTP response. The following verb handles the index request matched by the router above. It generates a fragment of HTML and uses the ok() verb to pass the fragment back to Dialog for serving. Any request parameters are passed in as parameters.

@program r:index as application/x-moocode
{parameters} = args;

/* in this example, `parameters' are unused */
body = {"<ul>"}; for player in (players()) body = {@body, tostr("<li>", player.name, "</li>")}; endfor body = {@body, "</ul>"};
this:ok(body); /* also moved(), not_found(), etc. */

/* If you need them, the request and response
* objects are available as:
* this.request
* this.response
*/
.

Generating HTML this way is hard. The Mustache package makes it easy to author pages using the Mustache templating language. See the section on Mustache Templates for more information.

The Request Prototype

Dialog encapsulates an HTTP request in a $dialog.request_proto object. The object provides convenient verbs for getting information about the request.

  • method The HTTP request method (GET, POST etc.)
  • headers A map of the headers
  • cookies A map of the contents of the Cookie header
  • uri The full URI (e.g. “/foo/bar?one=1&two=2”)
  • path The path part of the URI (e.g. “/foo/bar”)
  • query The query part of the URI (e.g. “one=1&two=2”)
  • parameters A map of the full request parameters. This includes the query parameters as well as URL encoded and form parameters from the body of the request (if present).
  • body The raw body.
  • type The MIME type of the body.
  • length The length of the body.

The Response Prototype

Dialog encapsulates an HTTP response in a $dialog.response_proto object. The object provides convenient verbs for setting information about the response.  (However, helper verbs like ok() usually take care of the details.)

  • set_status The HTTP response status (200, 302, etc.)
  • set_headers A map of the headers
  • set_body The body (as a MOO binary string).
  • set_type The MIME type of the body.
  • set_length The length of the body (will be calculated if omitted).
  • set_location The Location header (for redirects).

Mustache Templates

Writing code that generates HTML by hand is hard, and so 1990s. In the 21st century, programmers use a templating engine and let the computer do the work for them. Choice of templating technology is a matter of taste (and a subject of religious wars). Stunt | Improvise uses Mustache because it is simple to use and widely available in a number of languages (write your templates once and they render in Mustache in-MOO and in the browser).

An aside about verbs in Stunt | Improvise. The Stunt Kernel (the package that forms the basis for every other package that is part of Stunt | Improvise) supports languages other than MOOcode as verb code. The external compilers functionality transparently compiles these languages into MOOcode, and makes the original source available for subsequent viewing/editing. A Mustache template is just another language from this perspective – one that is transparently compatible with verb_code() and set_verb_code() and that generates HTML when executed.

The mustache template below generates the HTML fragment from the example above. (The verb name is “/”.) The phrase as application/x-mustache instructs the server to compile the source as a Mustache template (instead of MOOcode).

@program r:/ as application/x-mustache
<ul>
  {{#names}}
    <li>{{.}}</li>
  {{/names}}
</ul>
.

More concise, right?

The controller action/verb can now be rewritten to use the template verb, passing in a map of parameters and player names as arguments to the template for processing.

@program r:index as application/x-moocode
{parameters} = args;

players = $ask:from(players()):select("name"):all();
body = this:("/")(["names" -> players:slice("names")]); this:ok(body); .

Note that the index() verb just calls the view (/) like any other verb (because it is just like any other verb).