Friday, July 26, 2013

CORS on a Dart Server


There were a lot of really good questions after my OSCON talk on Dart. I think I was able to answer most of the questions statisfactorily and accurately, but to one I gave the wrong answer.

The question was how to make an HttpRequest to a different origin site. I thought that once the CORS headers were set properly on the server, there was a setting in HttpRequest that was all that was required. I followed up afterwards and found that the questioner had solved the problem in the meantime—in this case, it was hitting the wrong server (one without CORS headers set).

But it occurs to me that I have not tried this myself. In fact, I have specifically side-stepped the problem in the Hipster MVC test suite by only running my browser-based tests from content_shell. This utility which evaluates and analyzes a browser page also doubles as test harness by loading a test context page and sending the console output to STDOUT on the command line.

What is side-stepping my CORS question is that content_shell is able to make localhost connections to a test server. When I run the same test suite accessing the same test server from the Dartium browser, I get a bunch of errors like:
OPTIONS http://localhost:31337/widgets/ALL Origin null is not allowed by Access-Control-Allow-Origin. localhost:31337/widgets/ALL:1
XMLHttpRequest cannot load http://localhost:31337/widgets/ALL. Origin null is not allowed by Access-Control-Allow-Origin. index.html:1
OPTIONS http://localhost:31337/widgets/ALL Origin null is not allowed by Access-Control-Allow-Origin.
...
I am unsure how an OPTIONS request for cross origin request checking should respond in this case. Also, I am on a plane and too cheap to pay the $20 just to look up the answer, so I am going to start by replying with a simple 200 with no response body. In my Dart test server I do this as:
  HttpServer.bind('127.0.0.1', 31337).then((app) {
    app.listen((HttpRequest req) {
      if (req.method == 'OPTIONS') {
        HttpResponse res = req.response;
        res.statusCode = HttpStatus.OK;
        res.close();
        return;
      }
      // Handle other requests...
    });
  });
Nothing too fancy there—it reads quite well. If the current request is an OPTIONS, then I grab the response property from the incoming request object, set a 200/OK status code on it, and close the response, signalling that processing of this response is complete. I then return from the request stream so that no other handling is attemped.

But after restarting the server and re-running my tests, I still get the same errors in the Dartium console. So next I add the header that is being complained about, Access-Control-Allow-Origin to the response headers:

  HttpServer.bind('127.0.0.1', port).then((app) {
    app.listen((HttpRequest req) {
       HttpResponse res = req.response;
       res.headers
         ..add('Access-Control-Allow-Origin', 'null');

      if (req.method == 'OPTIONS') { /* ... */ }
      // Handle other requests...
    });
  });
I am fairly sure that a value of * would work here as well (confirmed later), but I start with the most restricted value that can possibly work—the “null” value currently supplied by Dartium when running a file:// test context page.

With that change applied to my test server, my tests still fail. But now with a new error message. Instead of complaing about the Access-Control-Allow-Origin header, Dart is complaining about Access-Control-Allow-Headers:
OPTIONS http://localhost:31337/widgets Request header field Content-type is not allowed by Access-Control-Allow-Headers. localhost:31337/widgets:1
Phew! So at least I am on the right track. It is never fun to try a theory and see no change at all.

To change this error, I add the apparently required header to my test server:
  HttpServer.bind('127.0.0.1', port).then((app) {
    app.listen((HttpRequest req) {
      HttpResponse res = req.response;
      res.headers
        ..add('Access-Control-Allow-Origin', 'null')
        ..add('Access-Control-Allow-Headers', 'Content-Type');

      if (req.method == 'OPTIONS') { /* ... */ }
      // Handle other requests...
    });
  });
I then get a similar (but different) warning:
OPTIONS http://localhost:31337/widgets/ALL Method DELETE is not allowed by Access-Control-Allow-Methods. 
I fix that with year another header:
  HttpServer.bind('127.0.0.1', port).then((app) {
    app.listen((HttpRequest req) {
      HttpResponse res = req.response;
      res.headers
        ..add('Access-Control-Allow-Origin', 'null')
        ..add('Access-Control-Allow-Headers', 'Content-Type')
        ..add('Access-Control-Allow-Methods', 'DELETE,PUT');

      if (req.method == 'OPTIONS') { /* ... */ }
      // Handle other requests...
    });
  });
(the PUT comes from another message that I see from one of the tests)

With that, I have eliminated all of my errors and have my test suite passing—now from both the browser and content shell:
➜  test git:(http-test-server) ✗ content_shell --dump-render-tree index.html
CONSOLE MESSAGE: unittest-suite-wait-for-done
CONSOLE MESSAGE: PASS: unsupported remove
CONSOLE MESSAGE: PASS: Hipster Sync can parse regular JSON
CONSOLE MESSAGE: PASS: Hipster Sync can parse empty responses
CONSOLE MESSAGE: PASS: Hipster Sync HTTP get it can parse responses
CONSOLE MESSAGE: PASS: Hipster Sync HTTP post it can POST new records
CONSOLE MESSAGE: PASS: Hipster Sync (w/ a pre-existing record) HTTP PUT: can update existing records
CONSOLE MESSAGE: PASS: Hipster Sync (w/ a pre-existing record) HTTP DELETE: can remove the record from the store
CONSOLE MESSAGE: PASS: Hipster Sync (w/ multiple pre-existing records) can retrieve a collection of records

CONSOLE MESSAGE:
CONSOLE MESSAGE: All 8 tests passed.
CONSOLE MESSAGE: unittest-suite-success
In the end, my response at the OSCON session was misleading (sorry about that). I had indicated that only a single parameter need be set in the HttpRequest constructor. I think I was likely thinking of the withCredentials option, which will send cookies and similar authorization data to the CORS server. The answer is that nothing else is needed. As long as the server sets the appropriate headers, which I can do now in my Dart server, the the HttpRequest object from dart:html will just work.


Day #824

No comments:

Post a Comment