Browse Source

http: improve http parser bindings

Speeds up HTTP benchmarks by 10% on average.
v0.7.4-release
Ben Noordhuis 13 years ago
parent
commit
84d0b1bcc5
  1. 98
      lib/http.js
  2. 365
      src/node_http_parser.cc
  3. 506
      test/simple/test-http-parser.js

98
lib/http.js

@ -37,58 +37,51 @@ if (process.env.NODE_DEBUG && /http/.test(process.env.NODE_DEBUG)) {
var parsers = new FreeList('parsers', 1000, function() { var parsers = new FreeList('parsers', 1000, function() {
var parser = new HTTPParser('request'); var parser = new HTTPParser(HTTPParser.REQUEST);
parser.onMessageBegin = function() { parser._headers = [];
parser.incoming = new IncomingMessage(parser.socket); parser._url = '';
parser.field = null;
parser.value = null; // Only called in the slow case where slow means
}; // that the request headers were either fragmented
// across multiple TCP packets or too large to be
// Only servers will get URL events. // processed in a single run. This method is also
parser.onURL = function(b, start, len) { // called to process trailing HTTP headers.
var slice = b.toString('ascii', start, start + len); parser.onHeaders = function(headers, url) {
if (parser.incoming.url) { parser._headers = parser._headers.concat(headers);
parser.incoming.url += slice; parser._url += url;
} else {
// Almost always will branch here.
parser.incoming.url = slice;
}
}; };
parser.onHeaderField = function(b, start, len) { // info.headers and info.url are set only if .onHeaders()
var slice = b.toString('ascii', start, start + len).toLowerCase(); // has not been called for this request.
if (parser.value != undefined) { //
parser.incoming._addHeaderLine(parser.field, parser.value); // info.url is not set for response parsers but that's not
parser.field = null; // applicable here since all our parsers are request parsers.
parser.value = null; parser.onHeadersComplete = function(info) {
} var headers = info.headers;
if (parser.field) { var url = info.url;
parser.field += slice;
} else {
parser.field = slice;
}
};
parser.onHeaderValue = function(b, start, len) { if (!headers) {
var slice = b.toString('ascii', start, start + len); headers = parser._headers;
if (parser.value) { parser._headers = [];
parser.value += slice;
} else {
parser.value = slice;
} }
};
parser.onHeadersComplete = function(info) { if (!url) {
if (parser.field && (parser.value != undefined)) { url = parser._url;
parser.incoming._addHeaderLine(parser.field, parser.value); parser._url = '';
parser.field = null;
parser.value = null;
} }
parser.incoming = new IncomingMessage(parser.socket);
parser.incoming.httpVersionMajor = info.versionMajor; parser.incoming.httpVersionMajor = info.versionMajor;
parser.incoming.httpVersionMinor = info.versionMinor; parser.incoming.httpVersionMinor = info.versionMinor;
parser.incoming.httpVersion = info.versionMajor + '.' + info.versionMinor; parser.incoming.httpVersion = info.versionMajor + '.' + info.versionMinor;
parser.incoming.url = url;
for (var i = 0, n = headers.length; i < n; i += 2) {
var k = headers[i];
var v = headers[i + 1];
parser.incoming._addHeaderLine(k.toLowerCase(), v);
}
if (info.method) { if (info.method) {
// server only // server only
@ -96,6 +89,7 @@ var parsers = new FreeList('parsers', 1000, function() {
} else { } else {
// client only // client only
parser.incoming.statusCode = info.statusCode; parser.incoming.statusCode = info.statusCode;
// CHECKME dead code? we're always a request parser
} }
parser.incoming.upgrade = info.upgrade; parser.incoming.upgrade = info.upgrade;
@ -123,10 +117,20 @@ var parsers = new FreeList('parsers', 1000, function() {
}; };
parser.onMessageComplete = function() { parser.onMessageComplete = function() {
this.incoming.complete = true; parser.incoming.complete = true;
if (parser.field && (parser.value != undefined)) {
parser.incoming._addHeaderLine(parser.field, parser.value); // Emit any trailing headers.
var headers = parser._headers;
if (headers) {
for (var i = 0, n = headers.length; i < n; i += 2) {
var k = headers[i];
var v = headers[i + 1];
parser.incoming._addHeaderLine(k.toLowerCase(), v);
} }
parser._headers = [];
parser._url = '';
}
if (!parser.incoming.upgrade) { if (!parser.incoming.upgrade) {
// For upgraded connections, also emit this after parser.execute // For upgraded connections, also emit this after parser.execute
parser.incoming.readable = false; parser.incoming.readable = false;
@ -1088,7 +1092,7 @@ ClientRequest.prototype.onSocket = function(socket) {
var parser = parsers.alloc(); var parser = parsers.alloc();
req.socket = socket; req.socket = socket;
req.connection = socket; req.connection = socket;
parser.reinitialize('response'); parser.reinitialize(HTTPParser.RESPONSE);
parser.socket = socket; parser.socket = socket;
parser.incoming = null; parser.incoming = null;
req.parser = parser; req.parser = parser;
@ -1346,7 +1350,7 @@ function connectionListener(socket) {
}); });
var parser = parsers.alloc(); var parser = parsers.alloc();
parser.reinitialize('request'); parser.reinitialize(HTTPParser.REQUEST);
parser.socket = socket; parser.socket = socket;
parser.incoming = null; parser.incoming = null;

365
src/node_http_parser.cc

@ -51,10 +51,7 @@ namespace node {
using namespace v8; using namespace v8;
static Persistent<String> on_message_begin_sym; static Persistent<String> on_headers_sym;
static Persistent<String> on_url_sym;
static Persistent<String> on_header_field_sym;
static Persistent<String> on_header_value_sym;
static Persistent<String> on_headers_complete_sym; static Persistent<String> on_headers_complete_sym;
static Persistent<String> on_body_sym; static Persistent<String> on_body_sym;
static Persistent<String> on_message_complete_sym; static Persistent<String> on_message_complete_sym;
@ -92,6 +89,8 @@ static Persistent<String> version_major_sym;
static Persistent<String> version_minor_sym; static Persistent<String> version_minor_sym;
static Persistent<String> should_keep_alive_sym; static Persistent<String> should_keep_alive_sym;
static Persistent<String> upgrade_sym; static Persistent<String> upgrade_sym;
static Persistent<String> headers_sym;
static Persistent<String> url_sym;
static struct http_parser_settings settings; static struct http_parser_settings settings;
@ -104,43 +103,29 @@ static char* current_buffer_data;
static size_t current_buffer_len; static size_t current_buffer_len;
// Callback prototype for http_cb #if defined(__GNUC__)
#define DEFINE_HTTP_CB(name) \ #define always_inline __attribute__((always_inline))
static int name(http_parser *p) { \ #elif defined(_MSC_VER)
Parser *parser = static_cast<Parser*>(p->data); \ #define always_inline __forceinline
Local<Value> cb_value = parser->handle_->Get(name##_sym); \ #else
if (!cb_value->IsFunction()) return 0; \ #define always_inline
Local<Function> cb = Local<Function>::Cast(cb_value); \ #endif
Local<Value> ret = cb->Call(parser->handle_, 0, NULL); \
if (ret.IsEmpty()) { \
parser->got_exception_ = true; \ #define HTTP_CB(name) \
return -1; \ static int name(http_parser* p_) { \
} else { \ Parser* self = container_of(p_, Parser, parser_); \
return 0; \ return self->name##_(); \
} \ } \
} int always_inline name##_()
// Callback prototype for http_data_cb #define HTTP_DATA_CB(name) \
#define DEFINE_HTTP_DATA_CB(name) \ static int name(http_parser* p_, const char* at, size_t length) { \
static int name(http_parser *p, const char *at, size_t length) { \ Parser* self = container_of(p_, Parser, parser_); \
Parser *parser = static_cast<Parser*>(p->data); \ return self->name##_(at, length); \
assert(current_buffer); \
Local<Value> cb_value = parser->handle_->Get(name##_sym); \
if (!cb_value->IsFunction()) return 0; \
Local<Function> cb = Local<Function>::Cast(cb_value); \
Local<Value> argv[3] = { *current_buffer \
, Integer::New(at - current_buffer_data) \
, Integer::New(length) \
}; \
Local<Value> ret = cb->Call(parser->handle_, 3, argv); \
assert(current_buffer); \
if (ret.IsEmpty()) { \
parser->got_exception_ = true; \
return -1; \
} else { \
return 0; \
} \ } \
} int always_inline name##_(const char* at, size_t length)
static inline Persistent<String> static inline Persistent<String>
@ -175,85 +160,241 @@ method_to_str(unsigned short m) {
} }
// helper class for the Parser
struct StringPtr {
StringPtr() {
on_heap_ = false;
Reset();
}
~StringPtr() {
Reset();
}
void Reset() {
if (on_heap_) {
delete[] str_;
on_heap_ = false;
}
str_ = NULL;
size_ = 0;
}
void Update(const char* str, size_t size) {
if (str_ == NULL)
str_ = str;
else if (on_heap_ || str_ + size != str) {
// Non-consecutive input, make a copy on the heap.
// TODO Use slab allocation, O(n) allocs is bad.
char* s = new char[size_ + size];
memcpy(s, str_, size_);
memcpy(s + size_, str, size);
if (on_heap_)
delete[] str_;
else
on_heap_ = true;
str_ = s;
}
size_ += size;
}
Handle<String> ToString() const {
if (str_)
return String::New(str_, size_);
else
return String::Empty();
}
const char* str_;
bool on_heap_;
size_t size_;
};
class Parser : public ObjectWrap { class Parser : public ObjectWrap {
public: public:
Parser(enum http_parser_type type) : ObjectWrap() { Parser(enum http_parser_type type) : ObjectWrap() {
Init(type); Init(type);
} }
~Parser() { ~Parser() {
} }
DEFINE_HTTP_CB(on_message_begin)
DEFINE_HTTP_CB(on_message_complete)
DEFINE_HTTP_DATA_CB(on_url) HTTP_CB(on_message_begin) {
DEFINE_HTTP_DATA_CB(on_header_field) num_fields_ = num_values_ = -1;
DEFINE_HTTP_DATA_CB(on_header_value) url_.Reset();
DEFINE_HTTP_DATA_CB(on_body) return 0;
}
HTTP_DATA_CB(on_url) {
url_.Update(at, length);
return 0;
}
HTTP_DATA_CB(on_header_field) {
if (num_fields_ == num_values_) {
// start of new field name
if (++num_fields_ == ARRAY_SIZE(fields_)) {
Flush();
num_fields_ = 0;
num_values_ = -1;
}
fields_[num_fields_].Reset();
}
assert(num_fields_ < (int)ARRAY_SIZE(fields_));
assert(num_fields_ == num_values_ + 1);
fields_[num_fields_].Update(at, length);
return 0;
}
HTTP_DATA_CB(on_header_value) {
if (num_values_ != num_fields_) {
// start of new header value
values_[++num_values_].Reset();
}
assert(num_values_ < (int)ARRAY_SIZE(values_));
assert(num_values_ == num_fields_);
values_[num_values_].Update(at, length);
return 0;
}
static int on_headers_complete(http_parser *p) {
Parser *parser = static_cast<Parser*>(p->data);
Local<Value> cb_value = parser->handle_->Get(on_headers_complete_sym); HTTP_CB(on_headers_complete) {
if (!cb_value->IsFunction()) return 0; Local<Value> cb = handle_->Get(on_headers_complete_sym);
Local<Function> cb = Local<Function>::Cast(cb_value);
if (!cb->IsFunction())
return 0;
Local<Object> message_info = Object::New(); Local<Object> message_info = Object::New();
if (have_flushed_) {
// Slow case, flush remaining headers.
Flush();
}
else {
// Fast case, pass headers and URL to JS land.
message_info->Set(headers_sym, CreateHeaders());
if (parser_.type == HTTP_REQUEST)
message_info->Set(url_sym, url_.ToString());
}
num_fields_ = num_values_ = -1;
// METHOD // METHOD
if (p->type == HTTP_REQUEST) { if (parser_.type == HTTP_REQUEST) {
message_info->Set(method_sym, method_to_str(p->method)); message_info->Set(method_sym, method_to_str(parser_.method));
} }
// STATUS // STATUS
if (p->type == HTTP_RESPONSE) { if (parser_.type == HTTP_RESPONSE) {
message_info->Set(status_code_sym, Integer::New(p->status_code)); message_info->Set(status_code_sym, Integer::New(parser_.status_code));
} }
// VERSION // VERSION
message_info->Set(version_major_sym, Integer::New(p->http_major)); message_info->Set(version_major_sym, Integer::New(parser_.http_major));
message_info->Set(version_minor_sym, Integer::New(p->http_minor)); message_info->Set(version_minor_sym, Integer::New(parser_.http_minor));
message_info->Set(should_keep_alive_sym, message_info->Set(should_keep_alive_sym,
http_should_keep_alive(p) ? True() : False()); http_should_keep_alive(&parser_) ? True() : False());
message_info->Set(upgrade_sym, p->upgrade ? True() : False()); message_info->Set(upgrade_sym, parser_.upgrade ? True() : False());
Local<Value> argv[1] = { message_info }; Local<Value> argv[1] = { message_info };
Local<Value> head_response = cb->Call(parser->handle_, 1, argv); Local<Value> head_response =
Local<Function>::Cast(cb)->Call(handle_, 1, argv);
if (head_response.IsEmpty()) { if (head_response.IsEmpty()) {
parser->got_exception_ = true; got_exception_ = true;
return -1; return -1;
} else { }
return head_response->IsTrue() ? 1 : 0; return head_response->IsTrue() ? 1 : 0;
} }
HTTP_DATA_CB(on_body) {
HandleScope scope;
Local<Value> cb = handle_->Get(on_body_sym);
if (!cb->IsFunction())
return 0;
Handle<Value> argv[3] = {
*current_buffer,
Integer::New(at - current_buffer_data),
Integer::New(length)
};
Local<Value> r = Local<Function>::Cast(cb)->Call(handle_, 3, argv);
if (r.IsEmpty()) {
got_exception_ = true;
return -1;
} }
static Handle<Value> New(const Arguments& args) { return 0;
}
HTTP_CB(on_message_complete) {
HandleScope scope; HandleScope scope;
String::Utf8Value type(args[0]->ToString()); if (num_fields_ != -1)
Flush(); // Flush trailing HTTP headers.
Parser *parser; Local<Value> cb = handle_->Get(on_message_complete_sym);
if (0 == strcasecmp(*type, "request")) { if (!cb->IsFunction())
parser = new Parser(HTTP_REQUEST); return 0;
} else if (0 == strcasecmp(*type, "response")) {
parser = new Parser(HTTP_RESPONSE); Local<Value> r = Local<Function>::Cast(cb)->Call(handle_, 0, NULL);
} else {
return ThrowException(Exception::Error( if (r.IsEmpty()) {
String::New("Constructor argument be 'request' or 'response'"))); got_exception_ = true;
return -1;
}
return 0;
}
static Handle<Value> New(const Arguments& args) {
HandleScope scope;
http_parser_type type =
static_cast<http_parser_type>(args[0]->Int32Value());
if (type != HTTP_REQUEST && type != HTTP_RESPONSE) {
return ThrowException(Exception::Error(String::New(
"Argument must be HTTPParser.REQUEST or HTTPParser.RESPONSE")));
} }
Parser* parser = new Parser(type);
parser->Wrap(args.This()); parser->Wrap(args.This());
return args.This(); return args.This();
} }
// var bytesParsed = parser->execute(buffer, off, len); // var bytesParsed = parser->execute(buffer, off, len);
static Handle<Value> Execute(const Arguments& args) { static Handle<Value> Execute(const Arguments& args) {
HandleScope scope; HandleScope scope;
@ -321,6 +462,7 @@ class Parser : public ObjectWrap {
} }
} }
static Handle<Value> Finish(const Arguments& args) { static Handle<Value> Finish(const Arguments& args) {
HandleScope scope; HandleScope scope;
@ -343,33 +485,83 @@ class Parser : public ObjectWrap {
return Undefined(); return Undefined();
} }
static Handle<Value> Reinitialize(const Arguments& args) { static Handle<Value> Reinitialize(const Arguments& args) {
HandleScope scope; HandleScope scope;
Parser *parser = ObjectWrap::Unwrap<Parser>(args.This());
String::Utf8Value type(args[0]->ToString()); http_parser_type type =
static_cast<http_parser_type>(args[0]->Int32Value());
if (0 == strcasecmp(*type, "request")) { if (type != HTTP_REQUEST && type != HTTP_RESPONSE) {
parser->Init(HTTP_REQUEST); return ThrowException(Exception::Error(String::New(
} else if (0 == strcasecmp(*type, "response")) { "Argument must be HTTPParser.REQUEST or HTTPParser.RESPONSE")));
parser->Init(HTTP_RESPONSE);
} else {
return ThrowException(Exception::Error(
String::New("Argument be 'request' or 'response'")));
} }
Parser* parser = ObjectWrap::Unwrap<Parser>(args.This());
parser->Init(type);
return Undefined(); return Undefined();
} }
private: private:
Local<Array> CreateHeaders() {
// num_values_ is either -1 or the entry # of the last header
// so num_values_ == 0 means there's a single header
Local<Array> headers = Array::New(2 * (num_values_ + 1));
for (int i = 0; i < num_values_ + 1; ++i) {
headers->Set(2 * i, fields_[i].ToString());
headers->Set(2 * i + 1, values_[i].ToString());
}
return headers;
}
// spill headers and request path to JS land
void Flush() {
HandleScope scope;
Local<Value> cb = handle_->Get(on_headers_sym);
if (!cb->IsFunction())
return;
Handle<Value> argv[2] = {
CreateHeaders(),
url_.ToString()
};
Local<Value> r = Local<Function>::Cast(cb)->Call(handle_, 2, argv);
if (r.IsEmpty())
got_exception_ = true;
url_.Reset();
have_flushed_ = true;
}
void Init(enum http_parser_type type) { void Init(enum http_parser_type type) {
http_parser_init(&parser_, type); http_parser_init(&parser_, type);
parser_.data = this; url_.Reset();
num_fields_ = -1;
num_values_ = -1;
have_flushed_ = false;
got_exception_ = false;
} }
bool got_exception_;
http_parser parser_; http_parser parser_;
StringPtr fields_[32]; // header fields
StringPtr values_[32]; // header values
StringPtr url_;
int num_fields_;
int num_values_;
bool have_flushed_;
bool got_exception_;
}; };
@ -380,16 +572,17 @@ void InitHttpParser(Handle<Object> target) {
t->InstanceTemplate()->SetInternalFieldCount(1); t->InstanceTemplate()->SetInternalFieldCount(1);
t->SetClassName(String::NewSymbol("HTTPParser")); t->SetClassName(String::NewSymbol("HTTPParser"));
PropertyAttribute attrib = (PropertyAttribute) (ReadOnly | DontDelete);
t->Set(String::NewSymbol("REQUEST"), Integer::New(HTTP_REQUEST), attrib);
t->Set(String::NewSymbol("RESPONSE"), Integer::New(HTTP_RESPONSE), attrib);
NODE_SET_PROTOTYPE_METHOD(t, "execute", Parser::Execute); NODE_SET_PROTOTYPE_METHOD(t, "execute", Parser::Execute);
NODE_SET_PROTOTYPE_METHOD(t, "finish", Parser::Finish); NODE_SET_PROTOTYPE_METHOD(t, "finish", Parser::Finish);
NODE_SET_PROTOTYPE_METHOD(t, "reinitialize", Parser::Reinitialize); NODE_SET_PROTOTYPE_METHOD(t, "reinitialize", Parser::Reinitialize);
target->Set(String::NewSymbol("HTTPParser"), t->GetFunction()); target->Set(String::NewSymbol("HTTPParser"), t->GetFunction());
on_message_begin_sym = NODE_PSYMBOL("onMessageBegin"); on_headers_sym = NODE_PSYMBOL("onHeaders");
on_url_sym = NODE_PSYMBOL("onURL");
on_header_field_sym = NODE_PSYMBOL("onHeaderField");
on_header_value_sym = NODE_PSYMBOL("onHeaderValue");
on_headers_complete_sym = NODE_PSYMBOL("onHeadersComplete"); on_headers_complete_sym = NODE_PSYMBOL("onHeadersComplete");
on_body_sym = NODE_PSYMBOL("onBody"); on_body_sym = NODE_PSYMBOL("onBody");
on_message_complete_sym = NODE_PSYMBOL("onMessageComplete"); on_message_complete_sym = NODE_PSYMBOL("onMessageComplete");
@ -427,6 +620,8 @@ void InitHttpParser(Handle<Object> target) {
version_minor_sym = NODE_PSYMBOL("versionMinor"); version_minor_sym = NODE_PSYMBOL("versionMinor");
should_keep_alive_sym = NODE_PSYMBOL("shouldKeepAlive"); should_keep_alive_sym = NODE_PSYMBOL("shouldKeepAlive");
upgrade_sym = NODE_PSYMBOL("upgrade"); upgrade_sym = NODE_PSYMBOL("upgrade");
headers_sym = NODE_PSYMBOL("headers");
url_sym = NODE_PSYMBOL("url");
settings.on_message_begin = Parser::on_message_begin; settings.on_message_begin = Parser::on_message_begin;
settings.on_url = Parser::on_url; settings.on_url = Parser::on_url;

506
test/simple/test-http-parser.js

@ -22,55 +22,513 @@
var common = require('../common'); var common = require('../common');
var assert = require('assert'); var assert = require('assert');
var HTTPParser = process.binding('http_parser').HTTPParser;
var CRLF = "\r\n";
var REQUEST = HTTPParser.REQUEST;
var RESPONSE = HTTPParser.RESPONSE;
// The purpose of this test is not to check HTTP compliance but to test the // The purpose of this test is not to check HTTP compliance but to test the
// binding. Tests for pathological http messages should be submitted // binding. Tests for pathological http messages should be submitted
// upstream to http://github.com/ry/http-parser for inclusion into // upstream to http://github.com/ry/http-parser for inclusion into
// deps/http-parser/test.c // deps/http-parser/test.c
var HTTPParser = process.binding('http_parser').HTTPParser;
var parser = new HTTPParser('request');
var Buffer = require('buffer').Buffer; function newParser(type) {
var buffer = new Buffer(1024); var parser = new HTTPParser(type);
var request = 'GET /hello HTTP/1.1\r\n\r\n'; parser.headers = [];
parser.url = '';
buffer.write(request, 0, 'ascii'); parser.onHeaders = function(headers, url) {
parser.headers = parser.headers.concat(headers);
parser.url += url;
};
var callbacks = 0; parser.onHeadersComplete = function(info) {
};
parser.onMessageBegin = function() { parser.onBody = function(b, start, len) {
console.log('message begin'); assert.ok(false, 'Function should not be called.');
callbacks++;
}; };
parser.onHeadersComplete = function(info) { parser.onMessageComplete = function() {
console.log('headers complete: ' + JSON.stringify(info));
assert.equal('GET', info.method);
assert.equal(1, info.versionMajor);
assert.equal(1, info.versionMinor);
callbacks++;
}; };
parser.onURL = function(b, off, len) { return parser;
//throw new Error('hello world'); }
callbacks++;
function mustCall(f, times) {
var actual = 0;
process.setMaxListeners(256);
process.on('exit', function() {
assert.equal(actual, times || 1);
});
return function() {
actual++;
return f.apply(this, Array.prototype.slice.call(arguments));
}; };
}
function expectBody(expected) {
return mustCall(function(buf, start, len) {
var body = '' + buf.slice(start, start + len);
assert.equal(body, expected);
});
}
parser.execute(buffer, 0, request.length);
assert.equal(3, callbacks); //
// Simple request test.
//
(function() {
var request = Buffer(
'GET /hello HTTP/1.1' + CRLF +
CRLF
);
var parser = newParser(REQUEST);
parser.onHeadersComplete = mustCall(function(info) {
assert.equal(info.method, 'GET');
assert.equal(info.url || parser.url, '/hello');
assert.equal(info.versionMajor, 1);
assert.equal(info.versionMinor, 1);
});
parser.execute(request, 0, request.length);
// //
// Check that if we throw an error in the callbacks that error will be // Check that if we throw an error in the callbacks that error will be
// thrown from parser.execute() // thrown from parser.execute()
// //
parser.onURL = function(b, off, len) { parser.onHeadersComplete = function(info) {
throw new Error('hello world'); throw new Error('hello world');
}; };
parser.reinitialize(HTTPParser.REQUEST);
assert.throws(function() { assert.throws(function() {
parser.execute(buffer, 0, request.length); parser.execute(request, 0, request.length);
}, Error, 'hello world'); }, Error, 'hello world');
})();
//
// Simple response test.
//
(function() {
var request = Buffer(
'HTTP/1.1 200 OK' + CRLF +
'Content-Type: text/plain' + CRLF +
'Content-Length: 4' + CRLF +
CRLF +
'pong'
);
var parser = newParser(RESPONSE);
parser.onHeadersComplete = mustCall(function(info) {
assert.equal(info.method, undefined);
assert.equal(info.versionMajor, 1);
assert.equal(info.versionMinor, 1);
assert.equal(info.statusCode, 200);
});
parser.onBody = mustCall(function(buf, start, len) {
var body = '' + buf.slice(start, start + len);
assert.equal(body, 'pong');
});
parser.execute(request, 0, request.length);
})();
//
// Trailing headers.
//
(function() {
var request = Buffer(
'POST /it HTTP/1.1' + CRLF +
'Transfer-Encoding: chunked' + CRLF +
CRLF +
'4' + CRLF +
'ping' + CRLF +
'0' + CRLF +
'Vary: *' + CRLF +
'Content-Type: text/plain' + CRLF +
CRLF
);
var seen_body = false;
function onHeaders(headers, url) {
assert.ok(seen_body); // trailers should come after the body
assert.deepEqual(headers,
['Vary', '*', 'Content-Type', 'text/plain']);
}
var parser = newParser(REQUEST);
parser.onHeadersComplete = mustCall(function(info) {
assert.equal(info.method, 'POST');
assert.equal(info.url || parser.url, '/it');
assert.equal(info.versionMajor, 1);
assert.equal(info.versionMinor, 1);
// expect to see trailing headers now
parser.onHeaders = mustCall(onHeaders);
});
parser.onBody = mustCall(function(buf, start, len) {
var body = '' + buf.slice(start, start + len);
assert.equal(body, 'ping');
seen_body = true;
});
parser.execute(request, 0, request.length);
})();
//
// Test header ordering.
//
(function() {
var request = Buffer(
'GET / HTTP/1.0' + CRLF +
'X-Filler: 1337' + CRLF +
'X-Filler: 42' + CRLF +
'X-Filler2: 42' + CRLF +
CRLF
);
var parser = newParser(REQUEST);
parser.onHeadersComplete = mustCall(function(info) {
assert.equal(info.method, 'GET');
assert.equal(info.versionMajor, 1);
assert.equal(info.versionMinor, 0);
assert.deepEqual(info.headers || parser.headers,
['X-Filler', '1337',
'X-Filler', '42',
'X-Filler2', '42']);
});
parser.execute(request, 0, request.length);
})();
//
// Test large number of headers
//
(function() {
// 256 X-Filler headers
var lots_of_headers = 'X-Filler: 42' + CRLF;
for (var i = 0; i < 8; ++i) lots_of_headers += lots_of_headers;
var request = Buffer(
'GET /foo/bar/baz?quux=42#1337 HTTP/1.0' + CRLF +
lots_of_headers +
CRLF
);
var parser = newParser(REQUEST);
parser.onHeadersComplete = mustCall(function(info) {
assert.equal(info.method, 'GET');
assert.equal(info.url || parser.url, '/foo/bar/baz?quux=42#1337');
assert.equal(info.versionMajor, 1);
assert.equal(info.versionMinor, 0);
var headers = info.headers || parser.headers;
assert.equal(headers.length, 2 * 256); // 256 key/value pairs
for (var i = 0; i < headers.length; i += 2) {
assert.equal(headers[i], 'X-Filler');
assert.equal(headers[i + 1], '42');
}
});
parser.execute(request, 0, request.length);
})();
//
// Test request body
//
(function() {
var request = Buffer(
'POST /it HTTP/1.1' + CRLF +
'Content-Type: application/x-www-form-urlencoded' + CRLF +
'Content-Length: 15' + CRLF +
CRLF +
'foo=42&bar=1337'
);
var parser = newParser(REQUEST);
parser.onHeadersComplete = mustCall(function(info) {
assert.equal(info.method, 'POST');
assert.equal(info.url || parser.url, '/it');
assert.equal(info.versionMajor, 1);
assert.equal(info.versionMinor, 1);
});
parser.onBody = mustCall(function(buf, start, len) {
var body = '' + buf.slice(start, start + len);
assert.equal(body, 'foo=42&bar=1337');
});
parser.execute(request, 0, request.length);
})();
//
// Test chunked request body
//
(function() {
var request = Buffer(
'POST /it HTTP/1.1' + CRLF +
'Content-Type: text/plain' + CRLF +
'Transfer-Encoding: chunked' + CRLF +
CRLF +
'3' + CRLF +
'123' + CRLF +
'6' + CRLF +
'123456' + CRLF +
'A' + CRLF +
'1234567890' + CRLF +
'0' + CRLF
);
var parser = newParser(REQUEST);
parser.onHeadersComplete = mustCall(function(info) {
assert.equal(info.method, 'POST');
assert.equal(info.url || parser.url, '/it');
assert.equal(info.versionMajor, 1);
assert.equal(info.versionMinor, 1);
});
var body_part = 0, body_parts = ['123', '123456', '1234567890'];
function onBody(buf, start, len) {
var body = '' + buf.slice(start, start + len);
assert.equal(body, body_parts[body_part++]);
}
parser.onBody = mustCall(onBody, body_parts.length);
parser.execute(request, 0, request.length);
})();
//
// Test chunked request body spread over multiple buffers (packets)
//
(function() {
var request = Buffer(
'POST /it HTTP/1.1' + CRLF +
'Content-Type: text/plain' + CRLF +
'Transfer-Encoding: chunked' + CRLF +
CRLF +
'3' + CRLF +
'123' + CRLF +
'6' + CRLF +
'123456' + CRLF
);
var parser = newParser(REQUEST);
parser.onHeadersComplete = mustCall(function(info) {
assert.equal(info.method, 'POST');
assert.equal(info.url || parser.url, '/it');
assert.equal(info.versionMajor, 1);
assert.equal(info.versionMinor, 1);
});
var body_part = 0, body_parts = [
'123', '123456', '123456789',
'123456789ABC', '123456789ABCDEF' ];
function onBody(buf, start, len) {
var body = '' + buf.slice(start, start + len);
assert.equal(body, body_parts[body_part++]);
}
parser.onBody = mustCall(onBody, body_parts.length);
parser.execute(request, 0, request.length);
request = Buffer(
'9' + CRLF +
'123456789' + CRLF +
'C' + CRLF +
'123456789ABC' + CRLF +
'F' + CRLF +
'123456789ABCDEF' + CRLF +
'0' + CRLF
);
parser.execute(request, 0, request.length);
})();
//
// Stress test.
//
(function() {
var request = Buffer(
'POST /it HTTP/1.1' + CRLF +
'Content-Type: text/plain' + CRLF +
'Transfer-Encoding: chunked' + CRLF +
CRLF +
'3' + CRLF +
'123' + CRLF +
'6' + CRLF +
'123456' + CRLF +
'9' + CRLF +
'123456789' + CRLF +
'C' + CRLF +
'123456789ABC' + CRLF +
'F' + CRLF +
'123456789ABCDEF' + CRLF +
'0' + CRLF
);
function test(a, b) {
var parser = newParser(REQUEST);
parser.onHeadersComplete = mustCall(function(info) {
assert.equal(info.method, 'POST');
assert.equal(info.url || parser.url, '/it');
assert.equal(info.versionMajor, 1);
assert.equal(info.versionMinor, 1);
});
var expected_body = '123123456123456789123456789ABC123456789ABCDEF';
parser.onBody = function(buf, start, len) {
var chunk = '' + buf.slice(start, start + len);
assert.equal(expected_body.indexOf(chunk), 0);
expected_body = expected_body.slice(chunk.length);
};
parser.execute(a, 0, a.length);
parser.execute(b, 0, b.length);
assert.equal(expected_body, '');
}
for (var i = 1; i < request.length - 1; ++i) {
var a = request.slice(0, i);
var b = request.slice(i);
test(a, b);
}
})();
//
// Byte by byte test.
//
(function() {
var request = Buffer(
'POST /it HTTP/1.1' + CRLF +
'Content-Type: text/plain' + CRLF +
'Transfer-Encoding: chunked' + CRLF +
CRLF +
'3' + CRLF +
'123' + CRLF +
'6' + CRLF +
'123456' + CRLF +
'9' + CRLF +
'123456789' + CRLF +
'C' + CRLF +
'123456789ABC' + CRLF +
'F' + CRLF +
'123456789ABCDEF' + CRLF +
'0' + CRLF
);
var parser = newParser(REQUEST);
parser.onHeadersComplete = mustCall(function(info) {
assert.equal(info.method, 'POST');
assert.equal(info.url || parser.url, '/it');
assert.equal(info.versionMajor, 1);
assert.equal(info.versionMinor, 1);
assert.deepEqual(info.headers || parser.headers,
['Content-Type', 'text/plain',
'Transfer-Encoding','chunked']);
});
var expected_body = '123123456123456789123456789ABC123456789ABCDEF';
parser.onBody = function(buf, start, len) {
var chunk = '' + buf.slice(start, start + len);
assert.equal(expected_body.indexOf(chunk), 0);
expected_body = expected_body.slice(chunk.length);
};
for (var i = 0; i < request.length; ++i) {
parser.execute(request, i, 1);
}
assert.equal(expected_body, '');
})();
//
//
//
(function() {
var req1 = Buffer(
'PUT /this HTTP/1.1' + CRLF +
'Content-Type: text/plain' + CRLF +
'Transfer-Encoding: chunked' + CRLF +
CRLF +
'4' + CRLF +
'ping' + CRLF +
'0' + CRLF
);
var req2 = Buffer(
'POST /that HTTP/1.0' + CRLF +
'Content-Type: text/plain' + CRLF +
'Content-Length: 4' + CRLF +
CRLF +
'pong'
);
function onHeadersComplete1(info) {
assert.equal(info.method, 'PUT');
assert.equal(info.url, '/this');
assert.equal(info.versionMajor, 1);
assert.equal(info.versionMinor, 1);
assert.deepEqual(info.headers,
['Content-Type', 'text/plain',
'Transfer-Encoding', 'chunked']);
};
function onHeadersComplete2(info) {
assert.equal(info.method, 'POST');
assert.equal(info.url, '/that');
assert.equal(info.versionMajor, 1);
assert.equal(info.versionMinor, 0);
assert.deepEqual(info.headers,
['Content-Type', 'text/plain',
'Content-Length', '4']);
};
var parser = newParser(REQUEST);
parser.onHeadersComplete = onHeadersComplete1;
parser.onBody = expectBody('ping');
parser.execute(req1, 0, req1.length);
parser.reinitialize(REQUEST);
parser.onBody = expectBody('pong');
parser.onHeadersComplete = onHeadersComplete2;
parser.execute(req2, 0, req2.length);
})();
Loading…
Cancel
Save