HTTP 请求的生命周期

Firefox 中的 HTTP 请求会经历几个步骤。请求消息和响应消息的各个部分会在特定的时间点变得可用。提取这些信息是一项挑战。

何时可用

数据

可用时间

示例 JS 代码

接口

测试代码

HTTP 请求方法

http-on-modify-request 观察者通知

channel.requestMethod

nsIHttpChannel

HTTP 请求 URI

http-on-modify-request 观察者通知

channel.URI

nsIChannel

HTTP 请求头

http-on-modify-request 观察者通知

channel.visitRequestHeaders(visitor)

nsIHttpChannel

HTTP 请求体

http-on-modify-request 观察者通知

channel.uploadStream

nsIUploadChannel

HTTP 响应状态

http-on-examine-response 观察者通知

channel.responseStatus
channel.responseStatusText

HTTP 响应头

http-on-examine-response 观察者通知

channel.visitResponseHeaders(visitor)

nsIHttpChannel

HTTP 响应体


onStopRequest 通过流监听器分流


见下文


请求:http-on-modify-request

在发送 HTTP 请求之前,Firefox 会触发“http-on-modify-request”观察者通知,并且会在所有观察者退出之前阻塞请求的发送。这通常是您可以修改 HTTP 请求头的地方(因此得名)。

为请求附加监听器非常简单

const obs = {
  QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),

  observe: function(channel, topic, data) {
    if (!(channel instanceof Ci.nsIHttpChannel))
      return;

    // process the channel's data
  }
}

Services.obs.addObserver(observer, "http-on-modify-request", false);

有关详细信息,请参阅 nsIObserverService

此时,请求方法和 URI 会立即可用。请求头也很容易获取

/**
 * HTTP header visitor.
 */
class HeaderVisitor {
  #targetObject;

  constructor(targetObject) {
    this.#targetObject = targetObject;
  }

  // nsIHttpHeaderVisitor
  visitHeader(header, value) {
    this.#targetObject[header] = value;
  }

  QueryInterface = ChromeUtils.generateQI(["nsIHttpHeaderVisitor"]);
}

// ...
const requestHeaders = {};
const visitor = new HeaderVisitor(requestHeaders);
channel.visitRequestHeaders(visitor);

如果需要,您也可以在此处设置请求头。在 nsIHttpChannel 接口上的方法是 channel.setRequestHeader(header, value);

大多数 HTTP 请求没有主体,因为它们是 GET 请求。但是,POST 请求通常有主体。正如 nsIUploadChannel 文档所述,大多数 HTTP 请求的主体可以通过可寻址流 (nsISeekableStream) 获取。因此,您可以简单地捕获主体流及其当前位置,以便稍后重新访问它。network-helper.js 包含读取请求体的代码。

响应:http-on-examine-response

在解析 HTTP 响应状态和头之后,但读取响应体之前,Firefox 会触发“http-on-examine-response”观察者通知。为这个阶段附加监听器也非常简单

Services.obs.addObserver(observer, "http-on-examine-response", false);

如果您对“http-on-modify-request”和“http-on-examine-response”使用相同的观察者,请确保在与通道交互之前检查主题参数。

响应状态可通过 responseStatusresponseStatusText 属性获取。响应头可通过 visitResponseHeaders 方法获取,并且需要相同的接口。

响应体:onStopRequest,流监听器分流

在“http-on-examine-response”通知期间,响应体是不可用的。但是,您可以使用流监听器分流来复制流,以便原始流数据继续传递,并且您有一个单独的输入流,您可以从中读取相同的数据。

以下是一些示例代码,说明您需要什么

const Pipe = Components.Constructor(
  "@mozilla.org/pipe;1",
  "nsIPipe",
  "init"
);
const StreamListenerTee = Components.Constructor(
  "@mozilla.org/network/stream-listener-tee;1",
  "nsIStreamListenerTee"
);
const ScriptableStream = Components.Constructor(
  "@mozilla.org/scriptableinputstream;1",
  "nsIScriptableInputStream",
  "init"
);

const obs = {
  QueryInterface: ChromeUtils.generateQI(["nsIObserver", "nsIRequestObserver"]),

  /** @typedef {WeakMap<nsIHttpChannel, nsIPipe>} */
  requestToTeePipe: new WeakMap,

  // nsIObserver
  observe: function(channel, topic, data) {
    if (!(channel instanceof Ci.nsIHttpChannel))
      return;

    /* Create input and output streams to take the new data.
       The 0xffffffff argument is the segment count.
       It has to be this high because you don't know how much data is coming in the response body.

       As for why these are blocking streams:  I believe this is because there's no actual need to make them non-blocking.
       The stream processing happens during onStopRequest(), so we have all the data then and the operation can be synchronous.
       But I could be very wrong on this.
    */
    const pipe = new Pipe(false, false, 0, 0xffffffff);

    // Install the stream listener tee to intercept the HTTP body.
    const tee = new StreamListenerTee;
    const originalListener = channel.setNewListener(tee);
    tee.init(originalListener, pipe.outputStream, this);

    this.requestToTeePipe.set(channel, pipe);
  }

  // nsIRequestObserver
  onStartRequest: function() {
    // do nothing
  }

  // nsIRequestObserver
  onStopRequest: function(channel, statusCode) {
    const pipe = this.requestToTeePipe.get(channel);

    // No more data coming in anyway.
    pipe.outputStream.close();
    this.requestToTeePipe.delete(channel);

    let length = 0;
    try {
      length = pipe.inputStream.available();
    }
    catch (e) {
      if (e.result === Components.results.NS_BASE_STREAM_CLOSED)
        throw e;
    }

    let responseBody = "";
    if (length) {
      // C++ code doesn't need the scriptable input stream.
      const sin = new ScriptableStream(pipe.inputStream);
      responseBody = sin.read(length);
      sin.close();
    }

    void(responseBody); // do something with the body
  }
}

test_traceable_channel.js 基本上就是这么做的。

字符编码和压缩

取消请求

HTTP 活动分发器说明

URIContentLoader 说明

操作顺序

  1. 构建 HTTP 通道。

  2. 触发“http-on-modify-request”观察者服务通知。

  3. 如果请求已取消,则在此步骤退出。

  4. 将 HTTP 通道的请求提交到服务器。时间流逝。

  5. HTTP 通道的响应从服务器传入。

  6. HTTP 通道解析响应状态和头。

  7. 触发“http-on-examine-response”观察者服务通知。

有用的代码示例和参考