Routing HTTP and Static Files with the chat httpServer sample
I’ve been playing some more with the chat sample httpServer.dart, and created a trivial routable http server.
The main function that imports, setsup and uses the RoutableHttpServer.dart is listed below, but here’s a quick run-through of the key points:
- RoutableHttpServer extends HttpServerImplementation from the chat sample by adding the add(route,method,handler) function to add a named route, and addStaticFileHandler(route,path) to allow serving static files.
- You can add named routes, and add a handler to each named route (eg: in the url http://127.0.0.1:8080/hello – /hello is the named route – if the url matches /hello, then this will be handled by the /hello handler).
- It throws 404 not found for routes that aren’t defined, and 500 server error (with stack trace) if an exception is thrown.
Caveats – this is a work in progress, and just what I could create in about an hour.
- Although you can specify the method (eg, GET, POST etc…) this is ignored.
- Route matching is trivial – if your url ends with the same route, you’ll get a match
- Static files only works for the specific folder you specify (ie, not any subfolders).
Big-time todo’s
- try to derive the mime type from the file extension
- implement proper route matching pattern matching (to allow /hello/{id}/blah which would match /hello/123/blah
- match also based upon method + path combination
- much better static file handling – eg, sub folders and such.
- better error handling
Anyway, the code is on github, here^, and the main function so that you can get an idea of how it is used, is below.
Instructions:
- download the two files from github
- change the relative path in the RoutableHttpServer.dart to point to ../PATH/TO/samples/chat/http.dart
- run httptest.dart from the IDE or dart VM with >dart.exe httptest.dart
- Navigate to the following url’s to test…
http://127.0.0.1:8080/hello?name=Chris <— you can change this name, or omit it
http://127.0.0.1:8080/exception <—- will throw an exception and return HTTP error 500
http://127.0.0.1:8080/static/RoutableHttpServer.dart <– will serve up the .dart file as plain text.
Hopefully I’ll find some time to fix the TODO’s – note, this is based upon the sample HTTPServer. A real one that forms part of the SDK should be appearing soon in the dart libraries.
(Update 23/02/2012: There is now a Part 2 to this article which adds “toy” sessions)
//httptest.dart
#import("RoutableHttpServer.dart");
main() {
RoutableHttpServer httpServer = new RoutableHttpServer();
/* ADD SOME SAMPLE ROUTES */
// http://127.0.0.1:8080/hello?name=Dart
httpServer.add("/hello", "GET", (req,res) {
try {
String name = "";
if (req.queryParameters.containsKey("name")) {
name = req.queryParameters["name"];
res.writeString("Hello ${name}");
}
else {
res.writeString("Who are you? - try /hello?name=Chris");
}
}
finally {
res.writeDone(); //TODO: Put in finally.
}
});
// http://127.0.0.1:8080/exception
httpServer.add("/exception", "GET", (req,res) {
res.foo(); //will throw a noSuchMethod exception
});
//will serve any files in the current folder "."
//when called with a url such as http://127.0.0.1:8080/static/myfile.html
// http://127.0.0.1:8080/static/http.dart
httpServer.addStaticFileHandler("/static", ".");
//Start listening
httpServer.go("127.0.0.1",8080);
}
//RoutableHttpServer.dart
#library("RoutableHttpServer");
#import("dart:io");
//would prefer to use this - but it doesn't work at the moment for dart vm
//#import("http://dart.googlecode.com/svn/branches/bleeding_edge/dart/samples/chat/http.dart");
#import("../../../../work/dart/dart-unstable-4349/dart/samples/chat/http.dart");
/**
*/
typedef HttpRequestHandlerFunction (HTTPRequest req, HTTPResponse);
class RoutableHttpServer extends HTTPServerImplementation {
RoutableHttpServer() :
_routes = new Map<String, HttpRequestHandlerFunction>()
{
//default constructor
}
/**
* When a request which matches the requestPath and method comes in,
* we will invoke the handler.
*/
add(String requestPath, String method, HttpRequestHandlerFunction handler) {
//TODO - CJB: Currently ignores the method
_routes[requestPath] = (req, res) => handler(req,res);
}
/**
* When a request which matches the requestPath comes in,
* we will serve up a file with the matching name in the absoluteDiskFolder
* Only works for "GET" method.
*/
addStaticFileHandler(String requestPath, String absoluteDiskFolder) {
//TODO - CJB: Make this only work for "GET" method
_routes[requestPath] = (req, res) => _serveStaticFile(req.path, res, absoluteDiskFolder);
}
/**
* Start listening...
*/
go(String host, int port) {
listen(host, port, _onConnection);
print("Listening on ${host}:${port}");
}
/**
* when a connection is received, find the correct route by pattern matching
and method.
If we can't find a correct route, return 404
*/
_onConnection(HTTPRequest req, HTTPResponse res) {
//does the request path match any specific route in th map?
print("${req.method}: ${req.path}");
HttpRequestHandlerFunction handler = _findCorrectHandler(req.path, req.method);
if (handler != null) {
try {
//call the handler
handler(req,res);
}
catch (Exception ex, var stack) {
//error 500
_serverErrorHandler(ex,stack,res);
}
}
else {
//otherwise, 404
_notFoundHandler(res);
}
}
/**
* Return the correct route handler
*/
HttpRequestHandlerFunction _findCorrectHandler(String path, String method) {
//very trivial implementation just to see if this works
//TODO - CJB: should do proper pattern matching and also take account of the request method
for (String key in _routes.getKeys()) {
if (path.startsWith(key)) {
//found, so return.
print("found matching route key=${key} path=${path}");
return _routes[key];
}
}
//not found
return null;
}
_serveStaticFile(String requestPath, HTTPResponse res, String absoluteDiskPath) {
print("GET: static file: " + requestPath);
//TODO - CJB: This is quick and dirty. Better would be just to test if the
//file requested actually existed.
_getFileList(absoluteDiskPath,(List<String> fileList) {
String requestedFile = requestPath.split("/").last();
print("requestedFile: ${requestedFile}");
bool fileFound = false;
for (String file in fileList) {
if (file.endsWith(requestedFile)) {
print("found file: ${file}");
fileFound = true;
//TODO - CJB: Change this to use file async
File f = new File(file);
RandomAccessFile raf = f.openSync();
List buffer = new List(raf.lengthSync());
raf.readListSync(buffer, 0, raf.lengthSync());
print("closing file");
raf.close();
res.writeList(buffer, 0, buffer.length);
//TODO - CJB: Try and guess the mime type by the extension.
}
else {
//TODO: quick and dirty - get rid of.
print("file ${file} doesn't end with ${requestedFile}");
}
}
if (fileFound) {
//TODO: should be in a finally
res.writeDone();
}
else {
_notFoundHandler(res);
}
});
}
/**
* handle not found.
*/
_notFoundHandler(HTTPResponseImplementation res) {
//if not, then not found.
res.statusCode = HTTPStatus.NOT_FOUND;
res.setHeader("Content-Type", "text/plain");
res.writeString("404 - Not found");
res.writeDone(); //TODO: Put in a finally
}
/**
* handle server error
*/
_serverErrorHandler(ex,stack,HTTPResponseImplementation res) {
res.statusCode = HTTPStatus.INTERNAL_SERVER_ERROR;
res.setHeader("Content-Type", "text/plain");
res.writeString("Exception: ${ex}");
res.writeString("\n");
res.writeString("Stack: ${stack}");
res.writeDone(); //TODO: put in a finally
}
/**
* returns a list of files in the current folder
*/
_getFileList(folder, onComplete) {
List<String> result = new List<String>();
Directory dir = new Directory(folder);
dir.fileHandler = (fileName) {
print(fileName);
result.add(fileName);
};
dir.doneHandler = (value) {
onComplete(result);
};
dir.errorHandler = (err) {
print("Error: ${err}");
onComplete(result);
};
dir.list();
}
//Contains the list of routes and handlers
Map<String, HttpRequestHandlerFunction> _routes;
}

No comments:
Post a Comment