Home > Programming Guide > Creating Appweb Handlers

Quick Nav

See Also

Creating Appweb Handlers

Appweb responds to client Http requests via request handlers. A request handler is responsible for analyzing the request and generating the appropriate response content.

Appweb provides a suite of handlers for various content types and web frameworks. The standard handlers supplied with Appweb are: CGI, directory, file, Ejscript, ESP and PHP. You can extend Appweb by creating your own custom handler to process Http requests and perform any processing you desire.

Handlers are typically packaged as modules and are configured in the appweb.conf configuration file. Modules are loaded by the LoadModule directive. A handler is specified to service requests for a route by either the AddHandler or SetHandler directives. For example:

LoadModule myHandler mod_my
<Route /my/>
    SetHandler myHandler
</Route>

The LoadModule directive causes Appweb to load a shared library containing your handler. The SetHandler directive then defines the handler to serve all requests that begin with /my/.

Request Pipeline

When configured for a route, a handler sets at one end of the request pipeline. The pipeline is a full-duplex data stream in which request data flows upstream from the client and response data to the client flows downstream.

pipeline

The request pipeline consists of a sequence of processing stages. The first stage is the handler followed by zero or more filters and a network connector. Each stage has two queues, one for outgoing data and one for incoming. A queue also has two links to refer to the upstream and downstream queues in the pipeline.

Queues

Queues provide open, close, put and service methods. These methods allow a queue instance to manage state and respond to data packets. A queue can respond immediately to an incoming packet by processing or forwarding a packet in its put() method. Alternatively, the queue can defer processing by queueing the packet on it's service queue and then waiting for it's service() method to be invoked.

If a queue does not define a put() method, the default put() method will be used. This puts data onto the service queue. The default incoming put() method joins incoming packets into a single packet on the service queue.

Packets

Packets are an instance of the HttpPacket structure. HttpPacket instances efficiently store data and can be passed through the pipeline without copying. Appweb network connectors are optimized to write packets to network connections using scatter/gather techniques so that multiple packets can be written in one O/S system call.

Handler Callbacks

A handler integrates into the pipeline via its two queues. Furthermore, the handler defines an extended set of callback functions that are invoked by the pipeline as the client request progresses through the pipeline life cycle. These callbacks are:

CallbackPurpose
matchTest if the request matches this handler.
openOpen a new request instance for the handler. Corresponds to the queue open callback.
startStart the request.
incomingDataAccept a packet of incoming data. Corresponds to the receive queue put callback.
readyThe request is fully parsed and all incoming data has been received.
outgoingServiceService the outgoing queue. Used primarily for non-blocking output and flow control.
writableThe outgoing pipeline can absorb more data.
closeClose the request. Corresponds to the queue close callback.

When the pipeline is constructed for a request, the relevant handler callbacks (open, close, incomingData, outgoingService) will be copied into the relevant queue callbacks (open, close, put, service). The handler callbacks are discussed in more detail below.

Handler Data Flow

A handler will receive request body data via its incomingData callback on its read queue. The callback will be invoked for each packet of data. The end of the input stream is signified by a zero length packet. The handler may choose to aggregate body data on its read service queue until the entire body is received.

A handler will generate output response data and send it downstream. This may be done by calling httpWrite which buffers output and creates packets as required and queues the packets on the handlers output queue. These packets will be flushed downstream if the queue becomes full or if httpFinalize is called. Alternatively, the handler can create its own packets via httpCreateDataPacket, fill with data and then send downstream by calling httpPutPackToNext.

The httpWrite routine will always accept and buffer the data so callers must take care not to overflow the queue which will grow the process memory heap to buffer the data. Rather handlers should take create to test if the output queue is full. If the output queue is full, the handler should pause generating the response and wait until the queue has drained sufficiently. When that occurs, the writable callback will be invoked. Handlers can test if the queue is full by comparing HttpQueue.count with HttpQueue.max.

Pipeline Life Cycle

