woomera 4.5.0

Woomera #

Introduction #

Woomera is a Dart package for implementing Web servers.

It is used to create server-side Dart programs that function as a Web server. A Web server simply listens for HTTP requests and respond to them with HTTP responses. But it quickly gets complicated (and difficult to maintain) when there are many different types of HTTP requests to process, different errors to detect and state needs to be maintained between 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 uncaught and unexpected exceptions. This ensures the Web application can always generate a user-friendly error page, instead of them seeing an internal error message. 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 even if cookies have been disabled in the browser.

  • Responses can be are 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.

  • A testing feature to test 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.

Note: This version requires Dart 2. Please use version "<3.0.0" if running Dart 1.

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

Tutorial #

1. A basic Web server #

1.1. Overview #

This is a basic Web server that serves up two HTML pages.

import 'dart:async';
import 'dart:io';
import 'package:woomera/woomera.dart';

Future<void> main() async {
  final ws = Server.fromAnnotations()
    ..bindAddress = InternetAddress.anyIPv6
    ..bindPort = 1024;

  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;
}

1.2. Importing the package #

Any program that uses the framework must first import the package:

import 'package:woomera/woomera.dart';

1.3. Creating the server #

A Server object must be created and configured for the TCP/IP address and port it will listen for HTTP requests on.

final ws = Server.fromAnnotations()
  ..bindAddress = InternetAddress.anyIPv6
  ..bindPort = 1024;

For this example, it sets it to InternetAddress.ANY_IP_V6, so the service is listening to connections on any interface (i.e. both loopback and public addresses).

When using InternetAddress.ANY_IP_V6, the optional v6Only member controls whether IPv4 addresses are included or not. It defaults to false, meaning it listens on both any IPv4 and any IPv6 address. If it is true, it only listens on any IPv6 addresses, and ignore all IPv4 addresses. To make it easy to connect to, this example uses ANY_IP_V6 and leaves v6Only set to false.

Often, when deployed in production, the service may be behind a reverse Web proxy (e.g. Apache or Nginx). The default bind address is InternetAddress.LOOPBACK_IP_V4 can be used to for this: it means only listens for connections on 127.0.0.1 (i.e. only clients from the same host can connect to it). Note: when configuring the reverse proxy, use 127.0.0.1. Avoid configuring it with "localhost", because on some systems that causes it to first try the IPv6 localhost address (::1) before trying the IPv4 localhost address: it will work, but will be less efficient.

A port number 1024 or greater should be used, because the lower port numbers are require special permission to use.

1.4 Annotating request handlers #

When a server is created using Server.fromAnnotations, it scans the program for top-level functions and static methods. Those with Handles annotations are used to create rules for handling HTTP requests. When processing a HTTP request, if the rule matches the request, the request handler is invoked.

A request handler is a function with a Request parameter and returns a Future to a Response.

This example has two request handler functions. They have these two annotations on them:

@Handles.get('~/')
...

@Handles.get('~/:greeting')
...

A Handles object indicates what HTTP method (e.g. GET, POST, PUT) and the pattern that is matched against the request URL path. The request handlers in this example process HTTP GET requests.

The first pattern, "~/", corresponds to the root path. That is, this request handler will match the HTTP request for "http://localhost:1024/".

The second pattern, "~/:greeting", has a segment with a variable called "greeting". For example, this request handler will match the HTTP request for "http://localhost:1024/Hello" and set the path variable named "greeting" to "Hello".

See the API documentation for more details about patterns. They consist of segments separated by a slash ("/") and the first segment must always be a tilde ("~"). There are several types of path segments: the most commonly used are literal segments and variable segments. Literal segments which must match exactly the path segment from the request URL's path. Variable segments match any path segment, and the value is made available to the request handler to use.

1.5. Running the server #

After configuring the Server, start it using its run method and it will start listening for HTTP requests.

The run method returns a Future that completes when/if the Web server finishes running, but normally a Web server is designed to run forever without stopping.

await ws.run();

When a HTTP request arrives, the request handler its method and path matches will be invoked.

1.6. Creating a Response #

1.6.1 Generating a buffered response #

The ResponseBuffered is commonly used to generate HTML pages_ for the HTTP response. It acts as a buffer where the contents is appended to it using the write method.

The different Response classes will be described later.

1.6.2 Escaping HTML attribute values #

The handleTopLevel request handler simply generates a static HTML page.

The HEsc.attr static method is used to escape values used inside HTML attributes. It will ensure any ampersands, less than signs, greater than signs, single quotes and double quotes are escaped.

1.6.3 Rewriting internal paths to produce external paths #

The two URLs are produced using the rewriteUrl method of the Request object. That takes an internal path and produces an external path suitable for the Web browser to use. The distinction between these will be described later, but for now the rewriteUrl method coverts an internal path to an external path.

This code:

final gDayUrl = req.rewriteUrl("~/G'day");

resp.write('<li><a href="${HEsc.attr(gDayUrl)}">Good day</a></li>');

Results in the HTML response containing:

