Wednesday, February 22, 2012

Routing HTTP and Static Files with the chat httpServer sample


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:
  1. download the two files from github
  2. change the relative path in the RoutableHttpServer.dart to point to ../PATH/TO/samples/chat/http.dart
  3. run httptest.dart from the IDE or dart VM with >dart.exe httptest.dart
  4. 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;

}
Image below for social media…

No comments:

Post a Comment