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