summaryrefslogtreecommitdiffstats
path: root/netwerk/test/unit/test_httpcancel.js
blob: 04958ffd2b2bf1c8223b8486f2ef92b42999c6db (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
// This file ensures that canceling a channel early does not
// send the request to the server (bug 350790)
//
// I've also shoehorned in a test that ENSURE_CALLED_BEFORE_CONNECT works as
// expected: see comments that start with ENSURE_CALLED_BEFORE_CONNECT:
//
// This test also checks that cancelling a channel before asyncOpen, after
// onStopRequest, or during onDataAvailable works as expected.

"use strict";

const { HttpServer } = ChromeUtils.importESModule(
  "resource://testing-common/httpd.sys.mjs"
);
const reason = "testing";

function inChildProcess() {
  return Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
}

var ios = Services.io;
var ReferrerInfo = Components.Constructor(
  "@mozilla.org/referrer-info;1",
  "nsIReferrerInfo",
  "init"
);
var observer = {
  QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),

  observe(subject, topic, data) {
    subject = subject.QueryInterface(Ci.nsIRequest);
    subject.cancelWithReason(Cr.NS_BINDING_ABORTED, reason);

    // ENSURE_CALLED_BEFORE_CONNECT: setting values should still work
    try {
      subject.QueryInterface(Ci.nsIHttpChannel);
      let currentReferrer = subject.getRequestHeader("Referer");
      Assert.equal(currentReferrer, "http://site1.com/");
      var uri = ios.newURI("http://site2.com");
      subject.referrerInfo = new ReferrerInfo(
        Ci.nsIReferrerInfo.EMPTY,
        true,
        uri
      );
    } catch (ex) {
      do_throw("Exception: " + ex);
    }
  },
};

let cancelDuringOnStartListener = {
  onStartRequest: function test_onStartR(request) {
    Assert.equal(request.status, Cr.NS_BINDING_ABORTED);
    // We didn't sync the reason to child process.
    if (!inChildProcess()) {
      Assert.equal(request.canceledReason, reason);
    }

    // ENSURE_CALLED_BEFORE_CONNECT: setting referrer should now fail
    try {
      request.QueryInterface(Ci.nsIHttpChannel);
      let currentReferrer = request.getRequestHeader("Referer");
      Assert.equal(currentReferrer, "http://site2.com/");
      var uri = ios.newURI("http://site3.com/");

      // Need to set NECKO_ERRORS_ARE_FATAL=0 else we'll abort process
      Services.env.set("NECKO_ERRORS_ARE_FATAL", "0");
      // we expect setting referrer to fail
      try {
        request.referrerInfo = new ReferrerInfo(
          Ci.nsIReferrerInfo.EMPTY,
          true,
          uri
        );
        do_throw("Error should have been thrown before getting here");
      } catch (ex) {}
    } catch (ex) {
      do_throw("Exception: " + ex);
    }
  },

  onDataAvailable: function test_ODA() {
    do_throw("Should not get any data!");
  },

  onStopRequest: function test_onStopR(request, status) {
    this.resolved();
  },
};

var cancelDuringOnDataListener = {
  data: "",
  channel: null,
  receivedSomeData: null,
  onStartRequest: function test_onStartR(request, ctx) {
    Assert.equal(request.status, Cr.NS_OK);
  },

  onDataAvailable: function test_ODA(request, stream, offset, count) {
    let string = NetUtil.readInputStreamToString(stream, count);
    Assert.ok(!string.includes("b"));
    this.data += string;
    this.channel.cancel(Cr.NS_BINDING_ABORTED);
    if (this.receivedSomeData) {
      this.receivedSomeData();
    }
  },

  onStopRequest: function test_onStopR(request, ctx, status) {
    Assert.ok(this.data.includes("a"), `data: ${this.data}`);
    Assert.equal(request.status, Cr.NS_BINDING_ABORTED);
    this.resolved();
  },
};

function makeChan(url) {
  var chan = NetUtil.newChannel({
    uri: url,
    loadUsingSystemPrincipal: true,
  }).QueryInterface(Ci.nsIHttpChannel);

  // ENSURE_CALLED_BEFORE_CONNECT: set original value
  var uri = ios.newURI("http://site1.com");
  chan.referrerInfo = new ReferrerInfo(Ci.nsIReferrerInfo.EMPTY, true, uri);
  return chan;
}

