• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Flags: --expose-internals --experimental-abortcontroller
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 { kEvents } = require('internal/event_target');
12const Countdown = require('../common/countdown');
13
14{
15  const server = h2.createServer();
16  server.listen(0, common.mustCall(() => {
17    const destroyCallbacks = [
18      (client) => client.destroy(),
19      (client) => client[kSocket].destroy(),
20    ];
21
22    const countdown = new Countdown(destroyCallbacks.length, () => {
23      server.close();
24    });
25
26    destroyCallbacks.forEach((destroyCallback) => {
27      const client = h2.connect(`http://localhost:${server.address().port}`);
28      client.on('connect', common.mustCall(() => {
29        const socket = client[kSocket];
30
31        assert(socket, 'client session has associated socket');
32        assert(
33          !client.destroyed,
34          'client has not been destroyed before destroy is called'
35        );
36        assert(
37          !socket.destroyed,
38          'socket has not been destroyed before destroy is called'
39        );
40
41        destroyCallback(client);
42
43        client.on('close', common.mustCall(() => {
44          assert(client.destroyed);
45        }));
46
47        countdown.dec();
48      }));
49    });
50  }));
51}
52
53// Test destroy before client operations
54{
55  const server = h2.createServer();
56  server.listen(0, common.mustCall(() => {
57    const client = h2.connect(`http://localhost:${server.address().port}`);
58    const socket = client[kSocket];
59    socket.on('close', common.mustCall(() => {
60      assert(socket.destroyed);
61    }));
62
63    const req = client.request();
64    req.on('error', common.expectsError({
65      code: 'ERR_HTTP2_STREAM_CANCEL',
66      name: 'Error',
67      message: 'The pending stream has been canceled'
68    }));
69
70    client.destroy();
71
72    req.on('response', common.mustNotCall());
73
74    const sessionError = {
75      name: 'Error',
76      code: 'ERR_HTTP2_INVALID_SESSION',
77      message: 'The session has been destroyed'
78    };
79
80    assert.throws(() => client.setNextStreamID(), sessionError);
81    assert.throws(() => client.setLocalWindowSize(), sessionError);
82    assert.throws(() => client.ping(), sessionError);
83    assert.throws(() => client.settings({}), sessionError);
84    assert.throws(() => client.goaway(), sessionError);
85    assert.throws(() => client.request(), sessionError);
86    client.close();  // Should be a non-op at this point
87
88    // Wait for setImmediate call from destroy() to complete
89    // so that state.destroyed is set to true
90    setImmediate(() => {
91      assert.throws(() => client.setNextStreamID(), sessionError);
92      assert.throws(() => client.setLocalWindowSize(), sessionError);
93      assert.throws(() => client.ping(), sessionError);
94      assert.throws(() => client.settings({}), sessionError);
95      assert.throws(() => client.goaway(), sessionError);
96      assert.throws(() => client.request(), sessionError);
97      client.close();  // Should be a non-op at this point
98    });
99
100    req.resume();
101    req.on('end', common.mustNotCall());
102    req.on('close', common.mustCall(() => server.close()));
103  }));
104}
105
106// Test destroy before goaway
107{
108  const server = h2.createServer();
109  server.on('stream', common.mustCall((stream) => {
110    stream.session.destroy();
111  }));
112
113  server.listen(0, common.mustCall(() => {
114    const client = h2.connect(`http://localhost:${server.address().port}`);
115
116    client.on('close', () => {
117      server.close();
118      // Calling destroy in here should not matter
119      client.destroy();
120    });
121
122    client.request();
123  }));
124}
125
126// Test destroy before connect
127{
128  const server = h2.createServer();
129  server.on('stream', common.mustNotCall());
130
131  server.listen(0, common.mustCall(() => {
132    const client = h2.connect(`http://localhost:${server.address().port}`);
133
134    server.on('connection', common.mustCall(() => {
135      server.close();
136      client.close();
137    }));
138
139    const req = client.request();
140    req.destroy();
141  }));
142}
143
144// Test close before connect
145{
146  const server = h2.createServer();
147
148  server.on('stream', common.mustNotCall());
149  server.listen(0, common.mustCall(() => {
150    const client = h2.connect(`http://localhost:${server.address().port}`);
151    client.on('close', common.mustCall());
152    const socket = client[kSocket];
153    socket.on('close', common.mustCall(() => {
154      assert(socket.destroyed);
155    }));
156
157    const req = client.request();
158    // Should throw goaway error
159    req.on('error', common.expectsError({
160      code: 'ERR_HTTP2_GOAWAY_SESSION',
161      name: 'Error',
162      message: 'New streams cannot be created after receiving a GOAWAY'
163    }));
164
165    client.close();
166    req.resume();
167    req.on('end', common.mustCall());
168    req.on('close', common.mustCall(() => server.close()));
169  }));
170}
171
172// Destroy with AbortSignal
173{
174  const server = h2.createServer();
175  const controller = new AbortController();
176
177  server.on('stream', common.mustNotCall());
178  server.listen(0, common.mustCall(() => {
179    const client = h2.connect(`http://localhost:${server.address().port}`);
180    client.on('close', common.mustCall());
181
182    const { signal } = controller;
183    assert.strictEqual(signal[kEvents].get('abort'), undefined);
184
185    client.on('error', common.mustCall(() => {
186      // After underlying stream dies, signal listener detached
187      assert.strictEqual(signal[kEvents].get('abort'), undefined);
188    }));
189
190    const req = client.request({}, { signal });
191
192    req.on('error', common.mustCall((err) => {
193      assert.strictEqual(err.code, 'ABORT_ERR');
194      assert.strictEqual(err.name, 'AbortError');
195    }));
196    req.on('close', common.mustCall(() => server.close()));
197
198    assert.strictEqual(req.aborted, false);
199    assert.strictEqual(req.destroyed, false);
200    // Signal listener attached
201    assert.strictEqual(signal[kEvents].get('abort').size, 1);
202
203    controller.abort();
204
205    assert.strictEqual(req.aborted, false);
206    assert.strictEqual(req.destroyed, true);
207  }));
208}
209// Pass an already destroyed signal to abort immediately.
210{
211  const server = h2.createServer();
212  const controller = new AbortController();
213
214  server.on('stream', common.mustNotCall());
215  server.listen(0, common.mustCall(() => {
216    const client = h2.connect(`http://localhost:${server.address().port}`);
217    client.on('close', common.mustCall());
218
219    const { signal } = controller;
220    controller.abort();
221
222    assert.strictEqual(signal[kEvents].get('abort'), undefined);
223
224    client.on('error', common.mustCall(() => {
225      // After underlying stream dies, signal listener detached
226      assert.strictEqual(signal[kEvents].get('abort'), undefined);
227    }));
228
229    const req = client.request({}, { signal });
230    // Signal already aborted, so no event listener attached.
231    assert.strictEqual(signal[kEvents].get('abort'), undefined);
232
233    assert.strictEqual(req.aborted, false);
234    // Destroyed on same tick as request made
235    assert.strictEqual(req.destroyed, true);
236
237    req.on('error', common.mustCall((err) => {
238      assert.strictEqual(err.code, 'ABORT_ERR');
239      assert.strictEqual(err.name, 'AbortError');
240    }));
241    req.on('close', common.mustCall(() => server.close()));
242  }));
243}
244