sindresorhus/got · error · Error

The session is gracefully closing. No new streams are allowe

Error message

The session is gracefully closing. No new streams are allowed.

What it means

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.

Source

Thrown at source/core/utils/http2-client.ts:1008

			clearSessionSetup();
			entry.reject(new Error('HTTP/2 session setup canceled'));
			session.destroy();
		};

		const request = session.request.bind(session);
		session.request = (headers, streamOptions) => {
			const hasReservedStream = (session.reservedStreamCount ?? 0) > 0;

			if (hasReservedStream) {
				session.reservedStreamCount = session.reservedStreamCount! - 1;
			}

			if (session.gracefullyClosing) {
				if (hasReservedStream) {
					this.releaseStream(session, shouldPoolSession);
				}

				throw new Error('The session is gracefully closing. No new streams are allowed.');
			}

			if (!hasReservedStream) {
				this.reserveStream(session);
			}

			let stream: ClientHttp2Stream;
			try {
				stream = request(headers, streamOptions);
			} catch (error: unknown) {
				this.releaseStream(session, shouldPoolSession);
				throw error;
			}

			stream.once('close', () => {
				this.releaseStream(session, shouldPoolSession);
			});

View on GitHub (pinned to e3924aa1e5)

Solutions

  1. Let Got manage the pool (don't reuse a captured `h2session` for new requests).
  2. Increase `http2.maxConcurrentStreams` and the pool's session count so reaping is less aggressive.
  3. Catch and retry the request once — Got usually picks a fresh session on the next attempt.
  4. If using `h2session` directly, open all streams before the session goes idle, or request a new session per logical batch.

Example fix

// before: reusing a captured session
const session = await got(..., {http2: true, h2session: ref});
// ref started closing -> next call throws

// after: let the pool manage sessions
await got(url, {http2: true});
Defensive patterns

Strategy: retry

Validate before calling

// Nothing to validate pre-call; the error is a transient pool state.
// Use a bounded retry instead:
async function resilientGot(url, options, attempts = 2) {
  for (let i = 0; i < attempts; i++) {
    try {
      return await got(url, options);
    } catch (error) {
      if (i === attempts - 1 || !/gracefully closing/i.test(error.message)) throw error;
    }
  }
}

Try / catch

try {
  await got(url, {http2: true});
} catch (error) {
  if (/gracefully closing/i.test(error.message)) {
    // session was mid-close; retry once and Got will pick a fresh session
    await got(url, {http2: true});
  } else throw error;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of sindresorhus/got@e3924aa1e5 (2026-08-03). Data as JSON: /data/errors/6ba8814219d601ad.json. Report an issue: GitHub.