<li><a href="/G&apos;day">Good day</a></li>

1.7 Parameter handling #

The handleGreeting request handler shows how parameters from the HTTP request are passed into the request handler via the Request.

The pathParams member contains the parameters from the HTTP request's URL's path, according to the pattern. Since the pattern was "~/:greeting", the path parameter named "greeting" will be set to the first segment in the path.

The queryParams member contains the parameters from the HTTP request's URL's query parameters.

For example, if the request URL was "http://localhost/foo?abc=def&xyz=uvw", then the path parameter named "greeting" will be set to "foo"; and the query parameters will contain a parameter named "abc" with the value of "def" and a parameter named "xyz" with the value of "def".

For retrieving parameters, the [] operator is a high-level method that always returns a single string whose value is trimmed of any leading and trailing whitespace. If the parameter does not exist, it returns the empty string. The values method provides a lower-level access to the parameters.

1.8 Escaping HTML text #

The response produced by handleGreeting uses HEsc.text to escape values used inside HTML text. It is similar to the HEsc.attr, but does not escape single quotes and double quotes.

If the value wasn't escaped, then this URL would produce the wrong HTML: http://localhost:1024/Hello%20%26%20Goodbye.

There is also HEsc.lines, which is similar to HEsc.text but also converts any new-lines into <br> tags.

1.9 Exception handler #

Visiting a URL like http://localhost:1024/nosuchpage/foo and the basic built-in error page appears. To customize the error page, a custom exception handler is used.

An exception handler processes any exceptions that are raised: either by one of the request handlers or by the framework.

It is similar to a request handler, because it is a method that returns a Response object. But it is different, because it is also passed the exception and sometimes a stack trace.

Here is an example of a server exception handler:

