woomera 4.2.0

  • README.md
  • CHANGELOG.md
  • Example
  • Installing
  • Versions
  • 74

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 listens for HTTP requests and respond to them with HTTP responses: a simple task, but one that can get complicated (and difficult to maintain) when the program has many different pages to display, handle errors and maintain state. This package aims to reduce that complexity.

Main features include:

  • URL pattern matching inspired by the Sinatra Web framework - allows easy parsing of URL path components as parameters;

  • Exception handling framework - ensures error pages are reliably generated and unexpected exceptions are always "caught" to generate an error page response;

  • Session management using cookies or URL rewriting;

  • Responses can be generated into a buffer - allows response to contain a complete error page instead of an incompletely generated result page.

  • Responses can be read from a stream of data.

  • Ability to test a Web server without needing a Web browser.

  • Pipelines of patterns for matching against URLs to allow sophisticated processing, if needed - allows requests to be processed by multiple handlers (e.g. to log/audit requests before handling them) and different exception handlers to be set for different resources;

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 one page. It creates a server with one response handler.

import 'dart:async';
import 'dart:io';

import 'package:woomera/woomera.dart';

Future main() async {
  // Create and configure server

  var ws = new Server();
  ws.bindAddress = InternetAddress.anyIPv6;
  ws.bindPort = 1024;

  // Register rules

  var p = ws.pipelines.first;
  p.get("~/", handleTopLevel);

  // Run the server

  await ws.run();
}

Future<Response> handleTopLevel(Request req) async {
  var name = req.queryParams["name"];
  name = (name.isEmpty) ? "world" : name;

  var resp = new ResponseBuffered(ContentType.HTML);
  resp.write("""
<html>
  <head>
    <title>Woomera Tutorial</title>
  </head>
  <body>
    <h1>Hello ${HEsc.text(name)}!</h1>
  </body>
</html>
""");
  return resp;
}

The most important feature of the package is to organise response handlers, so that HTTP requests can be matched to Dart code to process them and to generate a HTTP response.

A Server has of a sequence of pipelines, and each pipeline has a sequence of rules. Each rule consists of the HTTP method (e.g. GET or POST), a path pattern, and a request handler method.

When a HTTP request arrives, the pipelines are search (in order) for a rule that matches the request. A match is when the HTTP method is the same and the pattern matches the request URL's path. If found, the corresponding handler is invoked to produce the HTTP response. If no rule is found (after searching through all the rules in all the pipelines), the resource is treated as not found.

1.2. Importing the package #

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

import 'package:woomera/woomera.dart';

1.3. The server #

For the Web server, a Server object is created and configured for the TCP/IP address and port it will listen for HTTP requests on.

var ws = new Server();
ws.bindAddress = InternetAddress.ANY_IP_V6;
ws.bindPort = 1024;

For testing, the above example sets it to InternetAddress.ANY_IP_V6, so the service is listening to connections on any interface (i.e. both loopback and public). When using InternetAddress.ANY_IP_V6, the v6Only member is used to control whether IPv4 addresses are included or not.

Typically, when deployed in production, the service is accessed via a reverse Web proxy (e.g. Apache or Nginx). The default bind address is InternetAddress.LOOPBACK_IP_V4, which means it only listens for connections on 127.0.0.1. That is, only clients on the same host can connect to it.

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

1.4. The pipeline #

The Server (by default) automatically creates one pipeline, since that is the most common scenario. The pipelines member is a List of ServerPipeline objects, so retrieve it from the server using something like:

var p = ws.pipelines.first;

1.5. The rules #

Rules are registered with the pipeline. The get method on the ServerPipeline object will register a rule for the HTTP GET method, and the post method will register a rule for the HTTP POST method. The first parameter is the pattern. The second parameter is the handler method: the method that gets invoked when the rule matches the HTTP request.

p.get("~/", handlerTopLevel);

The tilde ("") indicates this is relative to the base path of the server. The default base path is "/". See the API documentation for information about changing the base path. For now, all paths should begin with "/".

1.6. Running the server #

After configuring the [Server], start it using its run method. The run method returns a Future that completes when the Web server finishes running; but normally a Web server runs forever without stopping.

await ws.run();

1.7. Request handlers #

A request handler method is used to process the HTTP request to produce a HTTP response. It is passed the HTTP request as a Request object; and it returns a HTTP response as represented by a Response object.

