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, 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 sits 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. Data flows inbound from the network connector, optionally through filters and terminates that the handler. Response data starts with the handler, optionally through output filters and is finally sent by the network connector to the client.

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 finally 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.

Stages

All pipeline stages provide a set of callbacks that are invoked by the pipeline as requests are received and data flows through the pipeline. A stage may provide incoming and outgoing callbacks to receive incoming packets and outgoing packets. However a handler will not have an outgoing callback as it generates the outgoing data and similarly, the network connector will not have an incoming callback.

The callbacks allow a stage to receive packets that can be forwarded unaltered downstream, or modified and forwarded, or the packets can be held for deferred processing by queueing the packet on the stage's service queue.

The stage queues are processed by the pipeline invoking the stage's incomingService or outgoingService callbacks.

Stage Callbacks

The full set of stage callbacks is:

CallbackPurpose
matchCallback to test if the stage is required by this request.
rewriteHandler callback to rewrite or redirect the request.
openOpen a new request instance for the handler. Corresponds to the queue open callback.
startHandler callback to start the request.
incomingAccept a packet of incoming data. Corresponds to the receive queue put callback.
incomingServiceImplement deferred processing on incoming data. callback.
readyHandler callback once the request is fully parsed and all incoming data has been received.
outgoingAccept a packet of outgoing data.
outgoingServiceImplement deferred processing for outgoing data. Used primarily for non-blocking output and flow control.
writableHandler callback to supply the outgoing pipeline with more data.
closeClose the stage for the current request. Corresponds to the queue close callback.

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.

Packets may contain prefix and suffix data to emit before and after the data packet.

Handler Data Flow

A handler will receive request body data via its incoming 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 packet with flags set to HTTP_PACKET_END. 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, so it may not need to have an outgoing service callback. The handler may generate data by httpWrite which buffers output and creates packets as required that may be queued handlers output service queue. These packets will be flushed downstream if the queue becomes full or if httpFlush 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 downstream queue is full by comparing HttpQueue.count with HttpQueue.max.

Handlers can also use the lower-level httpWriteBlock API which provides options for non-blocking writing.

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.

Rewriting a Request — rewrite

Once the handler is selected, it is given the opportunity to rewrite the request and the routing process is restarted with the rewritten request.

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 — incoming

If the request has body data (POST|PUT), the input data will be passed in packets to the handlers incoming 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:

HttpStream *stream = q->stream;
httpSetStatus(stream, 200);
httpSetHeader(stream, "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 stream->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(stream, 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(stream, 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(stream, 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). The httpWrite routine will automatically flush data as required. When all the data has been written, call: httpFinalizeOutput. This finalizes the output response data by sending an empty packet to the network connector which signifies the completion of the request.

httpWrite(stream, "Hello World\n");
httpFinalizeOutput(stream);

You can call httpFinalize if you have generated all the response output and completed all processing. This implies httpFinalizeOutput and may then discard any remaining input. Alternatively, call httpFinalizeInput when you have processed all input and httpFinalizeOutput when you have generated all output.

Generating Responses without Blocking

If a handler must generate a lot of response data, it should take care not to exceed the maximum downstream 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 writable 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->stream);
    } else {
        httpWriteString(q, getMoreData(q));
    }
}

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

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. To ensure output data has been fully written to the network, use httpFlushQueue(stream->writeq, HTTP_BLOCK).

Response Paradigms

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() callbacks and may 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 httpFinalizeOutput routine should be called when the output is fully generated.

Thread Safety

Most Appweb APIs are not thread-safe. To utilize multiple threads, Appweb serializes request activity on an event dispatcher that is used per request or connection. Appweb generally 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 threads via Appweb dispatchers.

Please read Thread Safety for details about how to communicate from foreign threads.

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.

steal the socket handle from Appweb and assume responsibility for the connection via: httpStealSocketHandle. This terminates the request and connection, but preserves the open socket handle. This is useful for upgrading from HTTP to a custom protocols after an initial HTTP connection is established.

-->

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.

Handler and Queue State

Sometimes a handler needs to store state information that can be retained and preserved during a garbage collection cycle. Appweb defines several fields that can be used by handlers to refer to state memory blocks. These fields must refer to managed memory — i.e. memory that has been allocated by the MPR APIs. During a garbage collection cycle, these fields will be automatically "marked" as being in-use.

The HttpStream.data field is available for use by handlers or applications. The HttpStream.reqData is available for use by web frameworks and the HttpQueue.queueData field is available for use by handlers or stages.

Appweb defines two fields that can be used to store un-managed memory references: HttpStream.staticData and HttpQueue.staticData. Use these to store references to memory allocated by malloc.

See: Appweb Memory for details about memory allocation.

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) {
        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
HttpStream.erroris set whenever there is an error with the request. i.e. httpError is called.
HttpStream.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.finalizedInputis set by the handler when all request body data has been received, 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.

Blocking

Handlers may only block in their open, close, ready, start and writable callbacks. Handlers may never block in any other callback. Filters and connectors must never block in any of their callbacks.

If a handler blocks by calling httpWrite, or any other blocking API, it will consume a worker thread. 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(MPR_YIELD_STICK) 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. When the sleeping API returns, they should call mprResetYield.

Blocking Rules

Blocking Recommendations

Recommended Paradigms

More Info

See the simple-handler for a working handler example.

Packages

Here are links to some third party Appweb extensions

© Embedthis Software. All rights reserved.