diff --git a/src/dapi/http/XhrHttpImpl.js b/src/dapi/http/XhrHttpImpl.js
new file mode 100644
index 0000000..ac9c6c7
--- /dev/null
+++ b/src/dapi/http/XhrHttpImpl.js
@@ -0,0 +1,145 @@
+/**
+ * XMLHttpRequest HTTP protocol implementation
+ *
+ * Copyright (C) 2014 LoVullo Associates, Inc.
+ *
+ * This file is part of the Liza Data Collection Framework
+ *
+ * Liza is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+var Class = require( 'easejs' ).Class,
+ HttpImpl = require( './HttpImpl' );
+
+
+/**
+ * An HTTP implementation using the standardized XMLHttpRequest prototype.
+ */
+module.exports = Class( 'XhrHttpImpl' )
+ .implement( HttpImpl )
+ .extend(
+{
+ /**
+ * XMLHttpRequest constructor
+ * @type {XMLHttpRequest}
+ * @constructor
+ */
+ 'private _Xhr': null,
+
+
+ /**
+ * Initializes with constructor to the object through which XHRs will be
+ * made
+ *
+ * @param {Object} XMLHttpRequest ctor to object to perform requests
+ */
+ __construct: function( XMLHttpRequest )
+ {
+ this._Xhr = XMLHttpRequest;
+ },
+
+
+ /**
+ * Perform HTTP request using the standard XMLHttpRequest
+ *
+ * @param {Object|string} data request params
+ * @param {function(Error, Object)} callback server response callback
+ *
+ * @return {HttpImpl} self
+ */
+ 'public requestData': function( url, method, data, callback )
+ {
+ var req = new this._Xhr();
+
+ try
+ {
+ this.openRequest( req, url, method );
+ this.onLoad( req, function( err, resp )
+ {
+ callback( err, resp );
+ } );
+ }
+ catch ( e )
+ {
+ callback( e, null );
+ }
+
+ return this;
+ },
+
+
+ /**
+ * Prepares a request to the given URL using the given HTTP method
+ *
+ * This method may be overridden by subtypes to set authentication data,
+ * modify headers, hook XHR callbacks, etc.
+ *
+ * Subtypes may throw exceptions; the caller of this method catches and
+ * properly forwards them to the callback.
+ *
+ * This method must be synchronous.
+ *
+ * @param {XMLHttpRequest} req request to prepare
+ * @param {string} url destination URL
+ * @param {string} method HTTP method
+ *
+ * @return {undefined}
+ */
+ 'virtual protected openRequest': function( req, url, method )
+ {
+ // alway async
+ req.open( method, url, true );
+ },
+
+
+ /**
+ * Hooks ready state change to handle data
+ *
+ * Subtypes may override this method to alter the ready state change
+ * actions taken (e.g. to display progress, handle errors, etc.)
+ *
+ * @param {XMLHttpRequest} req request to hook
+ * @param {function(string)} callback continuation to invoke with response
+ *
+ * @return {undefined}
+ *
+ * @throws {Error} if non-200 response received from server
+ */
+ 'virtual protected onLoad': function( req, callback )
+ {
+ req.onreadystatechange = function()
+ {
+ // ready state of 4 (DONE) indicates that the request is complete
+ if ( req.readyState !== 4 )
+ {
+ return;
+ }
+ else if ( req.status !== 200 )
+ {
+ callback(
+ Error( req.status + " error from server" ),
+ {
+ status: req.status,
+ data: req.responseText
+ }
+ );
+ return;
+ }
+
+ // successful
+ callback( null, req.responseText );
+ };
+ }
+} );
+
diff --git a/test/dapi/http/HttpDataApiTest.js b/test/dapi/http/HttpDataApiTest.js
index eeeb7d6..748912e 100644
--- a/test/dapi/http/HttpDataApiTest.js
+++ b/test/dapi/http/HttpDataApiTest.js
@@ -27,7 +27,7 @@ var dapi = require( '../../../' ).dapi,
dummy_url = 'http://foo',
dummy_impl = Class
.implement( dapi.http.HttpImpl )
- .extend( { requestData: function( _, __, ___, ____ ) {} } ),
+ .extend( { requestData: function( _, __, ___, ____ ) {} } )(),
dummy_sut = Sut( dummy_url, 'GET', dummy_impl );
diff --git a/test/dapi/http/XhrHttpImplTest.js b/test/dapi/http/XhrHttpImplTest.js
new file mode 100644
index 0000000..7e3efc6
--- /dev/null
+++ b/test/dapi/http/XhrHttpImplTest.js
@@ -0,0 +1,219 @@
+/**
+ * Test case for XMLHttpRequest HTTP protocol implementation
+ *
+ * Copyright (C) 2014 LoVullo Associates, Inc.
+ *
+ * This file is part of the Liza Data Collection Framework
+ *
+ * Liza is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+var dapi = require( '../../../' ).dapi,
+ expect = require( 'chai' ).expect,
+ Class = require( 'easejs' ).Class,
+ HttpImpl = dapi.http.HttpImpl,
+ Sut = dapi.http.XhrHttpImpl,
+
+ DummyXhr = function()
+ {
+ this.open = function()
+ {
+ DummyXhr.args = arguments;
+ };
+ };
+
+
+describe( 'XhrHttpImpl', function()
+{
+ /**
+ * Since ECMAScript does not have return typing, we won't know if the ctor
+ * actually returns an XMLHttpRequest until we try.
+ */
+ it( 'will accept any constructor', function()
+ {
+ expect( function()
+ {
+ Sut( function() {} );
+ } ).to.not.throw( Error );
+ } );
+
+
+ it( 'is an HttpImpl', function()
+ {
+ var sut = Sut( function() {} );
+ expect( Class.isA( HttpImpl, sut ) ).to.be.ok;
+ } );
+
+
+ describe( '.requestData', function()
+ {
+ it( 'requests a connection using the given url and method', function()
+ {
+ var url = 'http://foonugget',
+ method = 'GET',
+ sut = Sut( DummyXhr );
+
+ sut.requestData( url, method, {}, function() {} );
+
+ var args = DummyXhr.args;
+ expect( args[ 0 ] ).to.equal( method );
+ expect( args[ 1 ] ).to.equal( url );
+ expect( args[ 1 ] ).to.be.ok; // async
+ } );
+
+
+ /**
+ * Since the request is asynchronous, we should be polite and not return
+ * errors in two different formats; we will catch it and instead pass it
+ * back via the callback.
+ */
+ it( 'returns XHR open() errors via callback', function( done )
+ {
+ var e = Error( "Test error" ),
+ Xhr = function()
+ {
+ this.open = function()
+ {
+ throw e;
+ };
+ };
+
+ // should not throw an exception
+ expect( function()
+ {
+ // should instead provide to callback
+ Sut( Xhr )
+ .requestData( 'http://foo', 'GET', {}, function( err, data )
+ {
+ expect( err ).to.equal( e );
+ expect( data ).to.equal( null );
+ done();
+ } );
+ } ).to.not.throw( Error );
+ } );
+
+
+ it( 'returns XHR response via callback when no error', function( done )
+ {
+ var retdata = "foobar",
+ src = "moocow",
+ StubXhr = createStubXhr();
+
+ StubXhr.prototype.responseText = retdata;
+ StubXhr.prototype.readyState = 4; // done
+ StubXhr.prototype.status = 200; // OK
+
+ StubXhr.prototype.send = function( data )
+ {
+ expect( data ).is.equal( src );
+ StubXhr.inst.onreadystatechange();
+ };
+
+ Sut( StubXhr )
+ .requestData( 'http://bar', 'GET', src, function( err, resp )
+ {
+ expect( err ).to.equal( null );
+ expect( resp ).to.equal( retdata );
+ done();
+ } );
+
+ // invoke callback
+ StubXhr.inst.send( src );
+ } );
+
+
+ describe( 'if return status code is not 200', function()
+ {
+ /**
+ * This is the default behavior, but can be changed by overriding
+ * the onLoad method.
+ */
+ it( 'returns an error to the callback', function( done )
+ {
+ var StubXhr = createStubXhr();
+ StubXhr.prototype.status = 404;
+
+ Sut( StubXhr )
+ .requestData( 'http://foo', 'GET', '', function( err, _ )
+ {
+ expect( err ).to.be.instanceOf( Error );
+ expect( err.message ).to.contain(
+ StubXhr.prototype.status
+ );
+
+ done();
+ } );
+
+ StubXhr.inst.send( '' );
+ } );
+
+
+ it( 'returns response text with error code', function( done )
+ {
+ var StubXhr = createStubXhr(),
+ status = 404,
+ reply = 'foobunny';
+
+ StubXhr.prototype.status = status;
+ StubXhr.prototype.responseText = reply;
+
+ Sut( StubXhr )
+ .requestData( 'http://foo', 'GET', '', function( _, resp )
+ {
+ expect( resp.status ).to.equal( status );
+ expect( resp.data ).to.equal( reply );
+ done();
+ } );
+
+ StubXhr.inst.send( '' );
+ } );
+ } );
+
+
+ it( 'returns self', function()
+ {
+ var sut = Sut( function() {} ),
+ ret = sut.requestData(
+ 'http://foo', 'GET', {}, function() {}
+ );
+
+ expect( ret ).to.equal( sut );
+ } );
+ } );
+} );
+
+
+function createStubXhr()
+{
+ var StubXhr = function()
+ {
+ StubXhr.inst = this;
+ };
+
+ StubXhr.prototype = {
+ onreadystatechange: null,
+ responseText: '',
+ readyState: 4, // don,
+ status: 200, // O,
+
+ open: function() {},
+ send: function( data )
+ {
+ this.onreadystatechange();
+ }
+ };
+
+ return StubXhr;
+}
+