When a request is received from the client, the Appweb Http engine parses the Http request headers and then determines the best Appweb route for the request. A route contains the full details for how to process a request including the required handler and pipeline configuration.

Matching a Handler — match

Once parsed, the router tests each of the eligible handlers for the route by calling the handler match callback. The first handler to claim the request will be used.

Initializing a Handler — open

The Http engine then creates the pipeline and invokes the handler open callback to perform required initialization for the handler.

Starting a Handler — start

Once the request headers are fully parsed, the Http engine invokes the handler start callback. At this point, request body data may not have been received. Depending on the request, generating a response may or may not be appropriate until all body data has been received. If there is no body data, the handler can process and maybe generate the entire response for the request during this callback.

Receiving Input — incomingData

If the request has body data (POST|PUT), the input data will be passed in packets to the handlers incomingData callback. The handler should process as required.

Ready to Respond — ready

Once all body data has been received, the handler ready callback is invoked. At this stage, the handler has all the request information and can fully process the request. The handler may start or fully generate the response to the request.

Sending Responses — outgingService

A handler can write a response directly via httpWrite. Alternatively, it can create packets and queue them on its own outgoingService queue. Thereafter, the outgoingService callback is invoked to process these packets. This pattern is useful for handlers that need to accumulate response data before flushing to the client.

Pipeline Writable — writable

Some handlers may generate more response data than can be efficiently buffered without consuming too much memory. If the output TCP/IP socket to the client is full, a handler may not be able to continue processing until this data drains. In these cases, the writable callback will be invoked whenever the output service queue has drained sufficiently and can absorb more data. As such, it may be used to efficiently generate the response in chunks without blocking the server.

Generating Responses

An HTTP response consists of a status code, a set of HTTP headers and optionally a response body. If a status is not set, the successful status of 200 will be used. If not custom headers are defined, then a minimal standard set will be generated.

Setting Status and Headers

The response status may be set via: httpSetStatus. The response headers may be set via: httpSetHeader. For example:

HttpConn *conn = q->conn;
httpSetStatus(conn, 200);
httpSetHeader(conn, "NowIs", "The time is %s", mprGetDate(NULL));

Generating an Error Response

If the request has an error, the status and a response message may be set via: httpError. When httpError is called to indicate a request error, the supplied response text is used instead of any partially generated response body and the the connection field conn->error is set. Once set, pipeline processing is abbreviated and handler callbacks will not be called anymore. Consequently, if you need to continue handler processing, but want to set a non-zero status return code, do not use httpError. Rather, use httpSetStatus.

httpError(conn, 200, HTTP_CODE_GONE, "Can't find %s", path);

Aborting Requests

The status argument to httpError can also accept flags to control how the socket connection is managed. If HTTP_ABORT is supplied, the request will be aborted and the connection will be immediately closed. The supplied message will be logged but not transmitted. When a request encounters an error after generating the status and headers, the only way to indicate an error to the client is to prematurely close the connection. The browser should notice this and discard the response. The HTTP_CLOSE flag may be used to indicate the connection should be closed at the orderly completion of the request. Normally the connection is kept-open for subsequent requests on the same socket.

httpError(conn, 504, HTTP_ABOR, "Can't continue with the request");

Redirecting

Sometimes a handler will want to generate a response that will redirect the client to a new URI. Use the httpRedirect call to redirect the client. For example:

httpRedirect(conn, HTTP_CODE_MOVED_PERMANENTLY, uri);

Generating Response Body

The simplest way to generate a response is to use httpWrite. This is effective if the total response content can be buffered in the pipeline and socket without blocking. (Typically 64K or less depending on LimitSBuffer in appweb.conf). The httpWrite routine will automatically flush data as required. When all the data has been written, call: httpFinalize. This finalizes the output response data by sending an empty packet to the network connector which signifies the completion of the request.

httpWrite(conn, "Hello World\n");
httpFinalize(conn);

Generating Responses without Blocking

If a handler must generate a lot of response data, it should take care not to exceed the maximum queue size (q->max) and to size packets so as to not exceed the maximum queue packet size (q->packetSize). These advisory maximums are set to maximize efficiency.

