Sunday, November 11, 2012

Serving Public File in Dart HTTP Servers

‹prev | My Chain | next›

Tonight, I continue my exploration of server-side Dart as I build on my simple web server. So far, I have been able to get it to respond to different HTTP methods (POST, GET, etc.), serve up JSON, and even send a file from the filesystem. Best of all, it was quite painless to get it all working. The Dart HttpServer and related classes are very nice to work with.

Tonight, I would like to build on the ability to serve files from the backend by serving not just a single file, but any files in my public directory. To serve a single file, I am using the following:
#import('dart:io');

main() {
  HttpServer app = new HttpServer();

  app.addRequestHandler(
    (req) => req.method == 'GET' && req.path == '/',
    (req, res) {
      var file = new File('public/index.html');
      var stream = file.openInputStream();
      stream.pipe(res.outputStream);
    }
  );

  app.listen('127.0.0.1', 8000);
}
There is nothing too fancy there. The first argument to addRequestHandler is a matcher function that determines if this handler matches the incoming request. The second argument actually performs the response, streaming the file contents into the response output stream.

To get this to work for all files in the public directory, I will need to modify both the matcher and the responder.

The matcher needs to return true if the requested file exists in the public directory. I think that this needs to be a synchronous check. That is, I do not believe that I can rely on a Dart Future from the regular exists method in the File class. So instead, I start with the following matching code:
main() {
  // ...
  app.addRequestHandler(
    (req) {
      if (req.method != 'GET') return false;

      String path = publicPath(req.path);
      if (path == null) return false;

      return true;
    },
    (req, res) { /* ... */ }
  );
  // ...
}

String publicPath(String path) {
  if (pathExists("public$path")) return "public$path";
  if (pathExists("public$path/index.html")) return "public$path/index.html";
}

boolean pathExists(String path) => new File(path).existsSync();
The public path consists of two checks. First, it looks for the exact match. So, if the request is for /scripts/web/main.dart, then public/scripts/web/main.dart will be checked for existence with existsSync(). If that does not exist, a secondary check is made for an index.html. Then, if neither is found, nothing is returned, which causes the matcher function to return false signifying that it will not handle this request.

As for the reponder in addRequestHandler, the following will work:
  app.addRequestHandler(
    (req) { /* ... */ },
    (req, res) {
      var file = new File(publicPath(req.path));
      var stream = file.openInputStream();
      stream.pipe(res.outputStream);
    }
  );
I simply re-use the same publicPath function to determine which file to open.

And that works. I can open the root URL:
➜  public git:(app.dart) ✗ curl -i http://localhost:8000/    
HTTP/1.1 200 OK
transfer-encoding: chunked

<!DOCTYPE html>
<html>
<head>
  <title>Dart Comics</title>
...
I can request files deep withing the public directory:
➜  public git:(app.dart) ✗ curl -i http://localhost:8000/scripts/web/main.dart
HTTP/1.1 200 OK
transfer-encoding: chunked

#import('Collections.Comics.dart', prefix: 'Collections');
#import('Views.Comics.dart', prefix: 'Views');
#import('Views.AddComic.dart', prefix: 'Views');

#import('dart:html');
#import('dart:json');

#import('package:hipster_mvc/hipster_sync.dart');

main() {
...
And I get proper 404s for resources that do not exist on the filesystem:
➜  public git:(app.dart) ✗ curl -i http://localhost:8000/asdf                 
HTTP/1.1 404 Not Found
content-length: 0
This is not a perfect solution as I have to make not one, but two blocking calls to publicPath (and the underlying existsSync() method).

Unfortunately, my initial attempt at getting around this does not work. I had thought to set a "local_path" header on the request:
app.addRequestHandler(
    (req) {
      if (req.method != 'GET') return false;

      String path = publicPath(req.path);
      if (path == null) return false;

      req.headers.add('local_path', path);
      return true;
    },
    (req, res) { /* ... */ }
  }
But this results in a non-mutable header exception:
➜  dart-comics git:(app.dart) ✗ ~/local/dart/dart-sdk/bin/dart app.dart
Unhandled exception:
HttpException: HTTP headers are not mutable
#0      _HttpServer.listenOn.onConnection.<anonymous closure> (dart:io:3144:11)
#1      _HttpConnection._onConnectionClosed (dart:io:3055:14)
#2      _HttpConnectionBase._onError (dart:io:3002:24)
#3      _HttpConnection._HttpConnection.<anonymous closure> (dart:io:3051:40)
#4      _HttpParser.writeList (dart:io:4165:14)
#5      _HttpConnectionBase._onData._onData (dart:io:2983:41)
#6      _SocketBase._multiplex (dart:io:5846:26)
#7      _SocketBase._sendToEventHandler.<anonymous closure> (dart:io:5947:20)
#8      _ReceivePortImpl._handleMessage (dart:isolate-patch:37:92)
With a working solution under my belt, I call it a night here. I will pick back up tomorrow to see if I can figure out a way to communicate the local path information between the matcher and the responder callbacks. That little problem aside, this HTTP server in Dart continues to work well.


Day #567

No comments:

Post a Comment