There are different types of Response objects. The commonly used one for generating HTML pages is the ResponseBuffered. It acts as a buffer where the contents is appended to it using the write method. After the response is returned from the request handler, the framework uses it to generate the HTTP response that is sent back to the client.

This first example request handler returns a simple HTML page.

Future<Response> handleTopLevel(Request req) async {
  var name = req.queryParams["name"];
  name = (name.isEmpty) ? "world" : name;

  var resp = new ResponseBuffered(ContentType.HTML);
  resp.write("""
<html>
  <head><title>Example 1</title></head>
  <body>
    <h1>Hello ${HEsc.text(name)}!</h1>
  </body>
</html>
""");
  return resp;
}

The "name" query parameter is retrieved from the request. If it is the empty string, a default constant value is used instead. The square bracket operator returns the empty string if the parameter does not exist.

The name is used in the HTML heading. The HEsc.text method is used to escape any special characters, to prevent accidential or malicious HTML injection.

When a Web browser sends a request to the site's URL the HTML page is returned. In this document, the example URLs will show the hostname of the server as "localhost"; if necessary, change it to the hostname or IP address of the machine running your server.

Run the server and try visiting:

The last example demonstrates the importance of using HEsc.text to escape values.

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

1.8. Exception handler #

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.

When setting up the server, set its exception handler in main (anywhere before the server is run):

ws.exceptionHandler = myExceptionHandler;

And define the exception handler method as:

Future<Response> myExceptionHandler(Request req, Object ex, StackTrace st) async {
  var status;
  var message;

  if (ex is NotFoundException) {
    status = (ex.found == NotFoundException.foundNothing) ? HttpStatus.METHOD_NOT_ALLOWED : HttpStatus.NOT_FOUND;
    message = "Sorry, the page you were looking for could not be found.";
  } else {
    status = HttpStatus.INTERNAL_SERVER_ERROR;
    message = "Sorry, an internal error occured.";
    print("Exception: $ex");
  }

  var resp = new ResponseBuffered(ContentType.HTML);
  resp.status = status;

  resp.write("""
<html>
  <head>
    <title>Error</title>
  </head>
  <body>
    <h1>Error</h1>
    <p>$message</p>
  </body>
</html>
""");

  return resp;
}

This exception handler customizes the error page when the NotFoundException is encountered: it is raised when none of the rules matched the request. Notice that it reports a different status code if no rules for the 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).

Other exceptions can be detected and handled differently. But in this example, they all produce the same error page.

Run this server and visit http://localhost:1024/nosuchpage to see the custom error page.

2. HTML escaping methods #

The HEsc class defines three static methods which are useful for converting objects into Strings that are then escaped for embedded into HTML.

  • attr for escaping values to be inserted into attributes.
  • text for escaping values to be inserted into element content.
  • lines which is the same as text, but adds line breaks elements (i.e. <br/>) where newlines exist in the original value.

These methods will be used to escape values which might contain characters with special meaning in HTML.

3. Parameters #

The request handler methods can receive three different types of parameters:

  • path parametrs;
  • query parameters; and
  • post parameters.

3.1. Path parameters #

The path parameters are extracted from the path of the URL being requested.

The path parameters are defined by the rule's pattern, which is made up of components separated by a slash ("/"). Path parameters are represented by a component starting with a colon (":") followed by the name of the parameter.

The path parameters are made available to the handler via the pathParams member of the Request object.

This is an example of a rule with a fixed path, where each component must match the requested URL exactly and there are no path parameters.

p.get("~/foo/bar/baz", handleParams);

This is an example with a single parameter:

p.get("~/user/:name", handleParams);

This is an example with two parameters:

p.get("~/user/:name/:orderNumber", handleParams);

The wildcard is a special path parameter that will match zero or more segments in the URL path.

p.get("~/product/*", handleParams);

Here is an example request handler that shows the parameters in the request.