Here is an example routine to write a block of data downstream, but only send what the queue can absorb without blocking.

static ssize doOutput(HttpQueue *q, cchar *data, ssize len)
{
    HttpPacket  *packet;
    ssize       count;
    count = min(len, q->max - q->count);
    count = min(count, q->packetSize);
    packet = httpCreateDataPacket(count);
    mprPutBlockToBuf(packet->content, data, len);
    httpPutForService(q, packet, HTTP_SCHEDULE_QUEUE);
    /* Return the count of bytes actually written */
    return count;
}

The handler's process handler callback will be invoked once the request has received all body data and whenever the output queue can absorb more data. Thus the writable callback is an ideal place for generating the response in chunks.

static void writable(HttpQueue *q)
{
    if (finished) {
        httpFinalize(q->conn);
    } else {
        httpWriteString(q, getMoreData(q));
    }
}

This (trivial) example writes data in chunks each time the writable callback is invoked. When output is complete, the example calls httpFinalize.

If a handler puts data onto a service queue, it should call httpPump to enable the pipeline to advance and potentially invoke the handler's writable callback.

Flushing Data

Calling httpWrite will not automatically send the data to the client. Appweb buffers such output data to aggregate data into more efficient larger packets. If you wish to send buffered data to the client immediately, call httpFlush. When the request is complete, calling httpFinalize call automatically call httpFlush.

Response Paradigms

In summary, a handler may use one of several paradigms to implement how it responds to requests.

Blocking

A handler may generate its entire response in its start(), or ready() callback and will block if required while output drains to the client. In this paradigm, httpWrite is typically used and Appweb will automatically buffer the response if required. If the response is shorter than available buffering (typically 64K), the request should not block. After the handler has written all the data, it will return immediately and Appweb will use its event mechanism to manage completing the request. This is a highly efficient method for such short requests.

If the response is larger than available buffering, the Appweb worker thread will have to pause for data to drain to the client as there is more data than the pipeline can absorb. This will consume a worker thread while the request completes, and so is more costly in terms of Appweb resources. Use care when using this paradigm for larger responses. Ensure you have provided sufficient worker threads and/or this kind of request is infrequent. Otherwise this can lead to a denial-of-service vulnerability.

Non-Blocking

A more advanced technique is to write data in portions from the writable() callback. The callback will be invoked whenever the pipeline can absorb more data. The handler should test the q->max, q->count and q->packetSize values to determine how much to write before returning from writable(). The httpFinalize routine should be called when the request is complete.

Async Generation

A handler can asynchronously generate response data outside of the typical handler callbacks. This may be in response to an application or device vent. Consequently, care must be taken to ensure thread safety. Appweb serializes request activity on a connection dispatcher and does not use thread locking for handlers or pipeline processing. This is highly efficient but requires that all interaction with Appweb data structures be done from Appweb events via Appweb dispatchers. To run code from the connection dispatcher, use: mprCreateEvent.

mprCreateEvent(q->conn->dispatcher, "deviceEvent", 0, doMoreOutput, q, 0);

This will schedule the function doMoreOutput to run from the connection dispatcher.

You may call mprCreateEvent from any thread and the scheduled function will run serialized within the event loop for the connection.

Owning a Connection

Appweb monitors requests and imposes timeout limits. The RequestTimeout directive in the appweb.conf file specifies the maximum time a request can take to complete. The InactivityTimeout directive specifies the maximum time a request can perform no input or output before being terminated. If either of these timeouts are violated, the request will be terminated and the connection will be closed by Appweb.

A handler can modify these timeouts via the httpSetTimeout API. Alternatively, the handler can steal the connection from Appweb and assume responsibility for the connection via: httpStealConn. Otherwise, a handler should check that a connection has not been closed before generating output for a connection. This can be done by checking conn->tx which will be set to NULL when the connection is closed.