@Handles.exceptions()
Future<Response> myExceptionHandler(
    Request req, Object ex, StackTrace st) async {
  int status;
  String message;

  if (ex is NotFoundException) {
    status = (ex.found == NotFoundException.foundNothing)
        ? 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 occured.';
    print('Exception: $ex');
  }

  final resp = 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>
''');

  return resp;
}

This exception handler customizes the error page when the NotFoundException is encountered: which is raised by the framework when none of the rules matched the request. Notice that it reports a different HTTP status code if no rules for the HTTP request method could be found (405 method not allowed), versus when some rules for the method exist but their pattern did not match the requested path (404 not found).

2. Patterns vs internal paths vs external paths #

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

2.1 Why use internal paths? #

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.

3. Parameters #

3.1 Types of parameters #

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);

  resp.write('<h2>POST parameters</h2>');
  _dumpParam(req.postParams, resp);

  resp.write('''
  </body>
</html>
''');

  return resp;
}

void _dumpParam(RequestParams p, ResponseBuffered resp) {
  if (p != null) {
    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>');
    }
  } else {
    resp.write('<p>Not available.</p>');
  }
}

Here are a few URLs to try:

3.2. Retrieving parameters #

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'];

3.3 Raw vs processed values #

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);

3.4 Expect the unexpected #

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

4. Pipelines #

4.1 The default pipeline #

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

In the above example, the default pipeline was used. The default pipeline is created if the _Server.fromAnnotations` constructor is used with no parameters. The annotations define rules for the default pipeline, if no pipeline parameter is passed to the Handles constructor.

@Handles.get('~/foo/bar')
...

final server = Server.fromAnnotations();

4.2 Behavour of pipelines #

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 specify a priority in the Handles annotations, or to manually create rules and append them to the pipeline. Rules created from annotations are sorted by their priority first and then by their pattern.

The other useful feature of pipelines is each pipeline can have its own 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.

4.3 Creating multiple pipelines #

Multiple pipelines can be created by providing a list of pipeline names to the Server.fromAnnotations constructor. To associate an annotation to a pipeline, specify the pipeline name as a parameter to the Handles constructor.

@Handles.get('~/v1/account', pipeline: 'api')
...

@Handles.get('~/welcome') // for the default pipeline
...

@Handles.get('~/foo, pipeline: 'third')
...


final server = Server.fromAnnotations(['api', Pipeline.defaultName, 'third']);

Note: if a list of names is provided to the Server.fromAnnotations constructor, the default pipeline is not created unless its name is explicitly one of the names in the list.

4.4 Manually creating pipelines and rules #

Pipelines and rules manually, without using annotations. In version 4.3.0 and earlier, that was the only way.

This approach is still possible, but using annotations leads to more easily managed code. The manual method may be deprecated in a future version.

5. Exceptions #

5.1. Standard exceptions #

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 either HttpStatus.notFound or HttpStatus.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.

5.2 High-level exception handlers #

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.

5.2.1 Server exception handler #

There can be at most one server exception handler. Servers should provide one, because it is used to indicate a page is not found.

@Handles.exceptions()
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;
}
5.2.2 Pipeline exception handler #

Each pipeline can also have its own exception handler.

@Handles.pipelineExceptions()
Future<Response> myExceptionHandler(Request req
    Object exception, StackTrace st) async {
	// for the default pipeline
}

@Handles.pipelineExceptions(pipeline: 'myCustomPipeline')
Future<Response> myExceptionHandler(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.

5.3 Low-level exception handling #

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.

@Handles.rawExceptions()
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.

5.4 Exception handling process #

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.

6. Responses #

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

6.1. ResponseBuffered #

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.

6.2. ResponseRedirect #

This is used to generate a HTTP redirect, which tells the client to go to a different URL.

6.3. ResponseStream #

This is used to produce the contents of the response from a stream.

6.4. Common features #

With all three types of responses, the application can:

  • Set the HTTP status code;
  • Create HTTP headers; and/or
  • Create or delete cookies.

6.5 Common handlers provided #

6.5.1. Static file handler #

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.

6.5.2. Proxy handler #

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.

7. Sessions #

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.

8. Logging #

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.

9. References #

Changelog #

4.5.0 #

  • Added the use of annotations to create exception handlers.

4.4.0 #

  • Added the use of annotations to create rules on pipelines.

4.3.1 #

  • Code clean up to satisfy pana 0.13.2 health checks.

4.3.0 #

  • Include query parameters in URL of proxy requests.
  • Added support for a low-level exception handler.
  • Added headerAddDate method for adding headers with dates.
  • Automatically add Content-Length header when using ResponseBuffered.
  • Made settings headers in the Response case-independent.

4.2.0 #

  • Removed warning when redirecting to an absolute path/URL.
  • Updated dependencies to allow uuid v2.0.1 and test v1.6.3 to be used.

4.1.0 #

  • Support for using static file handler with reverse proxies on non-standard ports.

4.0.1 #

  • Fixed content-type for redirections.
  • Fixed bug with redirection URL for directories with static files.

4.0.0 #

  • Workaround for bug in Dart 2.1.x which prevents cookies from being deleted.
  • Merged in changes from v2.2.2.
  • Added proxy handler.
  • Simulation mechanism for testing servers.
  • Added external path to internal path conversion method.

3.0.1 #

  • Fixed problem with publishing documentation on pub.dartlang.org.

3.0.0 #

  • Updated the upper bound of the SDK constraint to <3.0.0.
  • Changed names to use new Dart 2 names.

2.2.2 #

  • Responds with HTTP 400 Bad Request if URL has malformed percent encodings.
  • Change logging level for FormatExceptions when parsing query/POST params.

2.2.1 #

  • This version runs under Dart 1.
  • Updated dependencies to allow for Dart 2 compatible versions to be used.

2.2.0 #

  • Changed RequestFactory to return FutureOr
  • Added release method on Request class to perform cleanup operations.
  • Deprecated requestFactory: renamed to requestCreator.

2.1.1 #

  • Included Length, Last-Modified, and Date HTTP headers for StaticFiles.

2.1.0 #

  • Added ability to retrieve the number of active sessions.
  • Added access to creation time for sessions.
  • Added expiry time for sessions.
  • Stopping a server also terminates any sessions.

2.0.0 #

  • Code made sound to support Dart strong mode.
  • Removed arbitrary properties from Request and Session: use subtypes instead.
  • Changed default bindAddress from LOOPBACK_IP_V6 to LOOPBACK_IP_V4.
  • Added convenience methods for registering PUT, PATCH, DELETE and HEAD handlers.
  • Added coverage tests.

1.0.5 #

  • Upgraded version dependency on uuid package.

1.0.4 #

2016-09-29

  • Fixed bug with parallel processing of HTTP requests.

1.0.3 #

2016-05-11

  • Fixed potential issue with URL rewriting in Chrome with GET forms.

1.0.2 #

2016-05-06

  • Improved exception catching in request processing loop.

1.0.1 #

2016-04-28

  • Fixed homepage URL.

1.0.0 #

2016-04-23

  • Initial release.

example/example.dart

/// Woomera demonstration Web Server.
///
/// This program runs a Web server to demonstrate the basic features of the
/// Woomera framework.
///
/// This program runs a single HTTP Web server (on port 1024).
///
/// Copyright (c) 2019, Hoylen Sue. All rights reserved. Use of this source code
/// is governed by a BSD-style license that can be found in the LICENSE file.
//----------------------------------------------------------------

import 'dart:async';
import 'dart:convert' show json;
import 'dart:io' show ContentType, HttpStatus, InternetAddress, HttpRequest;
import 'dart:math';

import 'package:logging/logging.dart';

import 'package:woomera/woomera.dart';

//================================================================
// Global constants

// Port server will listen on

const int port = 1024;

// Internal paths for the different resources that process HTTP GET and POST
// requests.
//
// Woomera uses internal paths, which are strings that always start with "~/".
// They need to be converted into real URLs when they are served to clients
// (e.g. when included as hyperlinks on HTML pages), by calling "rewriteURL".
//
// Constants are used for these so that the same value is used throughout the
// application if the values are changed (i.e. so the link URL always matches
// the path to the handler).
//
// The various parameter names are also defined as constants, so the same value
// is used in both the URL/form and when it is processed.

// For the general example showing path parameters

const String testPattern = '~/example/:foo/:bar/baz';
//const String _uParamFoo = 'foo';
//const String _uParamBar = 'bar';
//const testPattern2 = '~/example/:$_uParamFoo/:$_uParamBar/baz';

// For the POST request example

const String iPathFormHandler = '~/welcome';
const String _pParamName = 'personName';

// For the exception throwing example

const String iPathExceptionGenerator = '~/throw-exception';
const String _qParamProcessedBy = 'for';

//================================================================
// Globals

/// Application logger.

Logger log = Logger('app');
Logger simLog = Logger('simulation');

//================================================================
// Exceptions

enum HandledBy {
  pipelineExceptionHandler,
  serverExceptionHandler,
  defaultServerExceptionHandler
}

/// Exception that is thrown by [requestHandlerThatAlwaysThrowsException].
///
/// This is used to demonstrate how exceptions are processed by the
/// _pipeline exception handler_ and _server exception handler_.

class DemoException implements Exception {
  DemoException(this.handledBy);

  final HandledBy handledBy;
}

//================================================================
// Handlers
//
// These handlers are used for processing HTTP requests. They are all methods
// that take a [Request] and produces a future to a [Response].
//
// When setting up the server (in [_serverSetup]), rules are created to
// associate these handler methods with paths. The server uses the rules to
// handle the HTTP requests.

//----------------------------------------------------------------
/// Home page

@Handles.get('~/')
Future<Response> homePage(Request req) async {
  assert(req.method == 'GET');

  // The response can be built up by calling [write] multiple times on the
  // ResponseBuffered object. But for this simple page, the whole page is
  // produced with a single write.

  // Note the use of "req.ura" to convert an internal path (a string that starts
  // with "~/") into a URL, and to encode that URL so it is suitable for
  // inclusion in a HTML attribute. The method "ura" is a short way of using
  // `HEsc.attr(req.rewriteUrl(...))`.

  final resp = ResponseBuffered(ContentType.html)..write('''
<!DOCTYPE html>
<html lang="en">
<head>
      <title>Example</title>
</head>

<body>
      <header>
        <h1>Example</h1>
      </header>

      <h2>Request handlers</h2>
      
      <p>The framework finds a <em>request handler</em> to process the HTTP
      request. A match is found if the HTTP method is the same and the request
      URL's path matches the pattern.
      When a match is found, any path parameters (as defined by the pattern),
      query parameters and POST parameters are passed to the request handler.</p>
      
      <p>In the first two sets of links, this pattern will be matched:
       <code>${HEsc.text(testPattern)}</code></p>
       
      <ul>
        <li>
          Examples with path parameters:    
          <a href="${req.ura('~/example/first/second/baz')}">1</a>
          <a href="${req.ura('~/example/alpha/beta/baz')}">2</a>
          <a href="${req.ura('~/example/barComponentIsEmpty//baz')}">3</a>
        </li>
        <li>
          Example with query parameters:
          <a href="${req.ura('~/example/a/b/baz?alpha=1&beta=two&gamma=three')}">1</a>
          <a href="${req.ura('~/example/a/b/baz?delta=query++parameters&delta=are&delta=repeatable')}">2</a>
          <a href="${req.ura('~/example/a/b/baz?emptyString=')}">3</a>
        </li>
        <li>
          Example with form parameters:
          <form method="POST" action="${req.ura(iPathFormHandler)}">
            <input type="text" name="${HEsc.attr(_pParamName)}">
            <input type="submit">
          </form>
        </li>
      </ul>
    
      
      <h2>Exception handling</h2>
      
      <h3>Not found exceptions</h3>
      
      <p>If a <em>request handler</em> cannot be found, the framework throws a
      <em>NotFoundException</em>, which triggers the
      <em>server exception handler</em>.</p>
    
      <ul>
        <li><a href="${req.ura('~/no/such/page')}">
           Does not match any pattern</a></li>
         <li><a href="${req.ura('~/example/first/second/noMatch')}">
           A partial match is still not a match</a></li>
      </ul>
        
      <p>A <em>server exception handler</em> is defined using the
      <code>@Handles.serverException()</code>
      annotation on an <code>ExceptionHandler</code> function.</p>
      
      <h3>Other exceptions</h3>
      
      <p>If the <em>request handler</em> throws an exception, it triggers the
      <em>pipeline exception handler</em> from the pipeline the request
      handler was on. If there is no pipeline exception handler, or it also
      throws an exception, the <em>server exception handler</em> is
      triggered.</p>
      
      <ul>
        <li>
          <a href="${req.ura(iPathExceptionGenerator)}">Case 1</a>:
          Exception thrown by the request handler. It is processed by the
          pipeline exception handler.
        </li>
       <li>
          <a href="${req.ura('$iPathExceptionGenerator?$_qParamProcessedBy=server')}">
          Case 2</a>:
          Exception thrown by the request handler. It is processed by the
          pipeline exception handler, but it throws an exception. That second
          exception is processed by the server pipeline exception handler.
        </li>
        <li>
          <a href="${req.ura('$iPathExceptionGenerator?$_qParamProcessedBy=defaultServer')}">
          Case 3</a>:
          Exception thrown by the request handler. It is processed by the
          pipeline exception hander, but it throws an exception. That second
          exception is processed by the server exception handler, but it
          throws an exception. That third exception causes the built-in
          default server exception hander to run.
        </li>
      </ul>
      
      <p>A <em>pipeline exception handler</em> is defined using the
      <code>@Handles.exception()</code> annotation on an
      <code>ExceptionHandler</code> function. A <em>server exception handler</em>
      is defined using a <code>@Handles.serverException()</code> annotation
      on an <code>ExceptionHandler()</code> function.
      
      <p>There is also a <em>server raw exception handler</em> which is
      triggered in edge-case situations, when the normal server or
      pipeline exception handlers cannot be used. It is defined
      using the <code>@Handles.rawServerException()</code> annotation on an
      <code>ExceptionHandlerRaw</code> function. This example does not
      demonstrate the raw exception handler, since it is not easy to
      trigger it.</p>
      
      <h2>Other features</h2>

          <ul>
            <li>Request handler that produces a response from a stream:
              <a href="${req.ura('~/stream')}">no delay</a>,
              <a href="${req.ura('~/stream?milliseconds=200')}">with delay</a></li>
            <li><a href="${req.ura('~/json')}">JSON response instead of HTML</a></li>
          </ul>
      

      <footer>
        <p style="font-size: small">Demo of the
        <a style="text-decoration: none; color: inherit;"
           href="https://pub.dartlang.org/packages/woomera">Woomera Dart Package</a>
        </p>
      </footer>
</body>
</html>
''');

  // Note: the default status is HTTP 200 "OK", so it doesn't need to be changed

  return resp;
}

//----------------------------------------------------------------
/// Request handler that displays the parameters.
///
/// The [debugHandler] is a request handler that simply displays out all the
/// request parameters on the HTML page that is returned.

@Handles.get(testPattern)
Future<Response> myDebugHandler(Request req) async => debugHandler(req);

//----------------------------------------------------------------
/// Example request handler for a POST request
///
/// This handles the POST request when the form is submitted.

@Handles.post(iPathFormHandler)
Future<Response> dateCalcPostHandler(Request req) async {
  assert(req.method == 'POST');

  // Get the input values from the form
  //
  // HTTP requests with MIME type of "application/x-www-form-urlencoded"
  // (e.g. from a HTTP POST request for a HTML form) will populate the request's
  // postParams member.
  assert(req.postParams != null);

  // The input values can be retrieved as strings from postParams.

  var name = req.postParams[_pParamName];

  // The list access operator on postParams (pathParams and queryParams too)
  // cleans up values by collapsing multiple whitespaces into a single space,
  // and trimming whitespace from both ends. It always returns a string value
  // (i.e. it never returns null), so it returns an empty string if the value
  // does not exist. To tell the difference between a missing value and a value
  // that is the empty string (or only contains whitespace), use the
  // [RequestParams.values] method instead of the list access operator.
  // That [RequestParams.values] method can also be used to obtain the actual
  // value without any whitespace processing.

  assert(req.postParams['noSuchParameter'] == '');
  assert(req.postParams.values('noSuchParameter', raw: true).isEmpty);

  // Produce the response

  if (name.isEmpty) {
    name = 'world'; // default value if no name was provided
  }

  // Produce the response

  // Note: values that cannot be trusted should be escaped, in case they
  // contain reserved characters or malicious text. Text in HTML content can
  // be escaped by calling `HEsc.text`. Text in attributes can be escaped by
  // calling `HEsc.attr` (e.g. "... <a title="${HEsc.attr(value)} href=...").

  final resp = ResponseBuffered(ContentType.html)..write('''
<!DOCTYPE html>
<html lang="en">
<head>
  <title>Welcome</title>
</head>

<body>
  <header>
    <h1>Welcome</h1>
  </header>
    
  <p>Hello ${HEsc.text(name)}</p>

  <p><a href="${req.ura('~/')}">Home</a></p>
</body>
</html>
''');

  return resp;
}

//----------------------------------------------------------------
/// Request handler that generates an exception.
///
/// This is used to demonstrate the different exception handlers.

@Handles.get(iPathExceptionGenerator)
Future<Response> requestHandlerThatAlwaysThrowsException(Request req) async {
  final value = req.queryParams[_qParamProcessedBy];

  switch (value) {
    case '':
    case 'pipeline':
      throw DemoException(HandledBy.pipelineExceptionHandler);
      break;
    case 'server':
      throw DemoException(HandledBy.serverExceptionHandler);
      break;
    case 'defaultServer':
      throw DemoException(HandledBy.defaultServerExceptionHandler);
      break;
    default:
      throw FormatException('unsupported value: $value');
  }
}

//----------------------------------------------------------------
/// Example of a request handler that uses a stream to generate the response.
///
/// This is an example of using a [ResponseStream] to progressively
/// create the response, instead of using [ResponseBuffered]. The other class
/// used to create a [Response] is [ResponseRedirect] when the response is
/// a HTTP redirection.

@Handles.get('~/stream')
Future<Response> streamTest(Request req) async {
  // Get parameters

  final numIterations = 10;

  var secs = 0;
  if (req.queryParams['milliseconds'].isNotEmpty) {
    secs = int.parse(req.queryParams['milliseconds']);
  }

  // Produce the stream response

  final resp = ResponseStream(ContentType.text)..status = HttpStatus.ok;
  await resp.addStream(req, _streamSource(req, numIterations, secs));

  return resp;
}

//----------------
// The stream that produces the data making up the response.
//
// It produces a stream of bytes (List<int>) that make up the contents of
// the response.
//
// The content produces [iterations] lines of output, each waiting [ms]
// milliseconds before outputting it.

Stream<List<int>> _streamSource(Request req, int iterations, int ms) async* {
  final delay = Duration(milliseconds: ms);

  yield 'Stream of $iterations items (delay: $ms milliseconds)\n'.codeUnits;

  yield 'Started: ${DateTime.now()}\n'.codeUnits;

  for (var x = 1; x <= iterations; x++) {
    final completer = Completer<int>();
    Timer(delay, () => completer.complete(0));
    await completer.future;

    yield 'Item $x\n'.codeUnits;
  }
  yield 'Finished: ${DateTime.now()}\n'.codeUnits;
}

//----------------------------------------------------------------
/// Handler that returns JSON in the response.

@Handles.get('~/json')
Future<Response> handleJson(Request req) async {
  final data = {'name': 'John Citizen', 'number': 6};

  final resp = ResponseBuffered(ContentType.json)..write(json.encode(data));
  return resp;
}

//================================================================
// Exception handlers
//
// Woomera will invoke these methods if an exception was raised when processing
// a HTTP request.

//----------------------------------------------------------------
/// Exception handler used on the pipeline.
///
/// This will handle all exceptions raised by the application's request
/// handlers.

@Handles.pipelineExceptions()
Future<Response> pipelineExceptionHandler(
    Request req, Object exception, StackTrace st) async {
  log
    ..warning(
        'pipeline exception handler: ${exception.runtimeType}: $exception')
    ..finest('stack trace: $st');

  if (exception is DemoException) {
    if (exception.handledBy != HandledBy.pipelineExceptionHandler) {
      // Throw an exception. This will trigger the server exception handler
      // (if there is one) to process it.
      throw StateError('throw something');
    }
  }

  final resp = ResponseBuffered(ContentType.html)
    ..status = HttpStatus.internalServerError
    ..write('''
<!DOCTYPE html>
<html lang="en">
<head>
  <title>Error</title>
</head>
<body>
  <h1 style="color: red">Exception thrown</h1>

  <p style='font-size: small'>This error page was produced by the
  <strong>pipeline</strong> exception handler.
  See logs for details.</p>

  <a href="${req.ura('~/')}">Home</a>
</body>
</html>
''');

  return resp;
}

//----------------------------------------------------------------
/// Exception handler used on the server.
///
/// This will handle all exceptions raised outside the application's request
/// handlers, as well as if exceptions raised by the pipeline exception
/// handler.
///
/// Note: if there is no match a [NotFoundException] exception is raised for
/// this exception handler to process (i.e. generate a 404/405 error page for
/// the client).

@Handles.exceptions()
Future<Response> serverExceptionHandler(
    Request req, Object exception, StackTrace st) async {
  log
    ..warning('server exception handler: ${exception.runtimeType}: $exception')
    ..finest('stack trace: $st');

  if (exception is ExceptionHandlerException) {
    final originalException = exception.previousException;

    assert(exception.exception is StateError);

    if (originalException is DemoException) {
      if (originalException.handledBy != HandledBy.serverExceptionHandler) {
        // Throw an exception. This will trigger the server raw exception handler
        // (if there is one) to process it.
        throw originalException;
      }
    }
  }

  // Create a response

  final resp = ResponseBuffered(ContentType.html);

  // Set the status depending on the type of exception

  String message;
  if (exception is NotFoundException) {
    // A server exception handler gets this exception when no request handler
    // was found to process the request. HTTP has two different status codes
    // for this, depending on if the server supports the HTTP method or not.
    resp.status = (exception.found == NotFoundException.foundNothing)
        ? HttpStatus.methodNotAllowed
        : HttpStatus.notFound;
    message = 'Page not found';
  } else if (exception is ExceptionHandlerException) {
    // A server exception handler gets this exception if a pipeline exception
    // handler threw an exception (while it was trying to handle an exception
    // thrown by a request handler).
    resp.status = HttpStatus.badRequest;
    message = 'Pipeline exception handler threw an exception';
  } else {
    // A server exception handler gets all the exceptions thrown by a request
    // handler, if there was no pipeline exception handler.
    resp.status = HttpStatus.internalServerError;
    message = 'Internal error: unexpected exception';
  }

  resp.write('''
<!DOCTYPE html>
<html lang="en">
<head>
  <title>Exception</title>
</head>
<body>
  <h1 style="color: red">${HEsc.text(message)}</h1>

  <p style='font-size: small'>This error page was produced by the
  <strong>server</strong> exception handler.
  See logs for details.</p>

  <a href="${req.ura('~/')}">Home</a>
</body>
</html>
''');

  return resp;

  // If the server error handler raises an exception, a very basic error
  // response is sent back to the client. This situation should be avoided
  // (because that error page is very ugly and not user friendly) by making sure
  // the application's server exception handler never raises an exception.
}

//----------------------------------------------------------------
/// This is an example of a server raw exception handler.
///
/// But in this simple example, there is no way to invoke it. Raw exception
/// handlers are triggered in very rare situations.

@Handles.rawExceptions()
Future<void> myLowLevelExceptionHandler(
    HttpRequest rawRequest, String requestId, Object ex, StackTrace st) async {
  simLog.severe('[$requestId] raw exception (${ex.runtimeType}): $ex\n$st');

  final resp = rawRequest.response;
  assert(resp != null);

  resp
    ..statusCode = HttpStatus.internalServerError
    ..headers.contentType = ContentType.html
    ..write('''<!DOCTYPE html>
<html lang="en">
<head><title>Error</title></head>
<body>
  <h1>Error</h1>
  <p>Something went wrong.</p>
  
  <p style='font-size: small'>This error page was produced by the
  server <strong>raw</strong> exception handler.
  See logs for details.</p>
</body>
</html>
''');

  await resp.close();
}

//================================================================
// Simulated testing

//----------------------------------------------------------------
/// Uses the simulation features in Woomera to invoke the request handlers.
///
/// This is used for testing the server.
///
/// Run this program with the "-t" option to use this function, instead of
/// running a real server.
///
/// This function has been designed to exercise all the features of this
/// example program. So it can be used to perform coverage testing.

Future simulatedRun(Server server) async {
  simLog.info('started');

  {
    // Simulate a GET request to retrieve the home page

    simLog.info('GET home page');

    final req = Request.simulatedGet('~/');
    final resp = await server.simulate(req);
    simLog.info('home page content-type: ${resp.contentType}');
    assert(resp.status == HttpStatus.ok);
    assert(resp.contentType == ContentType.html);
    simLog.finer('home page body:\n${resp.bodyStr}');
  }

  {
    // Simulate a GET request to retrieve the example pattern page

    simLog.info('GET example page');

    final req = Request.simulatedGet('~/example/foo/bar/baz');
    final resp = await server.simulate(req);
    simLog.info('example page content-type: ${resp.contentType}');
    assert(resp.status == HttpStatus.ok);
    assert(resp.contentType == ContentType.text);
    simLog.finer('example page body:\n${resp.bodyStr}');
  }

  {
    // Simulate a POST request from submitting the form

    simLog.info('POST form');

    final postParams = RequestParamsMutable()..add(_pParamName, 'test process');

    final req = Request.simulatedPost(iPathFormHandler, postParams);
    final resp = await server.simulate(req);
    assert(resp.status == HttpStatus.ok);
    simLog.finer('form response body:\n${resp.bodyStr}');
    assert(resp.bodyStr.contains('Hello test process'));
  }

  {
    // Simulate a GET request that triggers the pipeline exception handler.

    simLog.info('GET: pipeline exception handler');

    final req = Request.simulatedGet(iPathExceptionGenerator);

    final resp = await server.simulate(req);
    assert(resp.status == HttpStatus.internalServerError);
    simLog.finer('exception body:\n${resp.bodyStr}');
    assert(
        resp.bodyStr.contains('<strong>pipeline</strong> exception handler'));
  }

  {
    // Simulate a GET request that triggers the server exception handler.

    simLog.info('GET: server exception handler');

    final req = Request.simulatedGet(iPathExceptionGenerator,
        queryParams: RequestParamsMutable()..add(_qParamProcessedBy, 'server'));

    final resp = await server.simulate(req);
    assert(resp.status == HttpStatus.badRequest);
    simLog.finer('exception body:\n${resp.bodyStr}');
    assert(resp.bodyStr.contains('<strong>server</strong> exception handler'));
  }

  {
    // Simulate a GET request that triggers the default server exception handler

    simLog.info('GET: default server exception handler');

    final req = Request.simulatedGet(iPathExceptionGenerator,
        queryParams: RequestParamsMutable()
          ..add(_qParamProcessedBy, 'defaultServer'));

    final resp = await server.simulate(req);
    assert(resp.status == HttpStatus.internalServerError);
    simLog.finer('exception body:\n${resp.bodyStr}');
  }

  {
    // Simulate a GET request for a page that doesn't exist

    simLog.info('GET non-existent page');

    final req = Request.simulatedGet('~/no/such/page', id: 'noSuchUrl');
    final resp = await server.simulate(req);
    assert(resp.status == HttpStatus.notFound); // 404
  }

  {
    // Simulate a GET where the response is produced as a stream

    simLog.info('GET stream');

    final req = Request.simulatedGet('~/stream',
        queryParams: RequestParamsMutable()..add('milliseconds', '100'));
    final resp = await server.simulate(req);
    assert(resp.status == HttpStatus.ok);
    assert(resp.contentType == ContentType.text);
    simLog.fine('stream body:\n${resp.bodyStr}');
    assert(resp.bodyStr.contains('Started:'));
    assert(resp.bodyStr.contains('Finished:'));
  }

  {
    // Simulate a GET where the response is JSON

    simLog.info('GET json');

    final req = Request.simulatedGet('~/json');
    final resp = await server.simulate(req);
    assert(resp.status == HttpStatus.ok);
    assert(resp.contentType == ContentType.json);
    simLog.finer('JSON body:\n${resp.bodyStr}');
    // ignore: avoid_as
    final j = json.decode(resp.bodyStr) as Object;
    assert(j is Map<String, Object>);
    if (j is Map<String, Object>) {
      assert(j.containsKey('name'));
      assert(j.containsKey('number'));
      assert(j['name'] is String);
      assert(j['number'] is int);
    }
  }

  simLog.info('finished');
}

//================================================================
// Top level methods

//----------------------------------------------------------------
/// Setup the server.
///
/// Creates a server and registers request and exception handlers for it.

Server _serverSetup() {
  //--------
  // Create a new Web server
  //
  // The bind address is setup to listen to any incoming connection from any IP
  // address (IPv4 or IPv6). If this is not done, by default it only listens
  // on the IPv4 loopback interface, which is good for deployment behind a
  // reverse Web proxy, but might be restrictive for testing.
  //
  // Since the Server constructor is not passed any pipeline names, by default
  // it creates one pipeline with the default name. Request handlers and
  // exception handlers are set up via the [Handles] annotations.

  final webServer = Server.fromAnnotations()
    ..bindAddress = InternetAddress.anyIPv6
    ..v6Only = false // false = listen to any IPv4 and any IPv6 address
    ..bindPort = port;

  log.info('Web server running on port $port');

  return webServer;
}

//----------------------------------------------------------------
// Set up logging
//
// Change this to the level and type of logging desired.

void _loggingSetup() {
  hierarchicalLoggingEnabled = true;
  Logger.root.onRecord.listen((rec) {
    print('${rec.time}: ${rec.loggerName}: ${rec.level.name}: ${rec.message}');
  });

  Logger.root.level = Level.OFF;

  final commonLevel = Level.INFO;

  Logger('app').level = commonLevel;
  Logger('simulation').level = commonLevel;

  Logger('woomera.server').level = commonLevel;
  Logger('woomera.request').level = Level.FINE; // FINE prints each URL
  Logger('woomera.request.header').level = commonLevel;
  Logger('woomera.request.param').level = commonLevel;
  Logger('woomera.response').level = commonLevel;
  Logger('woomera.session').level = commonLevel;

  // To see the Handles annotations that have been found, set this to
  // FINE. Set it to FINER for more details. Set it to FINEST to see what
  // files and/or libraries were scanned and not scanned for annotations.
  Logger('woomera.handles').level = commonLevel;
}

//----------------------------------------------------------------
/// Main

Future main(List<String> args) async {
  final testMode = args.contains('-t'); // test mode
  final quietMode = args.contains('-q'); // quiet mode

  if (!quietMode) {
    _loggingSetup();
  }

  // Create the server and either test it or run it

  final server = _serverSetup();

  if (testMode) {
    await simulatedRun(server); // run simulation for testing
  } else {
    await server.run(); // run Web server
    // Unless the server's [stop] method is invoked, the server will run
    // forever, listening for requests, so normally execution never gets here.
  }
}

Use this package as a library

1. Depend on it

Add this to your package's pubspec.yaml file:


dependencies:
  woomera: ^4.5.0

2. Install it

You can install packages from the command line:

with pub:


$ pub get

Alternatively, your editor might support pub get. Check the docs for your editor to learn more.

3. Import it

Now in your Dart code, you can use:


import 'package:woomera/woomera.dart';
  
Popularity:
Describes how popular the package is relative to other packages. [more]
31
Health:
Code health derived from static analysis. [more]
100
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
100
Overall:
Weighted score of the above. [more]
66
Learn more about scoring.

We analyzed this package on Feb 14, 2020, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.7.1
  • pana: 0.13.5

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=2.0.0 <3.0.0
http ^0.12.0+1 0.12.0+4
logging >=0.11.2 <0.11.5 0.11.4
uuid >=0.5.0 <3.0.0 2.0.4
Transitive dependencies
async 2.4.0
charcode 1.1.3
collection 1.14.12
convert 2.1.1
crypto 2.1.4
http_parser 3.1.3
meta 1.1.8
path 1.6.4
pedantic 1.9.0
source_span 1.6.0
string_scanner 1.0.5
term_glyph 1.1.0
typed_data 1.1.6
Dev dependencies
test >=0.12.10 <2.0.0