Woomera is a Dart package for implementing Web servers.
It is used to create server-side Dart programs that listens for HTTP requests and respond to them with HTTP responses.
A Web server is simple in theory, but in practice it quickly gets complicated and difficult to maintain. Especially when there are many different types of HTTP requests to process, different errors to detect and state needs to be maintained between the HTTP requests. This package aims to reduce that complexity.
Main features include:
-
URL pattern matching inspired by the Sinatra Web framework. This allows the HTTP request paths to be easily specified and different segments of the path to be used as parameters.
-
Exception handling mechanism to handle all uncaught and unexpected exceptions. This ensures the Web application can always generate a user-friendly error page, instead of sometimes producing unexpected results when an exception was not caught. This is especially useful when using third-party packages that might throw undocumented exceptions. Error handling is simplified and the Web application is more robust and reliable.
-
Session management using cookies or URL rewriting. The HTTP protocol does not maintain state between HTTP requests. This framework includes a mechanism for maintaining state. For example, it can be used to remember the user's account after they have signed in. URL rewriting works if cookies have been disabled in the browser (though this is rare these days).
-
Responses can be buffered, and sent as the HTTP response only when it is complete. Therefore, if an error occurs the user won't see a partially generated page.
-
Responses can be generated from a stream of data.
-
Pipelines allow request handlers to be invoked in the desired order. Multiple error handlers are supported. Requests can be arranged to be handled by multiple request handlers. For example, the first request handler can log the request and the second request handler perform the actual processing.
-
Features for testing the Web application without using a Web browser. This does not replace testing with a real Web browser, but runs faster than controlling a Web browser using WebDriver or Selenium Remote Control.
-
Can be statically compiled. Annotations are also defined if you want to dynamically identify the handler methods.
The following is a tutorial which provides an overview the main features of the package. For details about the package and its advanced features, please see the API documentation.
This package is supported on all platforms where "dart:io" is supported.
This is a basic Web server that has two request handlers for handling two types of URI requests. And it defines one server exception handler.
import 'dart:async';
import 'dart:io';
import 'package:woomera/woomera.dart';
Future<void> main() async {
// Create the server with one pipeline
final ws = Server()
..bindAddress = InternetAddress.anyIPv6
..bindPort = 1024
..exceptionHandler = myExceptionHandler
..pipelines.add(ServerPipeline()
..get('~/', handleTopLevel)
..get('~/:greeting', handleGreeting));
// Run the server
await ws.run();
}
@Handles.get('~/')
Future<Response> handleTopLevel(Request req) async {
final resp = ResponseBuffered(ContentType.html);
final helloUrl = req.rewriteUrl('~/Hello');
final gDayUrl = req.rewriteUrl("~/G'day");
resp.write('''
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Woomera Tutorial</title>
</head>
<body>
<h1>Woomera Tutorial</h1>
<ul>
<li><a href="${HEsc.attr(helloUrl)}">Hello</a></li>
<li><a href="${HEsc.attr(gDayUrl)}">Good day</a></li>
</ul>
</body>
</html>
''');
return resp;
}
@Handles.get('~/:greeting')
Future<Response> handleGreeting(Request req) async {
final greeting = req.pathParams['greeting'];
var name = req.queryParams['name'];
name = (name.isEmpty) ? 'world' : name;
final resp = ResponseBuffered(ContentType.html);
final homeUrl = req.rewriteUrl('~/');
resp.write('''
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Woomera Tutorial</title>
</head>
<body>
<h1>${HEsc.text(greeting)} ${HEsc.text(name)}!</h1>
<p><a href="${HEsc.attr(homeUrl)}">Home</a></p>
</body>
</html>
''');
return resp;
}
@ServerExceptionHandler()
Future<Response> myExceptionHandler(
Request req, Object ex, StackTrace st) async {
int status;
String message;
if (ex is NotFoundException) {
status =
ex.resourceExists ? HttpStatus.methodNotAllowed : HttpStatus.notFound;
message = 'Sorry, the page you were looking for could not be found.';
} else {
status = HttpStatus.internalServerError;
message = 'Sorry, an internal error occurred.';
print('Exception: $ex');
}
return ResponseBuffered(ContentType.html)
..status = status
..write('''
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Error</title>
</head>
<body>
<h1>Woomera Tutorial: Error</h1>
<p>${HEsc.text(message)}</p>
</body>
</html>
''');
}
Any program that uses the framework must first import the package:
import 'package:woomera/woomera.dart';
The Web server needs to create and configure a [Server] object. And
then invoke its asynchronous run
method which causes it to listen
and process HTTP requests.
This is the smallest possible server. It listens on port 80 of the IPv4 loopback address (127.0.0.1) for HTTP requests. But will respond to every HTTP request with a HTTP 401 Not found.
final ws = Server();
await ws.run();
The interface and port it listens on is configured by the bindPort and bindAddress properties.
This is a server that listens on port 1024 of all network interfaces (i.e. any IP address, both IPv4 and IPv6) of the host machine.
final ws = Server()
..bindAddress = InternetAddress.anyIPv6
..bindPort = 1024;
Typically the application Web server is deployed behind a reverse proxy. If the reverse proxy is running on the same host, restricting access to only the IPv4 loopback address is desirable; but usually the port number needs to be changed to avoid conflicts and issues with permissions.
The code to process HTTP requests is implemented in request handler functions.
A server is organised as an ordered list of pipelines. And each of those pipelines has an ordered list of rules and request handlers pairs.
The pipelines are represented by instances of the [ServerPipeline] class. The class has methods for registering rules with request handlers. Rules are made up of a HTTP method and a pattern. The register method (which is passed the HTTP method as a string) can be used, but there are convenient methods named after the standard HTTP methods too.
For example, the following creates a pipeline and registers two rules on it.
ServerPipeline()
..get('~/', handleTopLevel)
..get('~/:greeting', handleGreeting)
Both rules are for the HTTP GET method.
The pattern is represented by a string starting with "~/" and has path segments that will be matched against the request URI's path.
On the server object, the pipelines
member is a list of
ServerPipeline objects. So the standard Dart methods on lists can be
used to manage the pipelines.
In this example, the list add method is used to add the pipeline to the server.
final ws = Server()
..bindAddress = InternetAddress.anyIPv6
..bindPort = 1024
..pipelines.add(ServerPipeline()
..get('~/', handleTopLevel)
..get('~/:greeting', handleGreeting));
A HTTP method and a pattern is referred to as a "rule".
When a HTTP request is received a request handler is found, by searching for a rule that matches it.
The search is conducted in order. It examines the pipelines in order, and for each pipeline it examines each of its rules in order. If a rule matches, its request handler is invoked and the returned value used to produce the HTTP response.
Multiple pipelines is useful is some situations. They can be used to control the order in which rule matching is performed. They can be used to handle exceptions in different ways, which will be described later. And they can be used to group request handlers.
Instead of returning the response, a request handler could throw a [NoResponseProduced] exception. This is a special exception that tells the matching algorith to continue searching subsequent rules, and subsequent pipelines, for another match. This feature can be used to pre-preprocess requests; for example, to have a request handler that audits every HTTP request before letting a different request handler produce the response.
A rule matches the HTTP request if its HTTP method is the same as the request's HTTP method, and the pattern matches the path of the request URI. The type of segment in the pattern determines how it is matched to the segments in the URI. These are the types of segments found in a pattern:
-
A literal segment matches the exact same value (i.e. string equality).
-
A path parameter starts with a colon followed by the parameter name (e.g. ":greeting" or ":foo"). It matches exactly one segment at that position, and its value is assigned to the path parameter with that name.
-
A wildcard paramter is represented by "
*
". It matches has one or more segments. Those segments are assigned to the value of the path parameter with the special name of "*
".
The "" in the pattern is a reminder that a pattern is treated as a
relative or "internal" path. Typically, the pattern refers to the root
of the Web server. For example, the pattern "/foo/bar" will match the
URI http://localhost/foo/bar by default. This can be changed by
setting the server's basepath
member. For example, setting the
basepath to "/abc/def", will mean the pattern will match the URI
http://localhost/abc/def/foo/bar instead.
The example has two patterns:
-
The "~/" pattern matches the empty path (e.g. http://localhost:1024).
-
The "~/:greeting" matches any URI path with exactly one segment, assigning that segment to the value of the path parameter named "greeting". For example, it will match http://localhost:1024/Hello and sets greeting to "Hello". But it will not match "/", "/a/b" or "/a/b/c", since they don't have one segment in their path.
The order is important when registering rules to a pipeline, since
they are searched for in that order. For example, if a rule with the
pattern "/foo/:bar" is registered before "/foo/new", a request with
the URI path of "/foo/new" will always match the first rule (assigning
the value of "new" to the path parameter named "bar") and the second
rule will never be used (not unless the first request handler throws
a NoResponseProduced exception).
A request handler is a function (or static method) that is passed the request as a [Request] object and returns a Future to a [Response] object.
The example has two request handlers. The first one was registered with a rule so it handles HTTP requests for the root URI (e.g. http://localhost:1024). It generates a HTML page with hyperlinks to the other page.
The [ResponseBuffered] class is used to produce the response. It has a
write
method used to build up the body of the HTTP response. By
default, the status of the HTTP response is HTTP 200 OK.
Future<Response> handleTopLevel(Request req) async {
final resp = ResponseBuffered(ContentType.html);
final helloUrl = req.rewriteUrl('~/Hello');
final gDayUrl = req.rewriteUrl("~/G'day");
resp.write('''
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Woomera Tutorial</title>
</head>
<body>
<h1>Woomera Tutorial</h1>
<ul>
<li><a href="${HEsc.attr(helloUrl)}">Hello</a></li>
<li><a href="${HEsc.attr(gDayUrl)}">Good day</a></li>
</ul>
</body>
</html>
''');
return resp;
}
This example request handler shows the use of the
Request.rewriteUrl
to convert a local path (i.e. one starting with
"~/") to the actual path the server will be using. The full path of
the deployed Web server can be changed by just changing the
[Server.basePath] property: the behaviour of the patterns and the
values in the responses will both be changed.
This example also shows the use of the [HEsc] class to encode values
for HTML documents. Special characters (e.g. <
, >
and &
) will be
replaced by HTML entities.
The ResponseBufferd
is the commonly used type of Response. Other
responses are: [ResponseRedirect] to produce a HTTP 303 Redirect,
[ResponseNoContent] to produce a HTTP 204 No Content response
without a body and [ResponseStream] to produce the HTTP body from a
stream source.
The other request handler shows how path parameters can be used.
The [RequestParams.[]] operator on the request's pathParams
property
obtains the value corresponding to a named path parameter.
The pattern was "~/:greeting", so the first segment will be assigned to the path parameter named "greeting".
Future<Response> handleGreeting(Request req) async {
final greeting = req.pathParams['greeting'];
There are also query parameters which are accessed through the
request's queryParams
member.
var name = req.queryParams['name'];
name = (name.isEmpty) ? 'world' : name;
So if the request URI was http://localhost:1024/Hello?name=Remi then greeting will be assigned "Hello" and name will be assigned "Remi".
If a request handler throws an exception, it is passed to an exception handler. The exception handler should produce the response that gets sent back to the client.
There are three types of exception handlers a program can provide:
-
pipeline exception handlers can be registered per-pipeline. These are useful for generating different error responses, depending on which pipeline the matching request handler was registered to. For example, an API pipeline could have a pipeline exception handler that produces a JSON error response, while another pipeline has a pipeline exception handler that produces a HTML error response.
-
a server exception handler can be registered on the server. It handles exceptions thrown by a pipeline exception handler, or the original exception if there was no pipeline exception handler.
-
a server raw exception handler handles exceptions thrown by the server exception handler, when there is no server exception handler and in some other special situations.
The example has a server exception handler.
The server is configured with it.
final ws = Server()
..exceptionHandler = myExceptionHandler
The server exception handler is passed the Request as well as the exception object that was thrown and the stack trace of where it was thrown from.
Like a request handler it is expected to return a Future to the Response that will be used to produce the HTML response.
Future<Response> myExceptionHandler(
Request req, Object ex, StackTrace st) async {
...
}
The server exception handler will be invoked when any request handler throws an exception.
It will also be invoked when the framework cannot obtain a Response
from any of the request handlers. The exception will be a
NotFoundException
object and should result in the status of HTTP
404 Not Found.
This example treats all other exceptions as an internal error and response with a status of HTTP 500 Internal server error. A more useful server exception handler could generate different responses depending on the exceptioin.
int status;
String message;
if (ex is NotFoundException) {
status =
ex.resourceExists ? HttpStatus.methodNotAllowed : HttpStatus.notFound;
message = 'Sorry, the page you were looking for could not be found.';
} else {
status = HttpStatus.internalServerError;
message = 'Sorry, an internal error occurred.';
print('Exception: $ex');
}
The HTTP status is a property of the Response. The default is HTTP 200 OK. Depending on the exception, the server exception handler in the example sets it to either 404, 405 or 500.
return ResponseBuffered(ContentType.html)
..status = status
..write( ... );
}
-
Patterns are used for specifying which HTTP requests a request handler will process. When represented as a string, they look like
~/foo/bar/baz
or~/account/:varname/profile
. -
Paths are one component of a URL. There are two types of paths:
-
External paths which are values that can be used externally. For example,
/foo/bar/baz
and/account/24601/profile
. -
Internal paths are used internally in the code. They look similar to patterns, but every segment is a literal value. For example,
~/foo/bar/baz
and~/account/24601/profile
.
-
These different items are used in different places:
- Patterns are used in specifying rules to match request handlers.
- External paths appear in HTML that is used by the Web browser.
- Internal paths should be used to identify resources that
are implemented by a request handler. And they should be converted
into an external path using the
rewriteUrl
method on the Request.
You don't have to use internal paths. But it is recommended, because it forces the application to always invoke rewriteUrl before inserting a path into the response. Ensuring rewriteUrl is always used is important for two reasons:
-
when URL rewriting is used to preserve the state across different HTTP requests, rewriteUrl adds the state preserving query parameter. This is needed when using the session feature and the browser has cookies disabled; and
-
when the basePath of the server is set, rewriteUrl adds the base path to the external URL. For example, if the base path is set to "/api/v2", rewriting the internal path of "~/foo/bar" produces an external path of "/api/v2/foo/bar".
Since internal paths cannot be used by Web browsers, places where rewriteUrl didn't get invoked will be easily discovered during testing. Otherwise, the application could appear to be working correctly during testing, but will fail if the browser has cookies disabled.
The Request passed to request handlers can include three different types of parameters:
- path parameters;
- query parameters; and
- post parameters.
The post parameters is only populated if the HTTP request had a MIME type of "application/x-www-form-urlencoded". This occurs when a Web browser submits a HTTP POST request. If available, they are available through the postParams member of the Request. If they are not available, it is null.
Query parameters, obviously, are the query parameters from the request URL. They are available through the queryParams member of the Request.
The path parameters are extracted from the path of the URL being requested and are available through the pathParams member of the Request. They match the variable segments in the pattern. For example:
-
~/foo/bar/baz
is a pattern with no variable segments -
~/user/:id
is a pattern with one variable segment. The literal segments must match the corresponding path segment, and the path parameter named "id" will be set to the second segment from the path. -
~/user/:id/order/:orderNumber
is a pattern with two variable segments, resulting in two path parameters. -
~/product/*
contains a wildcard segment that will match zero or more segments in the URL path.
A pattern can also contain an optional segment. See the API documentation for more information.
This request handler that can be used to demonstrate the different types of parameters:
@Handles.get('~/demo/variable/:foo/bar/:baz')
@Handles.get('~/demo/wildcard/*')
Future<Response> handleParams(Request req) async {
final resp = ResponseBuffered(ContentType.html)
..write('''
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Woomera Tutorial</title>
</head>
<body>
<h1>Parameters</h1>
''');
// ignore: cascade_invocations
resp.write('<h2>Path parameters</h2>');
_dumpParam(req.pathParams, resp);
resp.write('<h2>Query parameters</h2>');
_dumpParam(req.queryParams, resp);
final _postParams = req.postParams;
if (_postParams != null) {
resp.write('<h2>POST parameters</h2>');
_dumpParam(_postParams, resp);
}
resp.write('''
</body>
</html>
''');
return resp;
}
void _dumpParam(RequestParams p, ResponseBuffered resp) {
final keys = p.keys;
if (keys.isNotEmpty) {
resp.write('<p>Number of keys: ${keys.length}</p>\n<dl>');
for (var k in keys) {
resp.write('<dt><code>${HEsc.text(k)}</code></dt><dd><ul>');
for (var v in p.values(k, raw: true)) {
resp.write('<li><code>${HEsc.text(v)}</code></li>');
}
resp.write('</ul></dd>');
}
resp.write('</dl>');
} else {
resp.write('<p>No parameters.</p>');
}
}
Here are a few URLs to try with the above example:
http://localhost:1024/demo/variable/aaa/bar/bbb
http://localhost:1024/demo/variable/aaa/bar/
http://localhost:1024/demo/variable/aaa/bar/ccc?x=ddd&y=eee&x=fff
http://localhost:1024/demo/wildcard/a/b/c
Parameters can have multiple values. For example, check boxes on a form will result in one named parameter with zero or more values (one for each checked check box). There can be multiple query parameters with the same name. Patterns can also be written with multiple variable segments with the same name.
The RequestParams
class can be thought of as a Map, where the keys
are the names of the parameters which maps into a List of values. If
there is only one value, there is still a list: a list containing only
one value.
The names of all the available parameters can be obtained using the keys method.
for (final k in req.queryParams.keys) {
print('Got a query parameter named: $k');
}
All the values for a given key can be obtained using the values method.
for (final k in req.queryParams.keys) {
final vList = req.queryParams.values(k);
for (final v in vList) {
print('$k = $v');
}
}
If your request handler is expecting only one value, the square-bracket operator can be used to retrieve a single value instead of a list.
final t = req.queryParams['title'];
The methods described above for retrieving value(s) returns a cleaned up version of the value which:
- removes all leading whitespaces;
- removes all trailing whitespace;
- collapses multiple consecutive whitespaces one whitespace; and
- convert all whitespace characters into the space character.
To obtain the unprocessed value, set raw to true with the values method:
req.queryParams.values('category', raw: true);
To make a robust application, do not make any assumptions about what parameters may or may not be present: check everything and fail gracefully. The parameters might be different from what is expected because of programming errors, misuse or (worst case, but very important to deal with) the application is under malicious attack.
If a parameter is missing, the square bracket operator returns an empty string, and the values method returns an empty list when it is returning processed values. In raw mode, the values method returns null if the value does not exist: which is the only way to detect the difference between the presence of a blank/empty parameter versus the absence of the parameter.
An application might be designed to expect exactly one instance of a parameter, but a malicious client might try to send two or more values to it. The square bracket operator, which is used when only one value is expected, will return the empty string if the multiple copies of the parameter exist (even if the values are not empty strings).
A server has a collection of rules. If a rule matches the HTTP request (i.e. matches the HTTP method and the request path), then its response handler is invoked. The order in which rules are examined, to see if they match the HTTP request, is determined by pipelines.
Web applications do not have to deal with pipelines if they don't want to. Applications only need to deal with pipelines if they want more control over how and when rules are matched (and consequently which request handlers are invoked).
The rules in a server are organised by the pipelines. A server has an ordered list of pipelines. Each pipeline separates out its rules by the HTTP method. Within each HTTP method, the rules are stored in an ordered list.
When a HTTP request arrives, it is tested against each rule until a match is found. Each pipeline is checked in order, and within the pipeline the rules are checked in order. If no match is found, after checking all the pipelines, then a NotFoundException is thrown.
Therefore, rules in earlier pipelines are checked first and within a pipeline earlier rules are checked first.
If a request handler returns null, the testing continues with the subsequent rules. So it is possible to design an application where a request is processed by multiple request handers, as long as the rules appear in the correct order.
Using multiple pipelines is one way of controlling the order in which rules are tested. The other way is to register the rules in a particular order.
The other useful feature of pipelines is each pipeline can have its own pipeline exception handler, in addition to the server's exception handler. This is useful if exceptions from different sets of request handlers should be handled differently. For example, there could be an exception handler that generates a HTML error page and another that generates an error in JSON.
Every pipeline has a name. The default name is the emptty string, but a different name can be provided to the ServerPipeline constructor.
final p1 = ServerPipeline('api');
final p2 = ServerPipeline('main');
Named pipelines are needed if using multiple pipelines with annotations, since they identify which pipeline to associate a request handler with.
All the exceptions thrown by the framework are subclasses
of the WoomeraException
class.
-
The
NotFoundException
is thrown when a matching rule is not found. The exception handler should produce a "page not found" error page with a HTTP response status of eitherHttpStatus.notFound
orHttpStatus.methodNotAllowed
depending on the value of its "found" member. -
The
ExceptionHandlerException
is a wrapper that is thrown if an application provided exception handler throws an exception while it is processing another exception.
See the package's documentation for the other exceptions. Most of them are in response to a malformed or potentially malicious HTTP request.
These exceptions, along with all exceptions thrown by the application's handlers, are processed according to the exception handling process. The application can provide its own high-level and low-level exception handlers for customizing this process.
High-level exception handlers are a type of handler used to process exceptions that are raised. They are passed the request and the exception, and are expected to generate a Response. The exception handler should create a response that is as an error page for the client.
There can be at most one server exception handler. Servers should provide one, because it is used to indicate a page is not found.
@ServerExceptionHandler()
Future<Response> myExceptionHandler(Request req
Object exception, StackTrace st) async {
var resp = ResponseBuffered(ContentType.html);
resp.write('''
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Error</title>
</head>
<body>
<h1>Error</h1>
<p>Sorry, an error occured: ${HEsc.text(exception.toString())}</p>
</body>
</html>
''');
return resp;
}
Each pipeline can also have its own exception handler.
final p1 = ServerPipeline()
..exceptionHandler = myExceptionHandler1;
final p2 = ServerPipeline('myCustomPipeline')
..exceptionHandler = myExceptionHandler2;
@Handles.pipelineExceptions()
Future<Response> myExceptionHandler1(Request req
Object exception, StackTrace st) async {
// for the default pipeline
}
@Handles.pipelineExceptions(pipeline: 'myCustomPipeline')
Future<Response> myExceptionHandler2(Request req
Object exception, StackTrace st) async {
// for the pipeline named "myCustomPipeline"
}
Different exception handlers for different pipelines can be used to handle exceptions differently. For example, one pipeline could be used for a RESTful API and its exception handler produces a XML or JSON error response; and other pipeline's exception handler could produce a HTML error page.
In addition to the high-level exception handlers, a low-level raw exception handler can be associated with the server.
It is called a "low-level" or "raw" exception handler, because it needs to process a Dart HttpRequest and generate a HTTP response without the aid of the Woomera classes.
@ServerExceptionHandlerRaw()
Future<void> myLowLevelExceptionHandler(
HttpRequest rawRequest, String requestId, Object ex, StackTrace st) async {
final resp = rawRequest.response;
resp
..statusCode = HttpStatus.internalServerError
..headers.contentType = ContentType.html
..write('''<!DOCTYPE html>
<html>
...
</html>
''');
await resp.close();
}
It is triggered in rare situations where a high-level exception handler cannot be used.
The process of dealing with exceptions depends on where the initial exception was thrown from, and what custom exception handlers the application has provided.
-
If an exception occurs inside a request handler method (and has not been caught and processed within the handler) it is passed to the exception handler attached to the pipeline: the pipeline with the rule that invoked the request handler method.
-
If no exception handler was attached to the pipeline, the high-level exception handler on the server is used. Exceptions that occur outside of any handler or pipeline (commonly when a matching handler is not found) are also handled by the server's high-level exception handler.
-
If no custom high-level exception handler was attached to the server, a built-in default high-level exception handler is used.
If one of those exception handlers throws an exception, the exception it was processing is wrapped in an ExceptionHandlerException, which is then passed to the next handler in the process.
It is recommended to provide at least the high-level server exception handler, since the default exception handler just produces a plain text response that purely functional and not pretty. It also handles the page not found errors.
The request handlers and exception handlers must return a Future that returns a Response object. The Response class is an abstract class and three subclasses of it have been defined in the package:
- ResponseBuffered
- ResponseStream
- ResponseRedirect
This is used to write the contents of the response into a buffer, which is used to create the HTTP response after the request hander returns.
The HTTP response is only created after the request handler finishes. If an error occurs while generating the response, the partially created ResponseBuffered object can be discarded and a new response created. The new response can be created in the response handler or in an exception handler. The new response can show an error page, instead of trying to output an error message at the end of a partially generated page.
This is used to generate a HTTP redirect, which tells the client to go to a different URL.
This is used to produce the contents of the response from a stream.
With all three types of responses, the application can:
- Set the HTTP status code;
- Create HTTP headers; and/or
- Create or delete cookies.
The package includes a request handler for serving up files and directories from the local disk. It can be used to serve static files for all or some of the Web server (for example, the images and stylesheets).
See the API documentation for the StaticFiles class.
The package includes a request handler for proxying requests to a different server. A request for one URI is converted into a target URI and the request is forward to it. The response from the target URI is used as the response.
See the API documentation for the Proxy class.
The framework provides a mechanism to manage sessions. HTTP is a stateless protocol, but sessions have been added to support the tracking of state.
A session can be created and attached to a HTTP request. That session will be attached to subsequent Request objects. The framework handles the preserving and restoration of the session using either session cookies or URL rewriting. The application can terminate a session, or they will automatically terminate after a nominated timeout period after they were last used.
Woomera uses the Logging package. See the Woomera library API documentation for the logger names.
In general, a logging level of "INFO" should produce no logging entries, unless there is a problem. Setting the "woomera.request" logger to "FINE" logs the URL of every HTTP request, which might be useful for testing.
Maintaining the code for a large Web server gets more complicated as the number of pipelines, request handlers and exception handlers grows. Code changes need to occur in two places: where the function is defined and where it is registered with a pipeline or server. For example, it is easy to accidentally create a request handler function and forget to register it against a pipeline.
Annotations can be used to help manage the code. The server and pipelines can be automatically generated from the annotations.
See the woomera_server_gen package for one way annotations can be used.
Please report bugs by opening an issue in GitHub.