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. 381
      src/node_http_parser.cc
  3. 522
      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 parser = new HTTPParser('request');
parser.onMessageBegin = function() {
parser.incoming = new IncomingMessage(parser.socket);
parser.field = null;
parser.value = null;
};
// Only servers will get URL events.
parser.onURL = function(b, start, len) {
var slice = b.toString('ascii', start, start + len);
if (parser.incoming.url) {
parser.incoming.url += slice;
} else {
// Almost always will branch here.
parser.incoming.url = slice;
}
var parser = new HTTPParser(HTTPParser.REQUEST);
parser._headers = [];
parser._url = '';
// 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
// processed in a single run. This method is also
// called to process trailing HTTP headers.
parser.onHeaders = function(headers, url) {
parser._headers = parser._headers.concat(headers);
parser._url += url;
};
parser.onHeaderField = function(b, start, len) {
var slice = b.toString('ascii', start, start + len).toLowerCase();
if (parser.value != undefined) {
parser.incoming._addHeaderLine(parser.field, parser.value);
parser.field = null;
parser.value = null;
}
if (parser.field) {
parser.field += slice;
} else {
parser.field = slice;
}
};
// info.headers and info.url are set only if .onHeaders()
// has not been called for this request.
//
// info.url is not set for response parsers but that's not
// applicable here since all our parsers are request parsers.
parser.onHeadersComplete = function(info) {
var headers = info.headers;
var url = info.url;
parser.onHeaderValue = function(b, start, len) {
var slice = b.toString('ascii', start, start + len);
if (parser.value) {
parser.value += slice;
} else {
parser.value = slice;
if (!headers) {
headers = parser._headers;
parser._headers = [];
}
};
parser.onHeadersComplete = function(info) {
if (parser.field && (parser.value != undefined)) {
parser.incoming._addHeaderLine(parser.field, parser.value);
parser.field = null;
parser.value = null;
if (!url) {
url = parser._url;
parser._url = '';
}
parser.incoming = new IncomingMessage(parser.socket);
parser.incoming.httpVersionMajor = info.versionMajor;
parser.incoming.httpVersionMinor = 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) {
// server only
@ -96,6 +89,7 @@ var parsers = new FreeList('parsers', 1000, function() {
} else {
// client only
parser.incoming.statusCode = info.statusCode;
// CHECKME dead code? we're always a request parser
}
parser.incoming.upgrade = info.upgrade;
@ -123,10 +117,20 @@ var parsers = new FreeList('parsers', 1000, function() {
};
parser.onMessageComplete = function() {
this.incoming.complete = true;
if (parser.field && (parser.value != undefined)) {
parser.incoming._addHeaderLine(parser.field, parser.value);
parser.incoming.complete = true;
// 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) {
// For upgraded connections, also emit this after parser.execute
parser.incoming.readable = false;
@ -1088,7 +1092,7 @@ ClientRequest.prototype.onSocket = function(socket) {
var parser = parsers.alloc();
req.socket = socket;
req.connection = socket;
parser.reinitialize('response');
parser.reinitialize(HTTPParser.RESPONSE);
parser.socket = socket;
parser.incoming = null;
req.parser = parser;
@ -1346,7 +1350,7 @@ function connectionListener(socket) {
});
var parser = parsers.alloc();
parser.reinitialize('request');
parser.reinitialize(HTTPParser.REQUEST);
parser.socket = socket;
parser.incoming = null;

381
src/node_http_parser.cc

@ -51,10 +51,7 @@ namespace node {
using namespace v8;
static Persistent<String> on_message_begin_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_sym;
static Persistent<String> on_headers_complete_sym;
static Persistent<String> on_body_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> should_keep_alive_sym;
static Persistent<String> upgrade_sym;
static Persistent<String> headers_sym;
static Persistent<String> url_sym;
static struct http_parser_settings settings;
@ -104,43 +103,29 @@ static char* current_buffer_data;
static size_t current_buffer_len;
// Callback prototype for http_cb
#define DEFINE_HTTP_CB(name) \
static int name(http_parser *p) { \
Parser *parser = static_cast<Parser*>(p->data); \
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> ret = cb->Call(parser->handle_, 0, NULL); \
if (ret.IsEmpty()) { \
parser->got_exception_ = true; \
return -1; \
} else { \
return 0; \
} \
}
#if defined(__GNUC__)
#define always_inline __attribute__((always_inline))
#elif defined(_MSC_VER)
#define always_inline __forceinline
#else
#define always_inline
#endif
// Callback prototype for http_data_cb
#define DEFINE_HTTP_DATA_CB(name) \
static int name(http_parser *p, const char *at, size_t length) { \
Parser *parser = static_cast<Parser*>(p->data); \
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; \
} \
}
#define HTTP_CB(name) \
static int name(http_parser* p_) { \
Parser* self = container_of(p_, Parser, parser_); \
return self->name##_(); \
} \
int always_inline name##_()
#define HTTP_DATA_CB(name) \
static int name(http_parser* p_, const char* at, size_t length) { \
Parser* self = container_of(p_, Parser, parser_); \
return self->name##_(at, length); \
} \
int always_inline name##_(const char* at, size_t length)
static inline Persistent<String>
@ -175,90 +160,246 @@ 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 {
public:
public:
Parser(enum http_parser_type type) : ObjectWrap() {
Init(type);
}
~Parser() {
}
DEFINE_HTTP_CB(on_message_begin)
DEFINE_HTTP_CB(on_message_complete)
DEFINE_HTTP_DATA_CB(on_url)
DEFINE_HTTP_DATA_CB(on_header_field)
DEFINE_HTTP_DATA_CB(on_header_value)
DEFINE_HTTP_DATA_CB(on_body)
HTTP_CB(on_message_begin) {
num_fields_ = num_values_ = -1;
url_.Reset();
return 0;
}
HTTP_DATA_CB(on_url) {
url_.Update(at, length);
return 0;
}
static int on_headers_complete(http_parser *p) {
Parser *parser = static_cast<Parser*>(p->data);
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();
}
Local<Value> cb_value = parser->handle_->Get(on_headers_complete_sym);
if (!cb_value->IsFunction()) return 0;
Local<Function> cb = Local<Function>::Cast(cb_value);
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;
}
HTTP_CB(on_headers_complete) {
Local<Value> cb = handle_->Get(on_headers_complete_sym);
if (!cb->IsFunction())
return 0;
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
if (p->type == HTTP_REQUEST) {
message_info->Set(method_sym, method_to_str(p->method));
if (parser_.type == HTTP_REQUEST) {
message_info->Set(method_sym, method_to_str(parser_.method));
}
// STATUS
if (p->type == HTTP_RESPONSE) {
message_info->Set(status_code_sym, Integer::New(p->status_code));
if (parser_.type == HTTP_RESPONSE) {
message_info->Set(status_code_sym, Integer::New(parser_.status_code));
}
// VERSION
message_info->Set(version_major_sym, Integer::New(p->http_major));
message_info->Set(version_minor_sym, Integer::New(p->http_minor));
message_info->Set(version_major_sym, Integer::New(parser_.http_major));
message_info->Set(version_minor_sym, Integer::New(parser_.http_minor));
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> head_response = cb->Call(parser->handle_, 1, argv);
Local<Value> head_response =
Local<Function>::Cast(cb)->Call(handle_, 1, argv);
if (head_response.IsEmpty()) {
parser->got_exception_ = true;
got_exception_ = true;
return -1;
}
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;
} else {
return head_response->IsTrue() ? 1 : 0;
}
return 0;
}
static Handle<Value> New(const Arguments& args) {
HTTP_CB(on_message_complete) {
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")) {
parser = new Parser(HTTP_REQUEST);
} else if (0 == strcasecmp(*type, "response")) {
parser = new Parser(HTTP_RESPONSE);
} else {
return ThrowException(Exception::Error(
String::New("Constructor argument be 'request' or 'response'")));
if (!cb->IsFunction())
return 0;
Local<Value> r = Local<Function>::Cast(cb)->Call(handle_, 0, NULL);
if (r.IsEmpty()) {
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());
return args.This();
}
// var bytesParsed = parser->execute(buffer, off, len);
static Handle<Value> Execute(const Arguments& args) {
HandleScope scope;
Parser *parser = ObjectWrap::Unwrap<Parser>(args.This());
Parser* parser = ObjectWrap::Unwrap<Parser>(args.This());
assert(!current_buffer);
assert(!current_buffer_data);
@ -321,10 +462,11 @@ class Parser : public ObjectWrap {
}
}
static Handle<Value> Finish(const Arguments& args) {
HandleScope scope;
Parser *parser = ObjectWrap::Unwrap<Parser>(args.This());
Parser* parser = ObjectWrap::Unwrap<Parser>(args.This());
assert(!current_buffer);
parser->got_exception_ = false;
@ -343,33 +485,83 @@ class Parser : public ObjectWrap {
return Undefined();
}
static Handle<Value> Reinitialize(const Arguments& args) {
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")) {
parser->Init(HTTP_REQUEST);
} else if (0 == strcasecmp(*type, "response")) {
parser->Init(HTTP_RESPONSE);
} else {
return ThrowException(Exception::Error(
String::New("Argument be 'request' or 'response'")));
if (type != HTTP_REQUEST && type != HTTP_RESPONSE) {
return ThrowException(Exception::Error(String::New(
"Argument must be HTTPParser.REQUEST or HTTPParser.RESPONSE")));
}
Parser* parser = ObjectWrap::Unwrap<Parser>(args.This());
parser->Init(type);
return Undefined();
}
private:
private:
void Init (enum http_parser_type type) {
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) {
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_;
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->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, "finish", Parser::Finish);
NODE_SET_PROTOTYPE_METHOD(t, "reinitialize", Parser::Reinitialize);
target->Set(String::NewSymbol("HTTPParser"), t->GetFunction());
on_message_begin_sym = NODE_PSYMBOL("onMessageBegin");
on_url_sym = NODE_PSYMBOL("onURL");
on_header_field_sym = NODE_PSYMBOL("onHeaderField");
on_header_value_sym = NODE_PSYMBOL("onHeaderValue");
on_headers_sym = NODE_PSYMBOL("onHeaders");
on_headers_complete_sym = NODE_PSYMBOL("onHeadersComplete");
on_body_sym = NODE_PSYMBOL("onBody");
on_message_complete_sym = NODE_PSYMBOL("onMessageComplete");
@ -427,6 +620,8 @@ void InitHttpParser(Handle<Object> target) {
version_minor_sym = NODE_PSYMBOL("versionMinor");
should_keep_alive_sym = NODE_PSYMBOL("shouldKeepAlive");
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_url = Parser::on_url;

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

@ -22,55 +22,513 @@
var common = require('../common');
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
// binding. Tests for pathological http messages should be submitted
// upstream to http://github.com/ry/http-parser for inclusion into
// deps/http-parser/test.c
var HTTPParser = process.binding('http_parser').HTTPParser;
var parser = new HTTPParser('request');
function newParser(type) {
var parser = new HTTPParser(type);
parser.headers = [];
parser.url = '';
parser.onHeaders = function(headers, url) {
parser.headers = parser.headers.concat(headers);
parser.url += url;
};
parser.onHeadersComplete = function(info) {
};
parser.onBody = function(b, start, len) {
assert.ok(false, 'Function should not be called.');
};
parser.onMessageComplete = function() {
};
return parser;
}
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);
});
}
//
// 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
// thrown from parser.execute()
//
parser.onHeadersComplete = function(info) {
throw new Error('hello world');
};
parser.reinitialize(HTTPParser.REQUEST);
assert.throws(function() {
parser.execute(request, 0, request.length);
}, Error, 'hello world');
})();
var Buffer = require('buffer').Buffer;
var buffer = new Buffer(1024);
var request = 'GET /hello HTTP/1.1\r\n\r\n';
//
// 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);
})();
buffer.write(request, 0, 'ascii');
//
// 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 callbacks = 0;
var parser = newParser(REQUEST);
parser.onMessageBegin = function() {
console.log('message begin');
callbacks++;
};
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.onHeadersComplete = function(info) {
console.log('headers complete: ' + JSON.stringify(info));
assert.equal('GET', info.method);
assert.equal(1, info.versionMajor);
assert.equal(1, info.versionMinor);
callbacks++;
};
parser.onBody = mustCall(function(buf, start, len) {
var body = '' + buf.slice(start, start + len);
assert.equal(body, 'ping');
seen_body = true;
});
parser.onURL = function(b, off, len) {
//throw new Error('hello world');
callbacks++;
};
parser.execute(request, 0, request.length);
})();
parser.execute(buffer, 0, request.length);
assert.equal(3, callbacks);
//
// Check that if we throw an error in the callbacks that error will be
// thrown from parser.execute()
// 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']);
};
parser.onURL = function(b, off, len) {
throw new Error('hello world');
};
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']);
};
assert.throws(function() {
parser.execute(buffer, 0, request.length);
}, Error, 'hello world');
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