Future<Response> handleParams(Request req) async {
  var resp = new ResponseBuffered(ContentType.HTML);
  resp.write("""
<html>
  <head>
    <title>Woomera Tutorial</title>
  </head>
  <body>
    <h1>Parameters</h1>
""");

  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) {
    var keys = p.keys;

    if (keys.isNotEmpty) {
      resp.write("<p>Number of keys: ${keys.length}</p>");
      resp.write("<dl>");

      for (var k in keys) {
        resp.write("<dt>${HEsc.text(k)}</dt><dd><ul>");
        for (var v in p.values(k)) {
          resp.write("<li>${HEsc.text(v)}</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. Query parameters #

The query parameters are the query parameters from the URL. That is, the name-value pairs after the question mark ("?").

The path parameters are made available to the handler via the queryParams member of the Request object. They are not (and cannot) be specified in the rule.

Here are a few URLs to try:

3.3. Post parameters #

The post parameters are extracted from the contents of a HTTP POST request. Obviously, they are only available when processing a POST request.

The path parameters are made available to the handler via the postParams member of the Request object, which is null unless it is a POST request. They are not (and cannot) be specified in the rule.

For example, try this form:

<form method="POST" action="http://example.com/transaction">
  <input type="radio" name="type" value="out" id="w"/> <label for="w">Withdraw</label>
  <input type="radio" name="type" value="in" id="d"/> <label for="d">Deposit</label>
  <input type="text" name="amount"/>
</form>

processed by the above handler prints out:

"Hello World"

3.4. Common aspects #

The three parameter members are instances of the RequestParams class.

It is important to remember that parameters can be repeated. For example, checkboxes on a form will result in one instance of the named parameter for every checkbox that is checked. This can apply to path parameters, query parameters and post parameters.

3.4.1. Retrieving parameters #

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 (var 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 (var k in req.queryParams.keys) {
  var vList = req.queryParams.values(k);
  for (var 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.

 var t = req.queryParams["title"];
3.4.2. Raw vs processed values #

The methods described above for retrieving value(s) returns a cleaned up processed version of the value. The processing:

  • removes all leading whitespaces;
  • removes all trailing whitespace;
  • collapses multiple whitespaces in a row into a single 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.3. Expecting 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 proceesed 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 break. 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.

Both the names and values are always strings.

4. Exceptions #

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 serves as an error page for the client.

Future<Response> myExceptionHandler(Request req
    Object exception, StackTrace st) async {
  var resp = new ResponseBuffered(ContentType.HTML);
  resp.write("""
<html>
  <head><title>Error</title></head>
  <body>
    <h1>Error</h1>
    <p>Sorry, an error occured: ${HEsc.text(exception.toString())}</p>
  </body>
</html>
""");
  return resp;
}

Exception handlers can be attached to the pipelines and the server.

A hierarchy determines which exception handler is invoked. 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 exception handler attached to the server is used. If no exception handler was attached to the server, a default exception handler is used.

The hierarchy is also used if an exception handler itself throws an exception. (Though, hopefully, exception handlers will not throw an exception). In that situation, a ExceptionHandlerException is thrown.

4.1. Standard exceptions #

The framework throws exceptions that are also processed by the same exception handling hierarchy.

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.NOT_FOUND or HttpStatus.METHOD_NOT_ALLOWED.

Other exceptions defined in the package are subclasses of WoomeraException.

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

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

5.2. ResponseRedirect #

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

5.3. ResponseStream #

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

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

5.5 Common handlers provided #

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

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

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

7. Logging #

Woomera uses the Logging package for logging.

Please see the woomera library API documentation for the logger names.

In general, a logging level of "INFO" should produce no logging 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.

8. References #

Changelog #

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;

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

const String pathFormGet = '~/date-calculator/form';
const String pathFormPost = pathFormGet; // can be a different value too

// Names of the form parameters.
// Constants are used for these so the HTML form inputs uses the same value that
// the form processor expects.

const String _pParamTitle = 'title';
const String _pParamFromDate = 'fromDate';
const String _pParamToDate = 'toDate';

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

/// Application logger.

Logger log = new Logger("app");
Logger simLog = new Logger("simulation");

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

class DemoException1 implements Exception {
  @override
  String toString() => 'wrong order: no title';
}

class DemoException2 implements Exception {
  DemoException2(this.title);
  String title;
  @override
  String toString() => 'wrong order: with title "$title"';
}

//================================================================
// 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

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 = new ResponseBuffered(ContentType.html)..write("""
<!doctype html>
<html>
<head>
  <title>Example</title>
</head>

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

  <ul>
    <li>
      Example with form parameters:
      <a href="${req.ura(pathFormGet)}">date calculator</a></li>
    <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>
      No match:
      <a href="${req.ura('~/no/such/page')}">1</a>
      <a href="${req.ura('~/example/first/second/noMatch')}">2</a>
    </li>
    <li>
      Other:
      <ul>
        <li>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')}">Response is JSON</a></li>
      </ul>
    </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;
}

//----------------------------------------------------------------
// Date calculator form page.
//
// This handles the GET request for the form.

Future<Response> dateCalcGetHandler(Request req) async {
  assert(req.method == "GET");

  final resp = new ResponseBuffered(ContentType.html)..write("""
<!doctype html>
<html>
<head>
  <title>Date calculator</title>
</head>

<body>
  <header>
    <h1>Date calculator</h1>
  </header>

  <form method="POST" action="${req.ura(pathFormPost)}">
    <p>Title: <input name="${HEsc.attr(_pParamTitle)}"/></p>
    
    <p>From
      <input name="${HEsc.attr(_pParamFromDate)}" type="date"/>
      to
      <input name="${HEsc.attr(_pParamToDate)}" type="date"/>
      <input type="submit" value="Calculate number of days"/>
    </p>
  </form>
  
  <p style="font-size: small">Enter a "from" date that is after the "to" date
  to cause the handler to raise an exception. Different exceptions are raised
  if the title is blank or not.</p>
  
  <footer><p><a href="${req.ura('~/')}">Home</a></p></footer>
</body>
</html>
""");

  return resp;
}

//----------------------------------------------------------------
/// Date calcualtor results page.
///
/// This handles the POST request when the form is submitted.

Future<Response> dateCalcPostHandler(Request req) async {
  assert(req.method == "POST");

  // Get the form parameters

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

  // The form parameters can be retrieved as strings from postParams.

  final title = req.postParams[_pParamTitle];
  final fromStr = req.postParams[_pParamFromDate];
  final toStr = req.postParams[_pParamToDate];

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

  try {
    // The form parameters are strings that may need to be converted

    // Note: a good Web application should validate all input, since the input
    // could be invalid or malicious. In this situation, the browser might not
    // support the HTML5 date input and the user could have typed in an invalid
    // value.

    final now = new DateTime.now();
    final today = new DateTime(now.year, now.month, now.day); // midnight

    final fromDate = (fromStr.isNotEmpty) ? DateTime.parse(fromStr) : today;

    final toDate = (toStr.isNotEmpty) ? DateTime.parse(toStr) : today;

    // Use the form parameters and produce the response

    if (fromDate.isAfter(toDate)) {
      // Normally a handler should deal with the error and produce an
      // appropriate response (e.g. a page with an error message).
      // But in this example, two different exceptions are thrown, to
      // demonstrate the exception handlers being used. Exception handlers
      // allow the Web application to always produce a user friendly response,
      // even if the handler didn't catch all the possible exceptions.
      if (title.isEmpty) {
        throw new DemoException1();
      } else {
        throw new DemoException2(title);
      }
    }

    final diff = toDate.difference(fromDate);

    // 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 = new ResponseBuffered(ContentType.html)..write("""
<!doctype html>
<html>
<head>
  <title>Date calculator</title>
</head>

<body>
  <header>
    <h1>Date calculator</h1>
  </header>
  
  <h2>${HEsc.text(title)}</h2>
  
  <p>From ${_formatDate(fromDate)} to ${_formatDate(toDate)}: ${diff.inDays} days.</p>

  <p><a href="${req.ura(pathFormGet)}">Back to form</a></p>
</body>
</html>
""");

    return resp;
  } on FormatException {
    // Produce an error response

    return new ResponseBuffered(ContentType.html)
      ..status = HttpStatus.badRequest
      ..write("""
 <!doctype html>
<html>
<head>
  <title>Date calculator</title>
</head>

<body>
  <header>
    <h1>Date calculator</h1>
  </header>
  
  <p>Error: invalid date(s) entered</p>

  <p><a href="${req.ura(pathFormGet)}">Back to form</a></p>
</body>
</html>
    """);
  }
}

String _formatDate(DateTime dt) => dt.toIso8601String().substring(0, 10);

//----------------------------------------------------------------
/// Stream handler
///
/// 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.

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 = new 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 = new Duration(milliseconds: ms);

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

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

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

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

//----------------------------------------------------------------
/// Handler that returns JSON in the response.
///
Future<Response> handleJson(Request req) async {
  final data = {'name': "John Citizen", 'number': 6};

  final resp = new 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.

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 DemoException1) {
    final h = new ResponseBuffered(ContentType.html)
      ..status = HttpStatus.badRequest;

    final message = 'Dates are in the wrong order';
    _produceErrorPage(h, exception, message, 'pipeline', req.rewriteUrl('~/'));

    return h;
  } else {
    // If this pipeline exception handler raises an exception, the server
    // exception handler will get an [ExceptionHandlerException] containing
    // the original exception and the exception that is raised.
    throw new StateError('pipeline exception hander raised exception');
  }
}

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

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

  // Create a response

  final resp = new ResponseBuffered(ContentType.html);

  // Set the status depending on the type of exception

  String message;
  if (exception is NotFoundException) {
    resp.status = (exception.found == NotFoundException.foundNothing)
        ? HttpStatus.methodNotAllowed
        : HttpStatus.notFound;
    message = 'Page not found';
  } else if (exception is ExceptionHandlerException) {
    resp.status = HttpStatus.badRequest;
    message = 'Pipeline exception handler threw an exception';
  } else {
    // Catch all
    resp.status = HttpStatus.internalServerError;
    message = 'Internal error: unexpected exception';
  }

  _produceErrorPage(resp, exception, message, 'server', req.rewriteUrl('~/'));

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

//----------------------------------------------------------------

void _produceErrorPage(ResponseBuffered resp, Object exception, String message,
    String whichExceptionHandler, String homePageUrl) {
  // Internal information should never be revealed to the client.

  resp.write("""
<!doctype html>
<html>
<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>${HEsc.text(whichExceptionHandler)}</strong> exception handler.
  See logs for details.</p>

  <a href="${HEsc.attr(homePageUrl)}">Home</a>
</body>
</html>
""");
}

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

//----------------------------------------------------------------
/// Uses the simulation features in Woomera to invoke the request handlers.
///
/// This is used for testing the server.
///
/// Try running this for 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 = new 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 form

    simLog.info("GET form");

    var req = new Request.simulatedGet(pathFormGet);
    var resp = await server.simulate(req);
    assert(resp.status == HttpStatus.ok);
    simLog.finer('form page body:\n${resp.bodyStr}');
    assert(resp.bodyStr.contains('<form '));
    assert(resp.bodyStr.contains('<input '));

    // Simulate a POST request from submitting the form

    simLog.info("POST form");

    final postParams = new RequestParamsMutable()
      ..add(_pParamTitle, 'Testing')
      ..add(_pParamFromDate, '2019-01-01')
      ..add(_pParamToDate, '2019-02-28');

    req = new Request.simulatedPost(pathFormPost, postParams);
    resp = await server.simulate(req);
    assert(resp.status == HttpStatus.ok);
    simLog.finer('form response body:\n${resp.bodyStr}');
    assert(resp.bodyStr.contains('58 days'));

    // Simulate a POST request from submitting the form with invalid dates
    // This causes an error that the handler takes care of.

    simLog.info("POST form: exception 0");

    req = new Request.simulatedPost(
        pathFormPost,
        new RequestParamsMutable()
          ..add(_pParamTitle, 'Testing')
          ..add(_pParamFromDate, 'yesterday')
          ..add(_pParamToDate, 'tomorrow')); // dates that can't be parsed

    resp = await server.simulate(req);
    assert(resp.status == HttpStatus.badRequest);
    simLog.finer('form error body 0:\n${resp.bodyStr}');
    assert(resp.bodyStr.contains('invalid date(s) entered'));

    // Simulate a POST request from submitting the form with invalid values
    // This raises an exception for the pipeline exception handler.

    simLog.info("POST form: exception 1");

    req = new Request.simulatedPost(
        pathFormPost,
        new RequestParamsMutable()
          ..add(_pParamTitle, '') // no title
          ..add(_pParamFromDate, '2019-12-31')
          ..add(_pParamToDate, '1970-01-01')); // to date before from date error

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

    // Simulate a POST request from submitting the form with invalid values
    // This raises an exception for the server exception handler.

    simLog.info("POST form: exception 2");

    req = new Request.simulatedPost(
        pathFormPost,
        new RequestParamsMutable()
          ..add(_pParamTitle, 'Testing') // title present
          ..add(_pParamFromDate, '2019-12-31')
          ..add(_pParamToDate, '1970-01-01')); // to date before from date error

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

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

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

    final req = new 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 = new Request.simulatedGet('~/stream',
        queryParams: new 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 = new 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.

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

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

  //--------
  // Setup the first (and only) pipeline with handlers for the GET and POST
  // requests, as well as an exception handler (to handle exceptions raised
  // by those handlers). Servers initially have one pipeline, but more can be
  // added if required.
  //
  // The first parameter to get/post is an internal URL, which is the path
  // starting with "~/". Path parameters are denoted using components that
  // start with a colon followed by the parameter name (e.g ":foo").s

  webServer.pipelines.first
    ..exceptionHandler = pipelineExceptionHandler
    ..get('~/', homePage)
    ..get(pathFormGet, dateCalcGetHandler)
    ..post(pathFormPost, dateCalcPostHandler)
    ..get('~/example/:foo/:bar/baz', debugHandler)
    ..get('~/stream', streamTest)
    ..get('~/json', handleJson);

  // The debugHandler is a handler that is provided by Woomera. It prints
  // out all the parameters it receives, and can be used for debugging.

  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;

  new Logger("app").level = commonLevel;
  new Logger("simulation").level = commonLevel;

  new Logger("woomera.server").level = commonLevel;
  new Logger("woomera.request").level = Level.FINE; // FINE prints each URL
  new Logger("woomera.request.header").level = commonLevel;
  new Logger("woomera.request.param").level = commonLevel;
  new Logger("woomera.response").level = commonLevel;
  new Logger("woomera.session").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 past
    // this line.
  }
}

Use this package as a library

1. Depend on it

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


dependencies:
  woomera: ^4.2.0

2. Install it

You can install packages from the command line:

with pub:


$ pub get

with Flutter:


$ flutter pub get

Alternatively, your editor might support pub get or flutter 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';
  
Version Uploaded Documentation Archive
4.2.0 May 15, 2019 Go to the documentation of woomera 4.2.0 Download woomera 4.2.0 archive
4.1.0 Mar 25, 2019 Go to the documentation of woomera 4.1.0 Download woomera 4.1.0 archive
4.0.1 Mar 21, 2019 Go to the documentation of woomera 4.0.1 Download woomera 4.0.1 archive
4.0.0 Mar 20, 2019 Go to the documentation of woomera 4.0.0 Download woomera 4.0.0 archive
3.0.1 Aug 10, 2018 Go to the documentation of woomera 3.0.1 Download woomera 3.0.1 archive
3.0.0 Aug 10, 2018 Go to the documentation of woomera 3.0.0 Download woomera 3.0.0 archive
2.2.2 Aug 17, 2018 Go to the documentation of woomera 2.2.2 Download woomera 2.2.2 archive
2.2.1 Aug 9, 2018 Go to the documentation of woomera 2.2.1 Download woomera 2.2.1 archive
2.2.0 Jun 28, 2018 Go to the documentation of woomera 2.2.0 Download woomera 2.2.0 archive
2.1.1 Jan 24, 2018 Go to the documentation of woomera 2.1.1 Download woomera 2.1.1 archive

All 18 versions...

Popularity:
Describes how popular the package is relative to other packages. [more]
49
Health:
Code health derived from static analysis. [more]
99
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
100
Overall:
Weighted score of the above. [more]
74
Learn more about scoring.

We analyzed this package on Jun 25, 2019, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.3.2
  • pana: 0.12.18

Platforms

Detected platforms: Flutter, other

Primary library: package:woomera/woomera.dart with components: io.

Health suggestions

Fix lib/src/core_request.dart. (-1 points)

Analysis of lib/src/core_request.dart reported 2 hints:

line 78 col 9: DO use curly braces for all flow control structures.

line 80 col 9: DO use curly braces for all flow control structures.

Fix lib/src/session.dart. (-0.50 points)

Analysis of lib/src/session.dart reported 1 hint:

line 206 col 8: Unnecessary cast.

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=2.0.0 <3.0.0
http ^0.12.0+1 0.12.0+2
logging >=0.11.2 <0.11.4 0.11.3+2
uuid >=0.5.0 <3.0.0 2.0.2
Transitive dependencies
async 2.2.0
charcode 1.1.2
collection 1.14.11
convert 2.1.1
crypto 2.0.6
http_parser 3.1.3
meta 1.1.7
path 1.6.2
pedantic 1.7.0
source_span 1.5.5
string_scanner 1.0.4
term_glyph 1.1.0
typed_data 1.1.6
Dev dependencies
test >=0.12.10 <2.0.0