• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Flags: --expose-internals
2
3'use strict';
4
5const common = require('../common');
6if (!common.hasCrypto)
7  common.skip('missing crypto');
8const assert = require('assert');
9const h2 = require('http2');
10const { kSocket } = require('internal/http2/util');
11const Countdown = require('../common/countdown');
12const { getEventListeners } = require('events');
13{
14  const server = h2.createServer();
15  server.listen(0, common.mustCall(() => {
16    const destroyCallbacks = [
17      (client) => client.destroy(),
18      (client) => client[kSocket].destroy(),
19    ];
20
21    const countdown = new Countdown(destroyCallbacks.length, () => {
22      server.close();
23    });
24
25    for (const destroyCallback of destroyCallbacks) {
26      const client = h2.connect(`http://localhost:${server.address().port}`);
27      client.on('connect', common.mustCall(() => {
28        const socket = client[kSocket];
29
30        assert(socket, 'client session has associated socket');
31        assert(
32          !client.destroyed,
33          'client has not been destroyed before destroy is called'
34        );
35        assert(
36          !socket.destroyed,
37          'socket has not been destroyed before destroy is called'
38        );
39
40        destroyCallback(client);
41
42        client.on('close', common.mustCall(() => {
43          assert(client.destroyed);
44        }));
45
46        countdown.dec();
47      }));
48    }
49  }));
50}
51
52// Test destroy before client operations
53{
54  const server = h2.createServer();
55  server.listen(0, common.mustCall(() => {
56    const client = h2.connect(`http://localhost:${server.address().port}`);
57    const socket = client[kSocket];
58    socket.on('close', common.mustCall(() => {
59      assert(socket.destroyed);
60    }));
61
62    const req = client.request();
63    req.on('error', common.expectsError({
64      code: 'ERR_HTTP2_STREAM_CANCEL',
65      name: 'Error',
66      message: 'The pending stream has been canceled'
67    }));
68
69    client.destroy();
70
71    req.on('response', common.mustNotCall());
72
73    const sessionError = {
74      name: 'Error',
75      code: 'ERR_HTTP2_INVALID_SESSION',
76      message: 'The session has been destroyed'
77    };
78
79    assert.throws(() => client.setNextStreamID(), sessionError);
80    assert.throws(() => client.setLocalWindowSize(), sessionError);
81    assert.throws(() => client.ping(), sessionError);
82    assert.throws(() => client.settings({}), sessionError);
83    assert.throws(() => client.goaway(), sessionError);
84    assert.throws(() => client.request(), sessionError);
85    client.close();  // Should be a non-op at this point
86
87    // Wait for setImmediate call from destroy() to complete
88    // so that state.destroyed is set to true
89    setImmediate(() => {
90      assert.throws(() => client.setNextStreamID(), sessionError);
91      assert.throws(() => client.setLocalWindowSize(), sessionError);
92      assert.throws(() => client.ping(), sessionError);
93      assert.throws(() => client.settings({}), sessionError);
94      assert.throws(() => client.goaway(), sessionError);
95      assert.throws(() => client.request(), sessionError);
96      client.close();  // Should be a non-op at this point
97    });
98
99    req.resume();
100    req.on('end', common.mustNotCall());
101    req.on('close', common.mustCall(() => server.close()));
102  }));
103}
104
105// Test destroy before goaway
106{
107  const server = h2.createServer();
108  server.on('stream', common.mustCall((stream) => {
109    stream.session.destroy();
110  }));
111
112  server.listen(0, common.mustCall(() => {
113    const client = h2.connect(`http://localhost:${server.address().port}`);
114
115    client.on('close', () => {
116      server.close();
117      // Calling destroy in here should not matter
118      client.destroy();
119    });
120
121    client.request();
122  }));
123}
124
125// Test destroy before connect
126{
127  const server = h2.createServer();
128  server.on('stream', common.mustNotCall());
129
130  server.listen(0, common.mustCall(() => {
131    const client = h2.connect(`http://localhost:${server.address().port}`);
132
133    server.on('connection', common.mustCall(() => {
134      server.close();
135      client.close();
136    }));
137
138    const req = client.request();
139    req.destroy();
140  }));
141}
142
143// Test close before connect
144{
145  const server = h2.createServer();
146
147  server.on('stream', common.mustNotCall());
148  server.listen(0, common.mustCall(() => {
149    const client = h2.connect(`http://localhost:${server.address().port}`);
150    client.on('close', common.mustCall());
151    const socket = client[kSocket];
152    socket.on('close', common.mustCall(() => {
153      assert(socket.destroyed);
154    }));
155
156    const req = client.request();
157    // Should throw goaway error
158    req.on('error', common.expectsError({
159      code: 'ERR_HTTP2_GOAWAY_SESSION',
160      name: 'Error',
161      message: 'New streams cannot be created after receiving a GOAWAY'
162    }));
163
164    client.close();
165    req.resume();
166    req.on('end', common.mustNotCall());
167    req.on('close', common.mustCall(() => server.close()));
168  }));
169}
170
171// Destroy with AbortSignal
172{
173  const server = h2.createServer();
174  const controller = new AbortController();
175
176  server.on('stream', common.mustNotCall());
177  server.listen(0, common.mustCall(() => {
178    const client = h2.connect(`http://localhost:${server.address().port}`);
179    client.on('close', common.mustCall());
180
181    const { signal } = controller;
182    assert.strictEqual(getEventListeners(signal, 'abort').length, 0);
183
184    client.on('error', common.mustCall(() => {
185      // After underlying stream dies, signal listener detached
186      assert.strictEqual(getEventListeners(signal, 'abort').length, 0);
187    }));
188
189    const req = client.request({}, { signal });
190
191    req.on('error', common.mustCall((err) => {
192      assert.strictEqual(err.code, 'ABORT_ERR');
193      assert.strictEqual(err.name, 'AbortError');
194    }));
195    req.on('close', common.mustCall(() => server.close()));
196
197    assert.strictEqual(req.aborted, false);
198    assert.strictEqual(req.destroyed, false);
199    // Signal listener attached
200    assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
201
202    controller.abort();
203
204    assert.strictEqual(req.aborted, false);
205    assert.strictEqual(req.destroyed, true);
206  }));
207}
208// Pass an already destroyed signal to abort immediately.
209{
210  const server = h2.createServer();
211  const controller = new AbortController();
212
213  server.on('stream', common.mustNotCall());
214  server.listen(0, common.mustCall(() => {
215    const client = h2.connect(`http://localhost:${server.address().port}`);
216    client.on('close', common.mustCall());
217
218    const { signal } = controller;
219    controller.abort();
220
221    assert.strictEqual(getEventListeners(signal, 'abort').length, 0);
222
223    client.on('error', common.mustCall(() => {
224      // After underlying stream dies, signal listener detached
225      assert.strictEqual(getEventListeners(signal, 'abort').length, 0);
226    }));
227
228    const req = client.request({}, { signal });
229    // Signal already aborted, so no event listener attached.
230    assert.strictEqual(getEventListeners(signal, 'abort').length, 0);
231
232    assert.strictEqual(req.aborted, false);
233    // Destroyed on same tick as request made
234    assert.strictEqual(req.destroyed, true);
235
236    req.on('error', common.mustCall((err) => {
237      assert.strictEqual(err.code, 'ABORT_ERR');
238      assert.strictEqual(err.name, 'AbortError');
239    }));
240    req.on('close', common.mustCall(() => server.close()));
241  }));
242}
243
244
245// Destroy ClientHttpSession with AbortSignal
246{
247  function testH2ConnectAbort(secure) {
248    const server = secure ? h2.createSecureServer() : h2.createServer();
249    const controller = new AbortController();
250
251    server.on('stream', common.mustNotCall());
252    server.listen(0, common.mustCall(() => {
253      const { signal } = controller;
254      const protocol = secure ? 'https' : 'http';
255      const client = h2.connect(`${protocol}://localhost:${server.address().port}`, {
256        signal,
257      });
258      client.on('close', common.mustCall());
259      assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
260
261      client.on('error', common.mustCall(common.mustCall((err) => {
262        assert.strictEqual(err.code, 'ABORT_ERR');
263        assert.strictEqual(err.name, 'AbortError');
264      })));
265
266      const req = client.request({}, {});
267      assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
268
269      req.on('error', common.mustCall((err) => {
270        assert.strictEqual(err.code, 'ERR_HTTP2_STREAM_CANCEL');
271        assert.strictEqual(err.name, 'Error');
272        assert.strictEqual(req.aborted, false);
273        assert.strictEqual(req.destroyed, true);
274      }));
275      req.on('close', common.mustCall(() => server.close()));
276
277      assert.strictEqual(req.aborted, false);
278      assert.strictEqual(req.destroyed, false);
279      // Signal listener attached
280      assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
281
282      controller.abort();
283    }));
284  }
285  testH2ConnectAbort(false);
286  testH2ConnectAbort(true);
287}
288
289// Destroy ClientHttp2Stream with AbortSignal
290{
291  const server = h2.createServer();
292  const controller = new AbortController();
293
294  server.on('stream', common.mustCall((stream) => {
295    stream.on('error', common.mustNotCall());
296    stream.on('close', common.mustCall(() => {
297      assert.strictEqual(stream.rstCode, h2.constants.NGHTTP2_CANCEL);
298      server.close();
299    }));
300    controller.abort();
301  }));
302  server.listen(0, common.mustCall(() => {
303    const client = h2.connect(`http://localhost:${server.address().port}`);
304    client.on('close', common.mustCall());
305
306    const { signal } = controller;
307    const req = client.request({}, { signal });
308    assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
309    req.on('error', common.mustCall((err) => {
310      assert.strictEqual(err.code, 'ABORT_ERR');
311      assert.strictEqual(err.name, 'AbortError');
312      client.close();
313    }));
314    req.on('close', common.mustCall());
315  }));
316}
317