0 purchases
woomera
Woomera #
Woomera is a Dart package for implementing Web servers.
It is used to create server-side Dart programs that listens for HTTP
requests and respond to them with HTTP responses.
A Web server is simple in theory, but in practice it quickly gets
complicated and difficult to maintain. Especially when there are many
different types of HTTP requests to process, different errors to
detect and state needs to be maintained between the HTTP
requests. This package aims to reduce that complexity.
Main features include:
URL pattern matching inspired by the
Sinatra Web framework. This allows the
HTTP request paths to be easily specified and different segments of
the path to be used as parameters.
Exception handling mechanism to handle all uncaught and unexpected
exceptions. This ensures the Web application can always generate a
user-friendly error page, instead of sometimes producing unexpected
results when an exception was not caught. This is especially useful
when using third-party packages that might throw undocumented
exceptions. Error handling is simplified and the Web application is
more robust and reliable.
Session management using cookies or URL rewriting. The HTTP protocol
does not maintain state between HTTP requests. This framework
includes a mechanism for maintaining state. For example, it can be
used to remember the user's account after they have signed in. URL
rewriting works if cookies have been disabled in the browser (though
this is rare these days).
Responses can be buffered, and sent as the HTTP response only
when it is complete. Therefore, if an error occurs the user won't
see a partially generated page.
Responses can be generated from a stream of data.
Pipelines allow request handlers to be invoked in the desired order.
Multiple error handlers are supported. Requests can be arranged to
be handled by multiple request handlers. For example, the first
request handler can log the request and the second request handler
perform the actual processing.
Features for testing the Web application without using a Web
browser. This does not replace testing with a real Web browser, but
runs faster than controlling a Web browser using WebDriver or
Selenium Remote Control.
Can be statically compiled. Annotations are also defined
if you want to dynamically identify the handler methods.
The following is a tutorial which provides an overview the main
features of the package. For details about the package and its
advanced features, please see the API documentation.
Platform support #
This package is supported on all platforms where "dart:io" is
supported.
Tutorial #
1. A basic Web server #
1.1. Overview #
This is a basic Web server that has two request handlers for
handling two types of URI requests. And it defines one server
exception handler.
import 'dart:async';
import 'dart:io';
import 'package:woomera/woomera.dart';
Future<void> main() async {
// Create the server with one pipeline
final ws = Server()
..bindAddress = InternetAddress.anyIPv6
..bindPort = 1024
..exceptionHandler = myExceptionHandler
..pipelines.add(ServerPipeline()
..get('~/', handleTopLevel)
..get('~/:greeting', handleGreeting));
// Run the server
await ws.run();
}
@Handles.get('~/')
Future<Response> handleTopLevel(Request req) async {
final resp = ResponseBuffered(ContentType.html);
final helloUrl = req.rewriteUrl('~/Hello');
final gDayUrl = req.rewriteUrl("~/G'day");
resp.write('''
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Woomera Tutorial</title>
</head>
<body>
<h1>Woomera Tutorial</h1>
<ul>
<li><a href="${HEsc.attr(helloUrl)}">Hello</a></li>
<li><a href="${HEsc.attr(gDayUrl)}">Good day</a></li>
</ul>
</body>
</html>
''');
return resp;
}
@Handles.get('~/:greeting')
Future<Response> handleGreeting(Request req) async {
final greeting = req.pathParams['greeting'];
var name = req.queryParams['name'];
name = (name.isEmpty) ? 'world' : name;
final resp = ResponseBuffered(ContentType.html);
final homeUrl = req.rewriteUrl('~/');
resp.write('''
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Woomera Tutorial</title>
</head>
<body>
<h1>${HEsc.text(greeting)} ${HEsc.text(name)}!</h1>
<p><a href="${HEsc.attr(homeUrl)}">Home</a></p>
</body>
</html>
''');
return resp;
}
@ServerExceptionHandler()
Future<Response> myExceptionHandler(
Request req, Object ex, StackTrace st) async {
int status;
String message;
if (ex is NotFoundException) {
status =
ex.resourceExists ? HttpStatus.methodNotAllowed : HttpStatus.notFound;
message = 'Sorry, the page you were looking for could not be found.';
} else {
status = HttpStatus.internalServerError;
message = 'Sorry, an internal error occurred.';
print('Exception: $ex');
}
return ResponseBuffered(ContentType.html)
..status = status
..write('''
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Error</title>
</head>
<body>
<h1>Woomera Tutorial: Error</h1>
<p>${HEsc.text(message)}</p>
</body>
</html>
''');
}
copied to clipboard
1.2. Importing the package #
Any program that uses the framework must first import the package:
import 'package:woomera/woomera.dart';
copied to clipboard
1.3. Creating the server #
The Web server needs to create and configure a [Server] object. And
then invoke its asynchronous run method which causes it to listen
and process HTTP requests.
This is the smallest possible server. It listens on port 80 of the
IPv4 loopback address (127.0.0.1) for HTTP requests. But will respond
to every HTTP request with a HTTP 401 Not found.
final ws = Server();
await ws.run();
copied to clipboard
The interface and port it listens on is configured by the bindPort
and bindAddress properties.
This is a server that listens on port 1024 of all network interfaces
(i.e. any IP address, both IPv4 and IPv6) of the host machine.
final ws = Server()
..bindAddress = InternetAddress.anyIPv6
..bindPort = 1024;
copied to clipboard
Typically the application Web server is deployed behind a reverse
proxy. If the reverse proxy is running on the same host, restricting
access to only the IPv4 loopback address is desirable; but usually the
port number needs to be changed to avoid conflicts and issues with
permissions.
1.4. Pipelines #
The code to process HTTP requests is implemented in request handler
functions.
A server is organised as an ordered list of pipelines. And each of
those pipelines has an ordered list of rules and request handlers
pairs.
The pipelines are represented by instances of the [ServerPipeline]
class. The class has methods for registering rules with request
handlers. Rules are made up of a HTTP method and a pattern. The
register method (which is passed the HTTP method as a string) can be
used, but there are convenient methods named after the standard HTTP
methods too.
For example, the following creates a pipeline and registers two rules
on it.
ServerPipeline()
..get('~/', handleTopLevel)
..get('~/:greeting', handleGreeting)
copied to clipboard
Both rules are for the HTTP GET method.
The pattern is represented by a string starting with "~/" and has path
segments that will be matched against the request URI's path.
On the server object, the pipelines member is a list of
ServerPipeline objects. So the standard Dart methods on lists can be
used to manage the pipelines.
In this example, the list add method is used to add the pipeline to
the server.
final ws = Server()
..bindAddress = InternetAddress.anyIPv6
..bindPort = 1024
..pipelines.add(ServerPipeline()
..get('~/', handleTopLevel)
..get('~/:greeting', handleGreeting));
copied to clipboard
1.5. Rule matching #
A HTTP method and a pattern is referred to as a "rule".
When a HTTP request is received a request handler is found, by
searching for a rule that matches it.
The search is conducted in order. It examines the pipelines in order,
and for each pipeline it examines each of its rules in order. If a
rule matches, its request handler is invoked and the returned value
used to produce the HTTP response.
Multiple pipelines is useful is some situations. They can be used to
control the order in which rule matching is performed. They can be
used to handle exceptions in different ways, which will be described
later. And they can be used to group request handlers.
Instead of returning the response, a request handler could throw a
[NoResponseProduced] exception. This is a special exception that tells
the matching algorith to continue searching subsequent rules, and
subsequent pipelines, for another match. This feature can be used to
pre-preprocess requests; for example, to have a request handler that
audits every HTTP request before letting a different request handler
produce the response.
A rule matches the HTTP request if its HTTP method is the same as the
request's HTTP method, and the pattern matches the path of the request
URI. The type of segment in the pattern determines how it is matched
to the segments in the URI. These are the types of segments found in a
pattern:
A literal segment matches the exact same value (i.e. string equality).
A path parameter starts with a colon followed by the parameter
name (e.g. ":greeting" or ":foo"). It matches exactly one segment at
that position, and its value is assigned to the path parameter with
that name.
A wildcard paramter is represented by "*". It matches has one or
more segments. Those segments are assigned to the value of the path
parameter with the special name of "*".
The "~" in the pattern is a reminder that a pattern is treated as a
relative or "internal" path. Typically, the pattern refers to the root
of the Web server. For example, the pattern "~/foo/bar" will match the
URI http://localhost/foo/bar by default. This can be changed by
setting the server's basepath member. For example, setting the
basepath to "/abc/def", will mean the pattern will match the URI
http://localhost/abc/def/foo/bar instead.
The example has two patterns:
The "~/" pattern matches the empty path (e.g. http://localhost:1024).
The "~/:greeting" matches any URI path with exactly one segment,
assigning that segment to the value of the path parameter named
"greeting". For example, it will match http://localhost:1024/Hello
and sets greeting to "Hello". But it will not match "/", "/a/b" or
"/a/b/c", since they don't have one segment in their path.
The order is important when registering rules to a pipeline, since
they are searched for in that order. For example, if a rule with the
pattern "~/foo/:bar" is registered before "~/foo/new", a request with
the URI path of "/foo/new" will always match the first rule (assigning
the value of "new" to the path parameter named "bar") and the second
rule will never be used (not unless the first request handler throws
a NoResponseProduced exception).
1.6. Request handler #
A request handler is a function (or static method) that is passed
the request as a [Request] object and returns a Future to a [Response]
object.
The example has two request handlers. The first one was registered
with a rule so it handles HTTP requests for the root URI
(e.g. http://localhost:1024). It generates a HTML page with hyperlinks
to the other page.
The [ResponseBuffered] class is used to produce the response. It has a
write method used to build up the body of the HTTP response. By
default, the status of the HTTP response is HTTP 200 OK.
Future<Response> handleTopLevel(Request req) async {
final resp = ResponseBuffered(ContentType.html);
final helloUrl = req.rewriteUrl('~/Hello');
final gDayUrl = req.rewriteUrl("~/G'day");
resp.write('''
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Woomera Tutorial</title>
</head>
<body>
<h1>Woomera Tutorial</h1>
<ul>
<li><a href="${HEsc.attr(helloUrl)}">Hello</a></li>
<li><a href="${HEsc.attr(gDayUrl)}">Good day</a></li>
</ul>
</body>
</html>
''');
return resp;
}
copied to clipboard
This example request handler shows the use of the
Request.rewriteUrl to convert a local path (i.e. one starting with
"~/") to the actual path the server will be using. The full path of
the deployed Web server can be changed by just changing the
[Server.basePath] property: the behaviour of the patterns and the
values in the responses will both be changed.
This example also shows the use of the [HEsc] class to encode values
for HTML documents. Special characters (e.g. <, > and &) will be
replaced by HTML entities.
The ResponseBufferd is the commonly used type of Response. Other
responses are: [ResponseRedirect] to produce a HTTP 303 Redirect,
[ResponseNoContent] to produce a HTTP 204 No Content response
without a body and [ResponseStream] to produce the HTTP body from a
stream source.
1.7. Processing path parameters #
The other request handler shows how path parameters can be used.
The [RequestParams.[]] operator on the request's pathParams property
obtains the value corresponding to a named path parameter.
The pattern was "~/:greeting", so the first segment will be assigned
to the path parameter named "greeting".
Future<Response> handleGreeting(Request req) async {
final greeting = req.pathParams['greeting'];
copied to clipboard
There are also query parameters which are accessed through the
request's queryParams member.
var name = req.queryParams['name'];
name = (name.isEmpty) ? 'world' : name;
copied to clipboard
So if the request URI was http://localhost:1024/Hello?name=Remi then
greeting will be assigned "Hello" and name will be assigned
"Remi".
1.8. Handling exceptions #
If a request handler throws an exception, it is passed to an
exception handler. The exception handler should produce the response
that gets sent back to the client.
There are three types of exception handlers a program can provide:
pipeline exception handlers can be registered per-pipeline. These
are useful for generating different error responses, depending on
which pipeline the matching request handler was registered to. For
example, an API pipeline could have a pipeline exception handler
that produces a JSON error response, while another pipeline has a
pipeline exception handler that produces a HTML error response.
a server exception handler can be registered on the server. It
handles exceptions thrown by a pipeline exception handler, or the
original exception if there was no pipeline exception handler.
a server raw exception handler handles exceptions thrown by the
server exception handler, when there is no server exception
handler and in some other special situations.
The example has a server exception handler.
The server is configured with it.
final ws = Server()
..exceptionHandler = myExceptionHandler
copied to clipboard
The server exception handler is passed the Request as well as the
exception object that was thrown and the stack trace of where it was
thrown from.
Like a request handler it is expected to return a Future to the
Response that will be used to produce the HTML response.
Future<Response> myExceptionHandler(
Request req, Object ex, StackTrace st) async {
...
}
copied to clipboard
The server exception handler will be invoked when any request
handler throws an exception.
It will also be invoked when the framework cannot obtain a Response
from any of the request handlers. The exception will be a
NotFoundException object and should result in the status of HTTP
404 Not Found.
This example treats all other exceptions as an internal error and
response with a status of HTTP 500 Internal server error. A more
useful server exception handler could generate different responses
depending on the exceptioin.
int status;
String message;
if (ex is NotFoundException) {
status =
ex.resourceExists ? HttpStatus.methodNotAllowed : HttpStatus.notFound;
message = 'Sorry, the page you were looking for could not be found.';
} else {
status = HttpStatus.internalServerError;
message = 'Sorry, an internal error occurred.';
print('Exception: $ex');
}
copied to clipboard
The HTTP status is a property of the Response. The default is HTTP
200 OK. Depending on the exception, the server exception handler in
the example sets it to either 404, 405 or 500.
return ResponseBuffered(ContentType.html)
..status = status
..write( ... );
}
copied to clipboard
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);
final _postParams = req.postParams;
if (_postParams != null) {
resp.write('<h2>POST parameters</h2>');
_dumpParam(_postParams, resp);
}
resp.write('''
</body>
</html>
''');
return resp;
}
void _dumpParam(RequestParams p, ResponseBuffered resp) {
final keys = p.keys;
if (keys.isNotEmpty) {
resp.write('<p>Number of keys: ${keys.length}</p>\n<dl>');
for (var k in keys) {
resp.write('<dt><code>${HEsc.text(k)}</code></dt><dd><ul>');
for (var v in p.values(k, raw: true)) {
resp.write('<li><code>${HEsc.text(v)}</code></li>');
}
resp.write('</ul></dd>');
}
resp.write('</dl>');
} else {
resp.write('<p>No parameters.</p>');
}
}
copied to clipboard
Here are a few URLs to try with the above example:
http://localhost:1024/demo/variable/aaa/bar/bbb
http://localhost:1024/demo/variable/aaa/bar/
http://localhost:1024/demo/variable/aaa/bar/ccc?x=ddd&y=eee&x=fff
http://localhost:1024/demo/wildcard/a/b/c
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');
}
copied to clipboard
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');
}
}
copied to clipboard
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'];
copied to clipboard
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);
copied to clipboard
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).
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 register the rules in a
particular order.
The other useful feature of pipelines is each pipeline can have its
own pipeline exception handler, in addition to the server's
exception handler. This is useful if exceptions from different sets
of request handlers should be handled differently. For example, there
could be an exception handler that generates a HTML error page and
another that generates an error in JSON.
4.3. Naming pipelines #
Every pipeline has a name. The default name is the emptty string, but
a different name can be provided to the ServerPipeline constructor.
final p1 = ServerPipeline('api');
final p2 = ServerPipeline('main');
copied to clipboard
Named pipelines are needed if using multiple pipelines with
annotations, since they identify which pipeline to associate a
request handler with.
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.
@ServerExceptionHandler()
Future<Response> myExceptionHandler(Request req
Object exception, StackTrace st) async {
var resp = ResponseBuffered(ContentType.html);
resp.write('''
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Error</title>
</head>
<body>
<h1>Error</h1>
<p>Sorry, an error occured: ${HEsc.text(exception.toString())}</p>
</body>
</html>
''');
return resp;
}
copied to clipboard
5.2.2. Pipeline exception handler
Each pipeline can also have its own exception handler.
final p1 = ServerPipeline()
..exceptionHandler = myExceptionHandler1;
final p2 = ServerPipeline('myCustomPipeline')
..exceptionHandler = myExceptionHandler2;
@Handles.pipelineExceptions()
Future<Response> myExceptionHandler1(Request req
Object exception, StackTrace st) async {
// for the default pipeline
}
@Handles.pipelineExceptions(pipeline: 'myCustomPipeline')
Future<Response> myExceptionHandler2(Request req
Object exception, StackTrace st) async {
// for the pipeline named "myCustomPipeline"
}
copied to clipboard
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.
@ServerExceptionHandlerRaw()
Future<void> myLowLevelExceptionHandler(
HttpRequest rawRequest, String requestId, Object ex, StackTrace st) async {
final resp = rawRequest.response;
resp
..statusCode = HttpStatus.internalServerError
..headers.contentType = ContentType.html
..write('''<!DOCTYPE html>
<html>
...
</html>
''');
await resp.close();
}
copied to clipboard
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. Annotations #
Maintaining the code for a large Web server gets more complicated as
the number of pipelines, request handlers and exception handlers
grows. Code changes need to occur in two places: where the function is
defined and where it is registered with a pipeline or server. For
example, it is easy to accidentally create a request handler
function and forget to register it against a pipeline.
Annotations can be used to help manage the code. The server and
pipelines can be automatically generated from the annotations.
See the
woomera_server_gen
package for one way annotations can be used.
Feedback #
Please report bugs by opening an
issue in GitHub.
For personal and professional use. You cannot resell or redistribute these repositories in their original state.
There are no reviews.