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