Monitor all http(s) network requests using the Mozilla Platform

In an xpcshell test, I recently needed a way to monitor all network requests and access both request and response data so I can save them for later use. This required a little bit of digging in Mozilla’s devtools code so I thought I’d write a short blog post about it.

This code will be used in a testcase that ensures that calendar providers in Lightning function properly. In the case of the CalDAV provider, we would need to access a real server for testing. We can’t just set up a few servers and use them for testing, it would end in an unreasonable amount of server maintenance. Given non-local connections are not allowed when running the tests on the Mozilla build infrastructure, it wouldn’t work anyway. The solution is to create a fakeserver, that is able to replay the requests in the same way. Instead of manually making the requests and figuring out how the server replies, we can use this code to quickly collect all the requests we need.

Without further delay, here is the code you have been waiting for:


/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
var allRequests = [];
/**
* Add the following function as a request observer:
* Services.obs.addObserver(httpObserver, "http-on-examine-response", false);
*
* When done listening on requests:
* dump(allRequests.join("\n===\n")); // print them
* dump(JSON.stringify(allRequests, null, " ")) // jsonify them
*/
function httpObserver(aSubject, aTopic, aData) {
if (aSubject instanceof Components.interfaces.nsITraceableChannel) {
let request = new TracedRequest(aSubject);
request._next = aSubject.setNewListener(request);
allRequests.push(request);
}
}
/**
* This is the object that represents a request/response and also collects the data for it
*
* @param aSubject The channel from the response observer.
*/
function TracedRequest(aSubject) {
let httpchannel = aSubject.QueryInterface(Components.interfaces.nsIHttpChannel);
let self = this;
this.requestHeaders = Object.create(null);
httpchannel.visitRequestHeaders({
visitHeader: function(k, v) {
self.requestHeaders[k] = v;
}
});
this.responseHeaders = Object.create(null);
httpchannel.visitResponseHeaders({
visitHeader: function(k, v) {
self.responseHeaders[k] = v;
}
});
this.uri = aSubject.URI.spec;
this.method = httpchannel.requestMethod;
this.requestBody = readRequestBody(aSubject);
this.responseStatus = httpchannel.responseStatus;
this.responseStatusText = httpchannel.responseStatusText;
this._chunks = [];
}
TracedRequest.prototype = {
uri: null,
method: null,
requestBody: null,
requestHeaders: null,
responseStatus: null,
responseStatusText: null,
responseHeaders: null,
responseBody: null,
toJSON: function() {
let j = Object.create(null);
for (let m of Object.keys(this)) {
if (typeof this[m] != "function" && m[0] != "_") {
j[m] = this[m];
}
}
return j;
},
onStartRequest: function(aRequest, aContext) this._next.onStartRequest(aRequest, aContext),
onStopRequest: function(aRequest, aContext, aStatusCode) {
this.responseBody = this._chunks.join("");
this._chunks = null;
this._next.onStopRequest(aRequest, aContext, aStatusCode);
this._next = null;
},
onDataAvailable: function(aRequest, aContext, aStream, aOffset, aCount) {
let binaryInputStream = Components.classes["@mozilla.org/binaryinputstream;1"]
.createInstance(Components.interfaces.nsIBinaryInputStream);
let storageStream = Components.classes["@mozilla.org/storagestream;1"]
.createInstance(Components.interfaces.nsIStorageStream);
let outStream = Components.classes["@mozilla.org/binaryoutputstream;1"]
.createInstance(Components.interfaces.nsIBinaryOutputStream);
binaryInputStream.setInputStream(aStream);
storageStream.init(8192, aCount, null);
outStream.setOutputStream(storageStream.getOutputStream(0));
let data = binaryInputStream.readBytes(aCount);
this._chunks.push(data);
outStream.writeBytes(data, aCount);
this._next.onDataAvailable(aRequest, aContext,
storageStream.newInputStream(0),
aOffset, aCount);
},
toString: function() {
let str = this.method + " " + this.uri;
for (let hdr of Object.keys(this.requestHeaders)) {
str += hdr + ": " + this.requestHeaders[hdr] + "\n";
}
if (this.requestBody) {
str += "\r\n" + this.requestBody + "\n";
}
str += "\n" + this.responseStatus + " " + this.responseStatusText
if (this.responseBody) {
str += "\r\n" + this.responseBody + "\n";
}
return str;
}
};
// Taken from:
// http://hg.mozilla.org/mozilla-central/file/2399d1ae89e9/toolkit/devtools/webconsole/network-helper.js#l120
function readRequestBody(aRequest, aCharset="UTF-8") {
let text = null;
if (aRequest instanceof Ci.nsIUploadChannel) {
let iStream = aRequest.uploadStream;
let isSeekableStream = false;
if (iStream instanceof Ci.nsISeekableStream) {
isSeekableStream = true;
}
let prevOffset;
if (isSeekableStream) {
prevOffset = iStream.tell();
iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
}
// Read data from the stream.
try {
let rawtext = NetUtil.readInputStreamToString(iStream, iStream.available())
let conv = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
conv.charset = aCharset;
text = conv.ConvertToUnicode(rawtext);
} catch (err) {
}
// Seek locks the file, so seek to the beginning only if necko hasn't
// read it yet, since necko doesn't eek to 0 before reading (at lest
// not till 459384 is fixed).
if (isSeekableStream && prevOffset == 0) {
iStream.seek(Components.interfaces.nsISeekableStream.NS_SEEK_SET, 0);
}
}
return text;
}

Leave a comment