{"id":"6ba8814219d601ad","repo":"sindresorhus/got","slug":"the-session-is-gracefully-closing-no-new-streams","errorCode":null,"errorMessage":"The session is gracefully closing. No new streams are allowed.","messagePattern":"The session is gracefully closing\\. No new streams are allowed\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"source/core/utils/http2-client.ts","lineNumber":1008,"sourceCode":"\t\t\tclearSessionSetup();\n\t\t\tentry.reject(new Error('HTTP/2 session setup canceled'));\n\t\t\tsession.destroy();\n\t\t};\n\n\t\tconst request = session.request.bind(session);\n\t\tsession.request = (headers, streamOptions) => {\n\t\t\tconst hasReservedStream = (session.reservedStreamCount ?? 0) > 0;\n\n\t\t\tif (hasReservedStream) {\n\t\t\t\tsession.reservedStreamCount = session.reservedStreamCount! - 1;\n\t\t\t}\n\n\t\t\tif (session.gracefullyClosing) {\n\t\t\t\tif (hasReservedStream) {\n\t\t\t\t\tthis.releaseStream(session, shouldPoolSession);\n\t\t\t\t}\n\n\t\t\t\tthrow new Error('The session is gracefully closing. No new streams are allowed.');\n\t\t\t}\n\n\t\t\tif (!hasReservedStream) {\n\t\t\t\tthis.reserveStream(session);\n\t\t\t}\n\n\t\t\tlet stream: ClientHttp2Stream;\n\t\t\ttry {\n\t\t\t\tstream = request(headers, streamOptions);\n\t\t\t} catch (error: unknown) {\n\t\t\t\tthis.releaseStream(session, shouldPoolSession);\n\t\t\t\tthrow error;\n\t\t\t}\n\n\t\t\tstream.once('close', () => {\n\t\t\t\tthis.releaseStream(session, shouldPoolSession);\n\t\t\t});\n","sourceCodeStart":990,"sourceCodeEnd":1026,"githubUrl":"https://github.com/sindresorhus/got/blob/e3924aa1e53a6ca3eb93a43618ce532442a89b40/source/core/utils/http2-client.ts#L990-L1026","documentation":"From the pooled HTTP/2 session manager at http2-client.ts:1008: when a session is mid-graceful-close (`session.gracefullyClosing = true`, set during idle reaping at line 965), any new `session.request()` call throws because opening a stream on a closing session would race the GOAWAY. The throw is the pool's way of saying 'pick a different session'; normally the pool retries on another session, but if every session is closing this surfaces to the caller.","triggerScenarios":"High-concurrency HTTP/2 traffic where the pool reaps an idle session between the moment Got selects it and the moment the stream opens; or calling `session.request(...)` directly on a session you obtained via `h2session`. Also fires under bursty traffic that exhausts maxConcurrentStreams on remaining live sessions.","commonSituations":"Spiky load against an HTTP/2 server with strict idle timeouts; misconfigured `http2.maxConcurrentStreams` / pool size; holding a reference to an `h2session` and reusing it after it began closing; a misbehaving server sending GOAWAY aggressively.","solutions":["Let Got manage the pool (don't reuse a captured `h2session` for new requests).","Increase `http2.maxConcurrentStreams` and the pool's session count so reaping is less aggressive.","Catch and retry the request once — Got usually picks a fresh session on the next attempt.","If using `h2session` directly, open all streams before the session goes idle, or request a new session per logical batch."],"exampleFix":"// before: reusing a captured session\nconst session = await got(..., {http2: true, h2session: ref});\n// ref started closing -> next call throws\n\n// after: let the pool manage sessions\nawait got(url, {http2: true});","handlingStrategy":"retry","validationCode":"// Nothing to validate pre-call; the error is a transient pool state.\n// Use a bounded retry instead:\nasync function resilientGot(url, options, attempts = 2) {\n  for (let i = 0; i < attempts; i++) {\n    try {\n      return await got(url, options);\n    } catch (error) {\n      if (i === attempts - 1 || !/gracefully closing/i.test(error.message)) throw error;\n    }\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  await got(url, {http2: true});\n} catch (error) {\n  if (/gracefully closing/i.test(error.message)) {\n    // session was mid-close; retry once and Got will pick a fresh session\n    await got(url, {http2: true});\n  } else throw error;\n}","preventionTips":["Let Got own the HTTP/2 session pool — don't capture and reuse `h2session`.","Tune `http2.maxConcurrentStreams` and pool size for bursty traffic.","Wrap HTTP/2 calls in a single bounded retry for transient pool races.","Avoid holding idle sessions long enough to be reaped mid-burst."],"tags":["http2","connection-pooling","concurrency"],"analyzedSha":"e3924aa1e53a6ca3eb93a43618ce532442a89b40","analyzedAt":"2026-08-03T19:22:24.770Z","schemaVersion":2}