用于单元测试的 HTTP 服务器¶
此页面描述了位于 netwerk/test/httpserver/
中的 HTTP 服务器的 JavaScript 实现。
服务器功能¶
以下是您可以使用服务器执行的一些操作
将文件目录映射到服务器上的 HTTP 路径,可以映射任意数量的此类目录(包括嵌套目录)
为 HTTP 错误代码定义自定义错误处理程序
为特定路径的请求提供指定的文件,可以选择添加自定义标头和状态
使用基于 JavaScript 的 API 为特定路径定义自定义“CGI”处理程序以创建响应(标头和实际内容)
在不同端口(8080、8081、8082 等)上同时运行多个服务器。
此功能应该足以满足任何需要 HTTP 行为的测试。
使用场景¶
该服务器主要用于 xpcshell
基于测试,可以用作内联脚本或 XPCOM 组件。Mochitest 框架也使用它来提供其测试,并且 reftests 在其行为依赖于特定 HTTP 标头值时可以选择使用它。
使用方法¶
应用程序更新测试
跨“服务器”安全测试
跨域安全测试,结合正确的代理设置(例如,使用 Proxy AutoConfig)
行为依赖于 HTTP 标头值(例如,Content-Type)的测试
任何需要使用非本地存储的文件的场景
OpenID:用户可以提供他们自己的 OpenID 服务器(只有在使用浏览器时才需要)
微博客:用户可以基于 RSS/Atom 等标准托管自己的微博客
REST API:Web 应用程序可以与 REST 或 SOAP API 交互以实现多种目的,例如:文件/数据存储、社交分享等
下载测试
使用服务器¶
您应该首先查看的文档是 netwerk/test/httpserver/nsIHttpServer.idl
。它非常全面和详细,应该足以弄清楚如何使服务器执行您想要的操作。我还建议您查看不太全面的服务器 README,尽管 IDL 通常应该足够。
运行服务器¶
在测试套件中,服务器应作为仅限测试的 JS 模块导入
ChromeUtils.import("resource://testing-common/httpd.js");
完成此操作后,您可以如下创建新的服务器
let server = new HttpServer(); // Or nsHttpServer() if you don't use ChromeUtils.import.
server.registerDirectory("/", nsILocalFileForBasePath);
server.start(-1); // uses a random available port, allows us to run tests concurrently
const SERVER_PORT = server.identity.primaryPort; // you can use this further on
// and when the tests are done, most likely from a callback...
server.stop(function() { /* continue execution here */ });
您还可以将数字端口参数传递给 start()
方法,但我们强烈建议您不要这样做。使用动态端口允许我们并行运行您的测试和其他测试,从而减少等待时间并使每个人都满意。如果您确实必须使用硬编码端口,则必须在 xpcshell 清单文件中使用 run-sequentially = REASON
注释您的测试。但是,这应该仅作为最后的选择。
注意
您**必须**确保在测试完成前停止服务器(上面的最后一行)。否则将导致“XPConnect is being called on a scope without a Components property”断言,这会导致您的测试在调试版本中失败,并且您会让人们在运行测试时感到恼火,因为您破坏了测试。
调试错误¶
服务器的默认错误页面没有提供太多信息,部分原因是错误分派机制目前无法处理此问题,部分原因是公开真实服务器中的错误可能会使其更容易被利用。如果您不知道服务器为何以特定方式运行,请编辑 httpd.js 并将 DEBUG
的值更改为 true
。这将导致服务器将有关请求处理(以及执行此操作时遇到的错误)的信息打印到控制台,通常不难从该输出中确定问题所在。DEBUG
默认情况下为 false
,因为设置其值为 true
时打印的信息会不必要地模糊 tinderbox 输出。
文件标头修改¶
服务器支持修改其提供的文件(而非请求处理程序)的标头。要修改文件的标头,请创建一个同级文件,其名称为第一个文件名后跟 ^headers^
。以下是如何创建此类文件的示例
HTTP 404 I want a cool HTTP description!
Content-Type: text/plain
状态行是可选的;所有其他行都以标准 HTTP 格式指定 HTTP 标头。任何换行符样式都被接受,并且文件可以选择以单个换行符结尾,以便与 Unix 文本工具(如 diff
和 hg
)配合使用。
SJS:服务器端脚本¶
对服务器端脚本的支持是通过 SJS 机制提供的。本质上,SJS 是一个具有特定扩展名的文件(由服务器创建者选择),其中包含一个名为 handleRequest
的函数,该函数用于确定服务器将生成的响应。该函数的行为与 nsIHttpRequestHandler
接口上的 handle
函数完全相同。首先,告诉服务器您正在使用什么扩展名
const SJS_EXTENSION = "cgi";
server.registerContentType(SJS_EXTENSION, "sjs");
现在只需创建一个扩展名为 cgi
的 SJS 并编写您想要的任何内容。例如
function handleRequest(request, response)
{
response.setStatusLine(request.httpVersion, 200, "OK");
response.write("Hello world! This request was dynamically " +
"generated at " + new Date().toUTCString());
}
可以在 Mozilla 源代码树 中的现有测试中找到更多示例。请求对象是 nsIHttpRequest
的实例,响应是 nsIHttpResponse
。有关更多详细信息,请参阅 IDL 文档 <https://searchfox.org/mozilla-central/source/netwerk/test/httpserver/nsIHttpServer.idl>。
跨请求存储信息¶
HTTP 本质上是一种无状态协议,httpd.js 服务器 API 在大多数情况下也类似地是无状态的。如果您通过 XPCOM 接口使用服务器,则可以简单地将您想要的状态存储在封闭的环境或全局变量中。但是,如果您通过 SJS 使用它,则每次处理发生时,您的请求都在一个几乎为空的环境中处理。为了支持有状态的 SJS 行为,以下函数已添加到 SJS 处理程序执行的全局范围中,提供了一种简单的键值状态存储机制
/*
* v : T means v is of type T
* function A() : T means A() has type T
*/
function getState(key : string) : string
function setState(key : string, value : string)
function getSharedState(key : string) : string
function setSharedState(key : string, value : string)
function getObjectState(key : string, callback : function(value : object) : void) // SJS API, XPCOM differs, see below
function setObjectState(key : string, value : object)
键是具有任意内容的字符串。相应的 value 也是一个字符串,用于非对象保存函数。对于对象保存函数,它是一个对象,或者为 null
。最初,所有键都与空字符串或 null
关联,具体取决于函数是否访问字符串或对象值存储。存储的值在请求之间以及服务器关闭和重启之间持续存在。状态方法在 SJS 中可用,并且为了方便起见,可以通过 nsIHttpServer
接口在 XPCOM 和 SJS 中使用 XPCOM。这些变体旨在支持不同的需求。
警告
**警告:**小心使用状态:您(用户)负责通过任何可用方法同步状态的所有使用。(这包括仅对每路径状态起作用的方法:如果您的请求处理程序异步生成响应,您仍然可能会遇到麻烦。此外,任何可以访问服务器 XPCOM 组件的代码都可以在请求之间修改它,即使您只在生成同步响应时使用或修改该状态。)JavaScript 的运行至完成行为将在简单情况下为您提供帮助,但在任何中等复杂的情况下,您都在玩火,如果您操作不当,就会被烧伤。
getState
和 setState
¶
getState
和 setState
旨在用于单个请求处理程序需要存储其第一次请求的信息以用于处理其第二次请求的情况——例如,如果您想实现一个实现计数器的请求处理程序
/**
* Generates a response whose body is "0", "1", "2", and so on. each time a
* request is made. (Note that browser caching might make it appear
* to not quite have that behavior; a Cache-Control header would fix
* that issue if desired.)
*/
function handleRequest(request, response)
{
var counter = +getState("counter"); // convert to number; +"" === 0
response.write("" + counter);
setState("counter", "" + ++counter);
}
这两个方法的有用特性是此状态不会泄漏到其所在的单个路径之外。例如,如果上述 SJS 位于 /counter
,则在其他路径上通过 getState("counter")
返回的值将与上面实现的计数器完全不同。这使得编写有状态的处理程序变得更加简单,而无需状态意外泄漏到不相关的处理程序之间。
注意
通过此方法保存的状态特定于 HTTP 路径,不包括查询字符串和哈希引用。/counter
、/counter?foo
和 /counter?bar#baz
对于这些方法而言共享相同的状态。(事实上,如果查询字符串更改时非共享状态发生更改,则非共享状态的用处会大大降低!)
注意
预定义的 __LOCATION__
状态包含 SJS 文件本身的本地路径。您可以将结果直接传递给 nsILocalFile.initWithPath()
。例如:thisSJSfile.initWithPath(getState('__LOCATION__'));
getObjectState
和 setObjectState
¶
getObjectState
和 setObjectState
支持上述方法未提供的剩余功能:存储非字符串值(对象值或 null
)。这两种方法与 getSharedState
和 setSharedState
相同,因为状态在路径之间可见;在一个处理程序中使用 setObjectState
将在另一个使用 getObjectState
和相同键的处理程序中公开该值。(这种选择是有意的,因为对象值已经公开了您必须小心使用的可变状态。)此功能对于协作请求处理程序特别有用,其中一个请求*挂起*另一个请求,然后必须由第三个请求在稍后时间*恢复*第二个请求。如果没有对象值存储,您需要诉诸于使用前面任何一个状态 API 对字符串值进行轮询;但是,有了它,您可以在特定事件发生时进行精确的回调。
SJS 中的 getObjectState
与通过 XPCOM 访问的 getObjectState
在一个重要方面有所不同。在 XPCOM 中,该方法接受一个字符串参数并直接返回对象或 null
。然而,在 SJS 中,返回该值的过程略有不同。
function handleRequest(request, response)
{
var key = request.hasHeader("key")
? request.getHeader("key")
: "unspecified";
var obj = null;
getObjectState(key, function(objval)
{
// This function is called synchronously with the object value
// associated with key.
obj = objval;
});
response.write("Keyed object " +
(obj && Object.prototype.hasOwnProperty.call(obj, "doStuff")
? "has "
: "does not have ") +
"a doStuff method.");
}
这种特立独行的 API 是沙箱当前工作方式强加的限制:添加到沙箱中的外部函数在沙箱内被调用时不能返回对象值。但是,此类函数可以接受和调用回调函数,因此我们在这里简单地使用回调函数来返回与键关联的对象值。
高级动态响应创建¶
请求处理程序的默认行为是完全构建响应、返回,然后才发送生成的数据。但是,对于某些用例,这是不可行的。例如,一个想要返回大量数据的处理程序(例如,在 32 位系统上超过 4GB)可能会在执行此操作时耗尽内存。或者,可能需要对数据传输的时机进行精确控制,例如,接收一个请求,“暂停”同时接收并完成另一个请求,然后完成。httpd.js 通过定义一个 processAsync()
方法来解决此问题,该方法指示服务器响应将由处理程序写入和完成。这是一个 SJS 文件的示例,该文件写入一些数据,等待五秒钟,然后写入更多数据并完成响应。
var timer = null;
function handleRequest(request, response)
{
response.processAsync();
response.setHeader("Content-Type", "text/plain", false);
response.write("hello...");
timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
timer.initWithCallback(function()
{
response.write("world!");
response.finish();
}, 5 * 1000 /* milliseconds */, Ci.nsITimer.TYPE_ONE_SHOT);
}
基本流程很简单:调用 processAsync
将响应标记为异步发送,根据需要将数据写入响应正文,并在完成后调用 finish()
。目前,如果您放弃此类响应,则没有任何内容会终止连接,并且服务器无法停止(停止 API 是异步的且基于回调);将来可能会应用默认的连接超时,但就目前而言,“不要那样做”。
processAsync()
及其与其他方法的交互的完整文档,一如既往,可以在 netwerk/test/httpserver/nsIHttpServer.idl
中找到。
手动、任意响应创建¶
响应创建的标准模式是完全同步的,并且保证生成语法正确的响应(不包括标头,在大多数情况下,标头可以设置为任意值)。异步处理使响应处理与外部事件协调成为可能,但同样,在大多数情况下,只能生成语法正确的响应。第三种处理方法通过允许响应通过 seizePower()
方法包含完全任意的数据来删除正确语法属性。调用此方法后,随后写入响应的任何数据都将直接写入网络作为响应,跳过标头并且根本不尝试确保传输数据的任何格式。与异步处理一样,响应是异步生成的,并且必须手动完成才能关闭连接。(同样,对于放弃的响应,没有任何内容会终止连接,因此再次,“不要那样做”。)这种处理模式对于测试不是 HTTP 或与 httpd.js 生成的精确规范表示不匹配的特定数据格式很有用。这是一个 SJS 文件的示例,该文件写入一个明显的 HTTP 响应,其状态文本包含一个空字节(HTTP/1.1 不允许,并且尝试通过 httpd.js 设置此类状态文本将引发异常),并且其标头跨越多行(否则,httpd.js 响应只会生成单行标头)。
function handleRequest(request, response)
{
response.seizePower();
response.write("HTTP/1.1 200 OK Null byte \u0000 makes this response malformed\r\n" +
"X-Underpants-Gnomes-Strategy:\r\n" +
" Phase 1: Collect underpants.\r\n" +
" Phase 2: ...\r\n" +
" Phase 3: Profit!\r\n" +
"\r\n" +
"FAIL");
response.finish();
}
虽然异步模式能够生成某些形式的无效响应(通过在正文传输开始之前设置错误的 Content-Length 标头等),但不能以这种方式使用。不会努力保留此类实现特性(事实上,有些甚至可能随着时间的推移而被删除):如果您想发送格式错误的数据,请改用 seizePower()
。
seizePower()
及其与其他方法的交互的完整文档,一如既往,可以在 netwerk/test/httpserver/nsIHttpServer.idl
中找到。
服务器的使用示例¶
较短的示例(仅执行一项测试的测试)
netwerk/test/unit/test_bug331825.js
netwerk/test/unit/test_httpcancel.js
netwerk/test/unit/test_cookie_header.js
较长的测试(需要执行多个异步服务器请求)
netwerk/test/httpserver/test/test_setstatusline.js
netwerk/test/unit/test_content_sniffer.js
netwerk/test/unit/test_authentication.js
netwerk/test/unit/test_event_sink.js
netwerk/test/httpserver/test/
可以在 netwerk/test/httpserver/test/data/cern_meta/
中找到修改文件中 HTTP 标头的示例。
未来方向¶
该服务器虽然功能强大,但尚未完成。有许多需要修复的事情和需要添加的功能,其中包括对流水线的支持、对增量接收请求的支持(而不是在调用请求处理程序之前缓冲整个正文)以及更好地符合 HTTP/1.1 的 MUST 和 SHOULD。如果您对功能有任何建议或发现错误,请在 Testing-httpd.js 中提交。