|
1 | 1 | 'use strict'; |
2 | 2 |
|
3 | 3 | // Regression test: when the server abruptly destroys the underlying socket, |
4 | | -// the client session 'close' event must fire before any stream 'close' |
5 | | -// callback can observe session.closed/session.destroyed as true. |
6 | | -// See https://github.com/nodejs/node/issues/<issue> |
| 4 | +// session.request() must not throw synchronously inside a stream 'close' |
| 5 | +// callback. Instead it should return a stream that emits 'error' async with |
| 6 | +// ERR_HTTP2_INVALID_SESSION, giving session lifecycle handlers a chance to |
| 7 | +// clear their cached session reference before any retry occurs. |
7 | 8 |
|
8 | 9 | const common = require('../common'); |
9 | 10 | if (!common.hasCrypto) |
@@ -36,33 +37,34 @@ server.on('stream', (stream, headers) => { |
36 | 37 | server.listen(0, common.mustCall(() => { |
37 | 38 | const session = http2.connect(`http://localhost:${server.address().port}`); |
38 | 39 |
|
39 | | - let sessionCloseFired = false; |
40 | | - |
41 | | - session.on('close', common.mustCall(() => { |
42 | | - sessionCloseFired = true; |
43 | | - server.close(); |
44 | | - })); |
45 | | - |
46 | | - // We accept that an error may or may not fire, but it must fire before |
47 | | - // any stream close callback sees session.destroyed. |
| 40 | + session.on('close', common.mustCall(() => server.close())); |
48 | 41 | session.on('error', () => {}); |
49 | 42 |
|
50 | 43 | const req = session.request({ ':path': '/close' }); |
51 | 44 | req.resume(); |
52 | 45 | req.on('response', () => {}); |
| 46 | + req.on('error', () => {}); // socket may emit ECONNRESET on abrupt close |
53 | 47 |
|
54 | | - // The stream 'close' event must not observe the session as destroyed |
55 | | - // before the session 'close' event has fired. |
56 | 48 | req.on('close', common.mustCall(() => { |
57 | | - // If the session is already destroyed, the session 'close' event must |
58 | | - // have already been emitted (sessionCloseFired === true). |
59 | | - if (session.destroyed) { |
60 | | - assert.strictEqual( |
61 | | - sessionCloseFired, |
62 | | - true, |
63 | | - 'session "close" event must fire before stream "close" callback ' + |
64 | | - 'observes session.destroyed === true', |
65 | | - ); |
| 49 | + if (!session.destroyed) return; |
| 50 | + |
| 51 | + // session.request() must NOT throw synchronously even though the session |
| 52 | + // is already destroyed. It should return a stream that errors async. |
| 53 | + let threw = false; |
| 54 | + let req2; |
| 55 | + try { |
| 56 | + req2 = session.request({ ':path': '/again' }); |
| 57 | + } catch { |
| 58 | + threw = true; |
66 | 59 | } |
| 60 | + |
| 61 | + assert.strictEqual(threw, false, |
| 62 | + 'session.request() must not throw synchronously inside a stream close callback'); |
| 63 | + |
| 64 | + // The returned stream should asynchronously emit ERR_HTTP2_INVALID_SESSION |
| 65 | + req2.on('error', common.mustCall((err) => { |
| 66 | + assert.strictEqual(err.code, 'ERR_HTTP2_INVALID_SESSION'); |
| 67 | + })); |
| 68 | + req2.resume(); |
67 | 69 | })); |
68 | 70 | })); |
0 commit comments