Wednesday, April 25, 2012

Bad SPDY Headers

‹prev | My Chain | next›

I continue to struggle with implementing version 3 of the SPDY specification in node-spdy. I was finally able to get a response from inbound spdy/3 requests, but Chrome does not like the format of the response.

In fact, I have gotten to the point that Chrome actually recognizes that I am trying to speak spdy/3:

But, when I look at what Chrome thinks of the conversation, I see:
--> description = "Could not parse Spdy Control Frame Header."
--> status = 1
--> stream_id = 1
I have gone through the actual construction of the head a couple of times by hand and it looks OK. Of course, it's all writing bytes and lengths and things like that so it would be easy to make a mistake. Then there is the the Zlib compression...

I know that I have the Zlib inflation working because I can parse the inbound SPDY headers from Chrome. So a logical next step would be to attempt to parse the SPDY headers that I am generating. But before that, could it be as simple as missing those colon headers?

When I first parsed the inbound headers, I noticed some headers that started with colons:
   { ':host': 'localhost:3000',
     ':method': 'GET',
     ':path': '/',
     ':scheme': 'https',
     ':version': 'HTTP/1.1',
     // ...
I mistakenly thought something had gone wrong, but it turns out that they are part of the spec. And there are also colon headers that I am not including outbound: :status and :version. So I convert those headers over to be leading colon:
  // ...
  var dict = headersToDict(headers, function(headers) {
        headers[':status'] = code;
        headers[':version'] = 'HTTP/1.1';

  this._synFrame('SYN_REPLY', id, null, dict, callback);
But unfortunately, Chrome still refuses to accept the headers.

So, it seems that I have to run through the internal parser after all. And it turns out that something is in fact not quite right with the header.
var spdy = require('spdy');
var inflate = spdy.utils.zwrap(spdy.utils.createInflate());
var b = new Buffer([0x38, 0xac, 0xe3, 0xc6, 0xa7, 0xc2, 0x02, 0xe5, 0x0e, 0x50, 0x7a, 0xb4, 0x82, 0x27, 0x52, 0x66, 0x60, 0x22, 0x05, 0x25, 0x4b, 0x2b, 0xa4, 0x24, 0x0a, 0x00, 0x00, 0x00, 0xff, 0xff]);

inflate(b, function(err, chunks, length) {
This ends up logging what looks like the correct opening series of bytes, but not the correct ending.

Sigh.... it seems that I will be assembling headers manually tomorrow.

Update: I figured it out! It was a simple matter of not including the correct number of octets / bytes for the headers. When I converted from spdy/2 to spdy/3, I accounted for the increase from 16 bit to 32 bit header length values everywhere except when totalling the length. All that I needed was an 8 (4 octets for the keys and 4 for the values) every time that I added a new key value pair to the header:
  // ...
  var len = 4,
      pairs = Object.keys(loweredHeaders).filter(function(key) {
        var lkey = key.toLowerCase();
        return lkey !== 'connection' && lkey !== 'keep-alive' &&
               lkey !== 'proxy-connection' && lkey !== 'transfer-encoding';
      }).map(function(key) {
        var klen = Buffer.byteLength(key),
            value = stringify(loweredHeaders[key]),
            vlen = Buffer.byteLength(value);

        len += 8 + klen + vlen;
        return [klen, key, vlen, value];
      result = new Buffer(len);
I cannot believe that I caused myself two nights of pain for an 4 when I meant 8. Binary protocols: catch the fever!

At any rate, I finally have a spdy/3 express site:

Tomorrow comes the great cleanup. I have a ton of debug statements in here.

Day #367

No comments:

Post a Comment