Note that the HttpConn object and other request objects will be preserved in memory even after Appweb closes a request and connection. This is because calling mprCreateEvent with a queue reference will maintain a reference to the queue and connection object and this reference will delay the garbage collector from reclaiming the memory for these objects.

When output is complete, httpFinalize should be called to signal the completion of the request. After writing or completing the request, httpPump() should be called to perform pipeline processing on the request.

Coding Issues

There are a few coding issues to keep in mind when creating handlers. Appweb is a multithreaded event loop server. As such, handlers must cooperate and take care when using resources.

Blocking

If a handler blocks, it will consume a worker thread. But more importantly, when a thread blocks, it must yield to the garbage collector. Appweb uses a cooperative garbage collector where worker thread yield to the collector at designated control points. This provides workers with a guarantee that temporary memory will not be prematurely collected. All MPR functions that wait implicitly also yield to the garbage collector. Handlers should call mprYield directly whenever they block and are not calling an MPR function that blocks. This ensures that the garbage collector can work and collect memory while the worker thread is asleep. Handlers do not need to call mprYield when executing on threads that were created outside of the MPR (i.e. not MPR worker threads).

Handler and Queue State

Sometimes a handler needs to store state information. Appweb defines two empty fields that can be used by handlers to hold managed references. The HttpConn.data field is available for use by handlers and can store a managed-memory reference. Managed-memory is memory allocated by the MPR layer APIs. See for details about memory allocation.

Similarly, HttpQueue.queueData field available for queue stages. This field is available for use by either handlers or filters. Both these fields must only be used for managed memory and not for memory allocated by malloc.

Appweb defines two fields that can be used to store unmanaged memory references: HttpConn.staticData and HttpQueue.staticData. Use these to store references to memory allocated by malloc.

Defining a Handler

To define an Appweb handler, you must do three things:

  1. Package as a module (see Creating Modules).
  2. Call httpCreateHandler in your module initialization code to define the handler when the module is loaded
  3. Define the handler in the appweb.conf configuration file via the AddHandler or SetHandler directives.
int maMyModuleInit(Http *http, MprModule *mp)
{
    HttpStage   *handler;
    if ((handler = httpCreateHandler(http, "myHandler", 0, mp)) == 0) {
        return MPR_ERR_CANT_CREATE;
    }
    handler->open = openSimple; 
    handler->close = closeSimple; 
    handler->start = startSimple; 
    return 0;
}

Connection State

As a handler progresses in its service of a request, it is often necessary to examine the state of the request or underlying connection. Appweb provides a set of object fields that can be examined.

FieldPurpose
HttpConn.erroris set whenever there is an error with the request. i.e. httpError is called.
HttpConn.connErroris set whenever HTTP_ABORT is supplied to httpError. This is used when the HTTP/1.1 protocol is compromised, corrupted or otherwise unlikely to be able to handle a subsequent keep-alive request.
HttpConn.state defines the connection state and is often the best thing to test. States are: HTTP_STATE_BEGIN, HTTP_STATE_CONNECTED, HTTP_STATE_FIRST, HTTP_STATE_CONTENT, HTTP_STATE_READY, HTTP_STATE_RUNNING, HTTP_STATE_FINALIZED and HTTP_STATE_COMPLETE.
HttpTx.finalizedis set by the handler when httpFinalize is called. This means the request is complete. httpFinalize calls httpFinalizeOutput.
HttpTx.finalizedOutputis set by the handler when all response data has been written, there may still be processing to complete.
HttpTx.finalizedConnectoris set by the connector when it has fully transmitted the response data to the network socket.
HttpTx.respondedis set by various routines when any part of a response has been initiated httpWrite, httpSetStatus etc.

More Info

See the simpleHandler for a working handler example.

Here are links to some of the Appweb supplied handlers:

Here are links to some of the Appweb supplied handlers:

Here are links to some of the Appweb supplied connectors:

© Embedthis Software LLC, 2003-2013. All rights reserved. Embedthis, Appweb, ESP, Ejscript and Embedthis GoAhead are trademarks of Embedthis Software LLC.