HTTP is one of the core technologies behind the Web. In addition to the actual content, some important information is passed with HTTP headers for both HTTP requests and responses.
You can add your own HTTP headers to any request the application makes, whether the request is initiated by your code explicitly opening an HTTP channel, because of XMLHttpRequest activity, an <img>
element in content, or even from CSS.
HTTP Channels
When you deal with HTTP requests and responses, typically you are doing this with an nsIHttpChannel
. The nsIHttpChannel
interface has a number of properties and methods, but the method that is of interest to us is setRequestHeader
. This method allows us to set an HTTP request header.
Below is some sample code where we set an HTTP header.
// adds "X-Hello: World" header to the request httpChannel.setRequestHeader("X-Hello", "World", false);
In the example code above we have a variable named httpChannel
which points to an object implementing nsIHttpChannel
. (This variable could have been named anything though.)
The setRequestHeader
method takes 3 parameters. The first parameter is the name of the HTTP request header. The second parameter is the value of the HTTP request header. And for now, just ignore the third parameter, and just always make it false
.
In our example code, the HTTP request header that we added is named X-Hello
and the value of this HTTP request header is World
.
NOTE: If you are making up your own HTTP header, you MUST put a X-
in front of the name. (In our example, our made up HTTP header is X-Hello
and NOT Hello
because we correctly added the X-
in front of our name.)
No longer the case: https://tools.ietf.org/html/rfc6648
Notifications
The question that may be coming to your mind right now is, how do you get the nsIHttpChannel
when an HTTP request is made.
In the case your code initiates the request, you probably already have one. Trapping other requests is done with notifications, which are a lot like events or signals found in other languages and frameworks.
In particular, to get the nsIHttpChannel
just before the HTTP request is made we need to observe the "http-on-modify-request"
topic. (And yes, "http-on-modify-request"
is a string.)
NOTE: There are many topics, besides just "http-on-modify-request"
, that you can get notifications about, for example "http-on-examine-response"
and "xpcom-shutdown"
. You can also make up your own topics and send out your own notifications.
For more information about notifications framework and a list of common notification topics, see Observer Notifications.
Observers
To get notified about some topic (like "http-on-modify-request"
) we need to create an observer. An observer is a component implementing nsIObserver interface. And once the observer is registered for a topic, it will get notified about the topic by having its observe
method called.
Below is an example observer that adds a custom header "X-Hello" to the channel passed in for http-on-modify-request notification:
var {Cc, Ci} = require("chrome"); var httpRequestObserver = { observe: function(subject, topic, data) { if (topic == "http-on-modify-request") { var httpChannel = subject.QueryInterface(Ci.nsIHttpChannel); httpChannel.setRequestHeader("X-Hello", "World", false); } } };
Please also note that the code itself usually is wrapped inside a exports.main = function() { ... } block.
Note that the number of parameter that the observe
method takes is important. It takes 3 parameters (as we've shown in the example code above). For the "http-on-modify-request"
topic, the first parameter (named subject
in the code above) will be the nsIHttpChannel
. However, it is passed to us as an nsISupports
. So we need to change the nsISupports
into a nsIHttpChannel
which is what the QueryInterface
call does. And yes, converting objects from one kind to another is very ugly, and lacks (what is usually called) syntactic sugar.
The second line of code in the if
block should already be familiar to you. It is the same code we used before, earlier in this article, to add the HTTP request header.
The name of this object -- httpRequestObserver
-- isn't important. We could have named it anything we liked.
Registering
After we've created the observer, we need to register it. In our case, we want to register it for the "http-on-modify-request"
topic. We can do this with the code below.
var observerService = Cc["@mozilla.org/observer-service;1"] .getService(Ci.nsIObserverService); observerService.addObserver(httpRequestObserver, "http-on-modify-request", false);
The first statement gets the object that will let us register with topics that we want to get notified about.
The second statement does the actual registering. We are saying we want httpRequestObserver
to be notified (by calling its observe
method) when a "http-on-modify-request"
topic takes place (which we know happens just before each HTTP request).
Unregistering
You should unregister the observer on shutdown. Failing to do that may cause memory leaks. To unregister the observer use nsIObserverService.removeObserver
as follows:
observerService.removeObserver(httpRequestObserver, "http-on-modify-request");
All-in-one example
Here is a slightly different version of our httpRequestObserver
object. While the previous version we showed before was good for learning, in an actual real-world application, you'd probably want to code it more like the following.
var httpRequestObserver = { observe: function(subject, topic, data) { if (topic == "http-on-modify-request") { var httpChannel = subject.QueryInterface(Ci.nsIHttpChannel); httpChannel.setRequestHeader("X-Hello", "World", false); } }, get observerService() { return Cc["@mozilla.org/observer-service;1"] .getService(Ci.nsIObserverService); }, register: function() { this.observerService.addObserver(this, "http-on-modify-request", false); }, unregister: function() { this.observerService.removeObserver(this, "http-on-modify-request"); } };
This object has a convenience register()
and unregister()
methods, so in order to activate it you just need to call:
httpRequestObserver.register();
You should also remember to unregister the observer at shutdown:
httpRequestObserver.unregister();
And that's it.
XPCOM components
You need to register a single http-on-modify-request
observer per application (and not one per window). This means that you should put the observer's implementation in an XPCOM component instead of an overlay. If you want to support Gecko2 (Firefox4) you need to register your javascript component as described here: https://developer.mozilla.org/en/XPCOM/XPCOM_changes_in_Gecko_2.0#JavaScript_components.
var headerName = "X-hello"; var headerValue = "world"; function LOG(text) { // var consoleService = Components.classes["@mozilla.org/consoleservice;1"].getService(Components.interfaces.nsIConsoleService); // consoleService.logStringMessage(text); } function myHTTPListener() { } myHTTPListener.prototype = { observe: function(subject, topic, data) { if (topic == "http-on-modify-request") { LOG("----------------------------> (" + subject + ") mod request"); var httpChannel = subject.QueryInterface(Components.interfaces.nsIHttpChannel); httpChannel.setRequestHeader(headerName, headerValue, false); return; } if (topic == "profile-after-change") { LOG("----------------------------> profile-after-change"); var os = Components.classes["@mozilla.org/observer-service;1"] .getService(Components.interfaces.nsIObserverService); os.addObserver(this, "http-on-modify-request", false); return; } }, QueryInterface: function (iid) { if (iid.equals(Components.interfaces.nsIObserver) || iid.equals(Components.interfaces.nsISupports)) return this; Components.returnCode = Components.results.NS_ERROR_NO_INTERFACE; return null; }, }; var myModule = { registerSelf: function (compMgr, fileSpec, location, type) { var compMgr = compMgr.QueryInterface(Components.interfaces.nsIComponentRegistrar); compMgr.registerFactoryLocation(this.myCID, this.myName, this.myProgID, fileSpec, location, type); LOG("----------------------------> registerSelf"); var catMgr = Components.classes["@mozilla.org/categorymanager;1"].getService(Components.interfaces.nsICategoryManager); catMgr.addCategoryEntry("app-startup", this.myName, this.myProgID, true, true); }, getClassObject: function (compMgr, cid, iid) { LOG("----------------------------> getClassObject"); return this.myFactory; }, myCID: Components.ID("{9cf5f3df-2505-42dd-9094-c1631bd1be1c}"), myProgID: "@dougt/myHTTPListener;1", myName: "Simple HTTP Listener", myFactory: { QueryInterface: function (aIID) { if (!aIID.equals(Components.interfaces.nsISupports) && !aIID.equals(Components.interfaces.nsIFactory)) throw Components.results.NS_ERROR_NO_INTERFACE; return this; }, createInstance: function (outer, iid) { LOG("----------------------------> createInstance"); return new myHTTPListener(); } }, canUnload: function(compMgr) { return true; } }; function NSGetModule(compMgr, fileSpec) { return myModule; }
Privacy and security good practice
A use case for setting specific a HTTP request header is to have a specific web site be able to check if a specific plugin / addon / extension is installed.
The good practice is not to have this specific HTTP header (for example
"X-site.net-extension") sent all the time but only when doing requests with this specific web sites. By not advertising to all sites what extensions are installed this improves both privacy (this makes it harder to track a user known by his set of plugins, addons and extensions) and security (some plugins, addons and extensions may be known to have flaws by attackers).
With this privacy and security addition the code to use becomes:
observe: function(subject, topic, data) { if (topic == "http-on-modify-request") { var httpChannel = subject.QueryInterface(Ci.nsIHttpChannel); if (/site.net/.test(httpChannel.originalURI.host)) { httpChannel.setRequestHeader("X-Hello", "World", false); } } },