Controllers and Actions

ESP controllers are the conductors of the application and they orchestrate the application's logic and responses to client requests. Via action functions, they receive client requests and generate appropriate responses, mutating the applications data model as required.

An ESP controller is a "C" source file that contains action functions to receive to incoming client requests and manage the applications response. The controller may be part of an ESP MVC application or it may be a stand-alone controller.

Example of a Controller

So what does a controller look like? Here is a partial example called controllers/greeting.c that has one action to say "Hello World".

#include "esp.h"

static void hello() {
    render("Hello World\n");
}

ESP_EXPORT int esp_controller_test_greeting(HttpRoute *route)
{
    espAction(route, "greeting-hello", "user", hello);
    return 0;
}

If the client browser issues a request for:

http://localhost/greeting/hello

ESP will compile the greeting.c controller and then load the controller and run the hello function which will respond to the client with "Hello World\n".

Controller Processing

ESP process requests in stages:

  1. Decode the URI and web request
  2. Route an incoming request to the ESP request handler
  3. If the request is for a controller, load the controller and run the requested action
  4. Run the specified controller action determined by the request route pattern
  5. If the request is for a web page or if the controller did not render a response look for a matching web page and run that to render a response

Routing Requests

At the heart of understanding how controllers are loaded and actions are run, is understanding request routing. When the HTTP server inside espreceives a client request, it examines each of the configured request routes. These may are configured via the esp.json file. Esp then searches the route table, until it finds a matching route for the request URI. The selected route will then break the URI into tokens and save the token values as request parameters.

For example: consider the URI format:

http://example.com/APP/CONTROLLER/ACTION/ID

In this example, APP is the (optional) name of the application, CONTROLLER is the controller name, ACTION is the name of the action method to run and ID is a selector for an element in the Model. When the request URI is tokenized, the ESP HTTP router will extract the controller name and then use this name to load the appropriate controller to service the request.

Restful Routes

ESP applications will typically use a collection of Restful routes. Restful routes are a simple, readable, and familiar pattern for many developers and users. They map common CRUD operations to specific URIs and controller actions.

ESP will create a initial set of Restful routes depending on the esp.server.routes configuration.

DescriptionMethodPatternAction
controllerGET/controller$${controller}
deletePOST/controller/{id=[0-9]+}/delete$delete
createPOST/controller(/)*$create
editGET/controller/{id=[0-9]+}/edit$edit
getGET/controller/{id=[0-9]+}$get
initGET/controller/init$init
listGET/controller/list$list
streamGET/controller/stream$stream
removeDELETE/controller/{id=[0-9]+}$remove
updatePUT/controller/{id=[0-9]+}/{action}(/)*$update
actionGET,POST/controller//{id=[0-9]+}{action}$${controller}/${action}

The delete route is a POST method variant of remove. The stream route is for WebSockets streaming of updates.

Actions

Actions are where the controller does its work. In ESP, actions are simple "C" functions and thus they need to be registered with ESP before use. This is done in the controller module initialization function. The initialization function is named: esp_controller_NAME, where NAME is the unique name of the controller. The first time the controller is invoked, the controller module will be loaded and the initialization function will be run. Typically, the initialization function will then call espAction to bind action functions to route actions.

#include "esp.h"
/* Actions */
static FUNCTION() {
    ...
}
/*
    Controller module initialization routine
 */
ESP_EXPORT int esp_controller_APP_NAME(HttpRoute *route)
{
    espAction(route, "NAME-ACTION", "user", FUNCTION);
    return 0;
}

If you want to programmatically bind a C function to a URI without creating a loadable module and without defining explicitly creating a route you can call the espBindProc API. This will create a new route by inheriting settings from an existing route and then define an action based on the supplied URI pattern. Because this creates a new route for each callback, it is best not to use if you have many callbacks.

espBindProc(route, "/do/something", callback);

Missing Actions

When responding to a request, if the required controller and action is not found, ESP will look for a corresponding ESP page of the same name. If that is not defined, ESP will respond with an HTTP 404 error indicating that the required action and page could not be found.

Processing the Request

The controller action can perform any processing it desires. There are no real restrictions except you don't want to block for too long without giving the client some feedback. Because ESP is multithreaded, you can block. In that case, the server will continue to run and serve other requests. However, note that threads are a limited resource. It may be better to use non-blocking techniques such as async processing.

Async Processing

An action may service a long-running request without blocking, by responding in pieces. An action function may return without completing the request. Normally, ESP will automatically finalize the request when the action returns. To prevent this, call dontAutoFinalize to tell ESP not to finalize the request and to keep the connection and response open. At anytime and from any other code, you may then call finalize to complete the request. To force output to the client, without finalizing, use flush.

For example:

static void second(HttpStream *stream) {
    setConn(stream);
    render("World\n");
    finalize();
}

static void first() {
    dontAutoFinalize();
    render("Hello ");
    flush();
    setTimeout(second, 5000, getStream());
}

This example will print "Hello ", then wait five seconds and then print "World". Note that the request is held open, but ESP is not blocked in any thread. The call to setTimeout will arrange to have the ESP event loop invoke second after five seconds have elapsed. This pattern is a highly efficient in its use of system resources and scales very well.

Request Timeout Limits

ESP has a request timeout and a request inactivity timeout. If a request duration exceeds the limit defined via the timeouts.request directive in esp.json, the request will be aborted. If the request does no I/O for more than the limit specified by the timeouts.inactivity it will be similarly aborted. These limits can be set per route in esp.json.

Loading and Caching Controllers