var httpserv = null;

add_task(async function setup() {
  httpserv = new HttpServer();
  httpserv.registerPathHandler("/failtest", failtest);
  httpserv.registerPathHandler("/cancel_middle", cancel_middle);
  httpserv.registerPathHandler("/normal_response", normal_response);
  httpserv.start(-1);

  registerCleanupFunction(async () => {
    await new Promise(resolve => httpserv.stop(resolve));
  });
});

add_task(async function test_cancel_during_onModifyRequest() {
  var chan = makeChan(
    "http://localhost:" + httpserv.identity.primaryPort + "/failtest"
  );

  if (!inChildProcess()) {
    Services.obs.addObserver(observer, "http-on-modify-request");
  } else {
    do_send_remote_message("register-observer");
    await do_await_remote_message("register-observer-done");
  }

  await new Promise(resolve => {
    cancelDuringOnStartListener.resolved = resolve;
    chan.asyncOpen(cancelDuringOnStartListener);
  });

  if (!inChildProcess()) {
    Services.obs.removeObserver(observer, "http-on-modify-request");
  } else {
    do_send_remote_message("unregister-observer");
    await do_await_remote_message("unregister-observer-done");
  }
});

add_task(async function test_cancel_before_asyncOpen() {
  var chan = makeChan(
    "http://localhost:" + httpserv.identity.primaryPort + "/failtest"
  );

  chan.cancel(Cr.NS_BINDING_ABORTED);

  Assert.throws(
    () => {
      chan.asyncOpen(cancelDuringOnStartListener);
    },
    /NS_BINDING_ABORTED/,
    "cannot open if already cancelled"
  );
});

add_task(async function test_cancel_during_onData() {
  var chan = makeChan(
    "http://localhost:" + httpserv.identity.primaryPort + "/cancel_middle"
  );

  await new Promise(resolve => {
    cancelDuringOnDataListener.resolved = resolve;
    cancelDuringOnDataListener.channel = chan;
    chan.asyncOpen(cancelDuringOnDataListener);
  });
});

var cancelAfterOnStopListener = {
  data: "",
  channel: null,
  onStartRequest: function test_onStartR(request, ctx) {
    Assert.equal(request.status, Cr.NS_OK);
  },

  onDataAvailable: function test_ODA(request, stream, offset, count) {
    let string = NetUtil.readInputStreamToString(stream, count);
    this.data += string;
  },

  onStopRequest: function test_onStopR(request, status) {
    info("onStopRequest");
    Assert.equal(request.status, Cr.NS_OK);
    this.resolved();
  },
};

add_task(async function test_cancel_after_onStop() {
  var chan = makeChan(
    "http://localhost:" + httpserv.identity.primaryPort + "/normal_response"
  );

  await new Promise(resolve => {
    cancelAfterOnStopListener.resolved = resolve;
    cancelAfterOnStopListener.channel = chan;
    chan.asyncOpen(cancelAfterOnStopListener);
  });
  Assert.equal(chan.status, Cr.NS_OK);

  // For now it's unclear if cancelling after onStop should throw,
  // silently fail, or overwrite the channel's status as we currently do.
  // See discussion in bug 1553083
  chan.cancel(Cr.NS_BINDING_ABORTED);
  Assert.equal(chan.status, Cr.NS_BINDING_ABORTED);
});

// PATHS

// /failtest
function failtest(metadata, response) {
  do_throw("This should not be reached");
}

function cancel_middle(metadata, response) {
  response.processAsync();
  response.setStatusLine(metadata.httpVersion, 200, "OK");
  let str1 = "a".repeat(128 * 1024);
  response.write(str1, str1.length);
  response.bodyOutputStream.flush();

  let p = new Promise(resolve => {
    cancelDuringOnDataListener.receivedSomeData = resolve;
  });
  p.then(() => {
    let str2 = "b".repeat(128 * 1024);
    response.write(str2, str2.length);
    response.finish();
  });
}

function normal_response(metadata, response) {
  response.setStatusLine(metadata.httpVersion, 200, "OK");
  let str1 = "Is this normal?";
  response.write(str1, str1.length);
}