Before a controller can run, it must first be compiled and linked into a loadable library. On Windows, this will be a DLL, on Unix, it will be a shared library with a ".so" extension. On VxWorks it will be a loadable task module.

The compilation of controllers into libraries happens automatically if a compiler is installed on the system and if the EspUpdate directive is enabled. If so, when a request is received, ESP will compile and link the controller as a library and save the result into the ESP cache directory for future use. After servicing the first request for the controller, the controller code is retained in memory and the controller will not be reloaded unless the source code is modified. If ESP is rebooted, the cached library will be reloaded without recompiling. This provides two levels of caching: in-memory and on-disk as a shared library.

Development and Production Modes

When a request for a controller is received, ESP will test if the source code has been updated to determine if the controller must be recompiled. If the source has been changed, ESP will wait for all requests that are using the already loaded controller, to gracefully complete. When no requests are using the old controller version, ESP will unload the controller, and ESP will recompile the updated controller source and create a new shared library that will then be loaded and the request servicing resumed.

If ESP was configured and built in debug mode, the default value for EspUpdate will be on. If ESP was built in release mode, the default value is off. In release mode is it common practice to lock down the compiled controllers and not auto-recompile once deployed.

Controller Context

ESP establishes a request context for the controller before invoking the controller action. The top level of the context is represented by the HttpStream connection object. From this, all other request information can be reached, including the:

ESP Short-Form API

ESP defines a terse, short-form, API that uses the current connection object to provide context for the API. When using this API, explicit access to the connection object should not typically be required. The ESP short-form API should cover 95% of requirements for action processing.

Explicit Connection Access

If explicit access to the connection object is required, action functions may define a connection argument as which is passed into all actions.

static void ACTION(HttpStream *stream) {
    /* Use the stream reference here */
}

Alternatively, the connection object can can be retrieved using the getStream API.

Navigating the Connection Object

The HttpStream object represents the current TCP/IP connection. By using HTTP KeepAlive, the connection may be utilized for multiple requests. The fields of the HttpStream object are public and can be accessed and navigated.

HttpStream PropertyPurpose
rxReference to the HttpRx receive object
txReference to the HttpTx transmit object
hostReference to the HttpHost object
httpReference to the Http object
endpointReference to the HttpEndpoint transmit object
limitsReference to the HttpLimits object
ipRemote client IP address
portRemote client port

Navigating the Receive Object

The HttpRx object represents the receive side of the HTTP protocol. On the server, it holds state regarding the client HTTP request. The fields of the HttpRx object are public and can be accessed and navigated.

HttpRx PropertyPurpose
methodHTTP request method
uriCurrent URI (may be rewritten)
pathInfoPath portion of the URI after the scriptName
scriptNameScriptName portion of the URI
lengthContent length
routeReference to current HttpRoute object
paramsRequest params (query, form and route parameters)
filesUploaded files

Navigating the Tx Object

The HttpTx object represents the transmit side of the HTTP protocol. On the server, it holds state regarding the response to the client. The fields of the HttpTx object are public and can be accessed and navigated.

HttpTx PropertyPurpose
filenameName of the real file being served
extFilename extension
handlerRequest handler object
lengthResponse content length
statusResponse HTTP status
headersResponse HTTP headers

Sample of ESP API

Here are a few of the ESP APIs that may be used inside controller actions:

Method / Property Description
addHeader Add a response HTTP header.
createSession Enable session control.
destroySession Destroy a session.
dontAutoFinalize Don't automatically finalize output when the action returns. Useful for async actions.
error Send an error flash message to the next web page.
inform Send an informational flash message to the next web page.
param Get a request parameter value.
redirect Redirect the client to a new URI.
render Render the text data back to the client.
renderFile Render a file's contents back to the client.
setContentType Set the response content type.
setCookie Define a cookie to include in the response.
setSessionVar Set a variable in session state storage.
uri Make a URI from parameters.

Request Parameters

ESP will collect request query, form data and route parameters into one params object which is accessible to actions via the param API. Each query key/value pair and all request form elements posted by the client will become a properties of the params object. When routing the request, ESP will tokenize the URI and create parameters for each positional token in the URI. The Controller name and Action are defined as the parameters: controller and token.

Rendering Views

After processing the request, the controller is responsible for rendering a response back to the client. The controller can choose how to respond. It may explicitly create the response body by calling render to generate HTML. Alternatively, the action may call renderView to response with a view web page. If the action method does not explicitly generate any response, ESP will invoke a view with the same name as the action method.

Generating Controllers and Actions

If you are creating an MVC application, you may use the ESP application generator, called esp to generate controllers, actions and controller scaffolds. To generate a new controller, run:

esp generate controller name [actions...]

name is the controller name. actions... are the names of the action functions you want to generate. This command will create the controller source file under the controllers directory.

If no actions are requested when generating, esp will create a controller with an initialization function but without any actions. If actions are specified, esp will create an empty action method for each specified action. You can safely edit the controller source to meet your needs.

For example, if you use the command:

$ esp generate controller admin edit login logout command

The following controller code will be generated. There is one function generated for each action and a call to espAction is added to register the controller. The esp_controller_admin initialization function is invoked once when the controller is first loaded.

#include "esp.h"

static void edit() {}
static void login() {}
static void logout() {}
static void command() {}

ESP_EXPORT int esp_controller_demo_admin(HttpRoute *route)
{
    cchar   *role = "user";
    espAction(route, "admin/edit", role, edit);
    espAction(route, "admin/login", role, login);
    espAction(route, "admin/logout", role, logout);
    espAction(route, "admin/command", role, command);
    return 0;
}

© Embedthis Software. All rights reserved.