Repository: nodejs/node
Pull request: http2: introduce http1Options and use storeHTTPOptions and initialize keepAliveTimeout and keepAliveTimeoutBuffer when allowHTTP1 is true

This PR fixes an issue where `keepAliveTimeout` and related timeout properties were `undefined` on `Http2SecureServer` instances, even when `allowHTTP1: true` was set.

When falling back to HTTP/1.1, the server should respect standard HTTP timeout behaviors. This change ensures these properties are initialized with their default values (matching `http.Server` and `https.Server`), preventing inconsistent behavior during HTTP/1.1 fallback.

Fixes: #59783

Changed files:
filename: doc/api/deprecations.md
patch: @@ -4430,6 +4430,42 @@ Passing the `type` option to [`Duplex.toWeb()`][] is deprecated. To specify the
 type of the readable half of the constructed readable-writable pair, use the
 `readableType` option instead.
 
+### DEP0202: `Http1IncomingMessage` and `Http1ServerResponse` options of HTTP/2 servers
+
+<!-- YAML
+changes:
+  - version: REPLACEME
+    pr-url: https://github.com/nodejs/node/pull/61713
+    description: Documentation-only deprecation.
+-->
+
+Type: Documentation-only
+
+The `Http1IncomingMessage` and `Http1ServerResponse` options of
+[`http2.createServer()`][] and [`http2.createSecureServer()`][] are
+deprecated. Use `http1Options.IncomingMessage` and
+`http1Options.ServerResponse` instead.
+
+```cjs
+// Deprecated
+const server = http2.createSecureServer({
+  allowHTTP1: true,
+  Http1IncomingMessage: MyIncomingMessage,
+  Http1ServerResponse: MyServerResponse,
+});
+```
+
+```cjs
+// Use this instead
+const server = http2.createSecureServer({
+  allowHTTP1: true,
+  http1Options: {
+    IncomingMessage: MyIncomingMessage,
+    ServerResponse: MyServerResponse,
+  },
+});
+```
+
 [DEP0142]: #dep0142-repl_builtinlibs
 [NIST SP 800-38D]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
 [RFC 6066]: https://tools.ietf.org/html/rfc6066#section-3
@@ -4509,6 +4545,8 @@ type of the readable half of the constructed readable-writable pair, use the
 [`http.ServerResponse`]: http.md#class-httpserverresponse
 [`http.get()`]: http.md#httpgetoptions-callback
 [`http.request()`]: http.md#httprequestoptions-callback
+[`http2.createSecureServer()`]: http2.md#http2createsecureserveroptions-onrequesthandler
+[`http2.createServer()`]: http2.md#http2createserveroptions-onrequesthandler
 [`https.get()`]: https.md#httpsgetoptions-callback
 [`https.request()`]: https.md#httpsrequestoptions-callback
 [`message.connection`]: http.md#messageconnection

filename: doc/api/http2.md
patch: @@ -2796,6 +2796,10 @@ Throws `ERR_INVALID_ARG_TYPE` for invalid `settings` argument.
 <!-- YAML
 added: v8.4.0
 changes:
+  - version: REPLACEME
+    pr-url: https://github.com/nodejs/node/pull/61713
+    description: Added `http1Options` option. The `Http1IncomingMessage`
+                 and `Http1ServerResponse` options are now deprecated.
   - version:
       - v23.0.0
       - v22.10.0
@@ -2914,9 +2918,27 @@ changes:
   * `Http1IncomingMessage` {http.IncomingMessage} Specifies the
     `IncomingMessage` class to used for HTTP/1 fallback. Useful for extending
     the original `http.IncomingMessage`. **Default:** `http.IncomingMessage`.
+    **Deprecated.** Use `http1Options.IncomingMessage` instead. See
+    [DEP0202][].
   * `Http1ServerResponse` {http.ServerResponse} Specifies the `ServerResponse`
     class to used for HTTP/1 fallback. Useful for extending the original
     `http.ServerResponse`. **Default:** `http.ServerResponse`.
+    **Deprecated.** Use `http1Options.ServerResponse` instead. See
+    [DEP0202][].
+  * `http1Options` {Object} An options object for configuring the HTTP/1
+    fallback when `allowHTTP1` is `true`. These options are passed to the
+    underlying HTTP/1 server. See [`http.createServer()`][] for available
+    options. Among others, the following are supported:
+    * `IncomingMessage` {http.IncomingMessage} Specifies the
+      `IncomingMessage` class to use for HTTP/1 fallback.
+      **Default:** `http.IncomingMessage`.
+    * `ServerResponse` {http.ServerResponse} Specifies the `ServerResponse`
+      class to use for HTTP/1 fallback.
+      **Default:** `http.ServerResponse`.
+    * `keepAliveTimeout` {number} The number of milliseconds of inactivity
+      a server needs to wait for additional incoming data, after it has
+      finished writing the last response, before a socket will be destroyed.
+      **Default:** `5000`.
   * `Http2ServerRequest` {http2.Http2ServerRequest} Specifies the
     `Http2ServerRequest` class to use.
     Useful for extending the original `Http2ServerRequest`.
@@ -2990,6 +3012,9 @@ server.listen(8000);
 <!-- YAML
 added: v8.4.0
 changes:
+  - version: REPLACEME
+    pr-url: https://github.com/nodejs/node/pull/61713
+    description: Added `http1Options` option.
   - version:
       - v15.10.0
       - v14.16.0
@@ -3108,6 +3133,20 @@ changes:
     and trailing whitespace validation for HTTP/2 header field names and values
     as per [RFC-9113](https://www.rfc-editor.org/rfc/rfc9113.html#section-8.2.1).
     **Default:** `true`.
+  * `http1Options` {Object} An options object for configuring the HTTP/1
+    fallback when `allowHTTP1` is `true`. These options are passed to the
+    underlying HTTP/1 server. See [`http.createServer()`][] for available
+    options. Among others, the following are supported:
+    * `IncomingMessage` {http.IncomingMessage} Specifies the
+      `IncomingMessage` class to use for HTTP/1 fallback.
+      **Default:** `http.IncomingMessage`.
+    * `ServerResponse` {http.ServerResponse} Specifies the `ServerResponse`
+      class to use for HTTP/1 fallback.
+      **Default:** `http.ServerResponse`.
+    * `keepAliveTimeout` {number} The number of milliseconds of inactivity
+      a server needs to wait for additional incoming data, after it has
+      finished writing the last response, before a socket will be destroyed.
+      **Default:** `5000`.
 * `onRequestHandler` {Function} See [Compatibility API][]
 * Returns: {Http2SecureServer}
 
@@ -4937,6 +4976,7 @@ you need to implement any fall-back behavior yourself.
 [ALPN Protocol ID]: https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids
 [ALPN negotiation]: #alpn-negotiation
 [Compatibility API]: #compatibility-api
+[DEP0202]: deprecations.md#dep0202-http1incomingmessage-and-http1serverresponse-options-of-http2-servers
 [HTTP/1]: http.md
 [HTTP/2]: https://tools.ietf.org/html/rfc7540
 [HTTP/2 Headers Object]: #headers-object
@@ -4963,6 +5003,7 @@ you need to implement any fall-back behavior yourself.
 [`Http2Stream`]: #class-http2stream
 [`ServerHttp2Stream`]: #class-serverhttp2stream
 [`TypeError`]: errors.md#class-typeerror
+[`http.createServer()`]: http.md#httpcreateserveroptions-requestlistener
 [`http2.SecureServer`]: #class-http2secureserver
 [`http2.Server`]: #class-http2server
 [`http2.createSecureServer()`]: #http2createsecureserveroptions-onrequesthandler

filename: lib/internal/http2/core.js
patch: @@ -48,8 +48,12 @@ const { Duplex } = require('stream');
 const tls = require('tls');
 const { setImmediate, setTimeout, clearTimeout } = require('timers');
 
-const { kIncomingMessage } = require('_http_common');
-const { kServerResponse, Server: HttpServer, httpServerPreClose, setupConnectionsTracking } = require('_http_server');
+const {
+  Server: HttpServer,
+  httpServerPreClose,
+  setupConnectionsTracking,
+  storeHTTPOptions,
+} = require('_http_server');
 const JSStreamSocket = require('internal/js_stream_socket');
 
 const {
@@ -3257,8 +3261,6 @@ function connectionListener(socket) {
   if (socket.alpnProtocol === false || socket.alpnProtocol === 'http/1.1') {
     // Fallback to HTTP/1.1
     if (options.allowHTTP1 === true) {
-      socket.server[kIncomingMessage] = options.Http1IncomingMessage;
-      socket.server[kServerResponse] = options.Http1ServerResponse;
       return httpConnectionListener.call(this, socket);
     }
     // Let event handler deal with the socket
@@ -3340,9 +3342,18 @@ function initializeOptions(options) {
     options.unknownProtocolTimeout = 10000;
 
 
-  // Used only with allowHTTP1
-  options.Http1IncomingMessage ||= http.IncomingMessage;
-  options.Http1ServerResponse ||= http.ServerResponse;
+  // Initialize http1Options bag for HTTP/1 fallback when allowHTTP1 is true.
+  // This bag is passed to storeHTTPOptions() to configure HTTP/1 server
+  // behavior (timeouts, IncomingMessage/ServerResponse classes, etc.).
+  options.http1Options = { ...options.http1Options };
+
+  // Backward compat: migrate deprecated top-level Http1 options (DEP0201)
+  if (options.Http1IncomingMessage !== undefined) {
+    options.http1Options.IncomingMessage ??= options.Http1IncomingMessage;
+  }
+  if (options.Http1ServerResponse !== undefined) {
+    options.http1Options.ServerResponse ??= options.Http1ServerResponse;
+  }
 
   options.Http2ServerRequest ||= Http2ServerRequest;
   options.Http2ServerResponse ||= Http2ServerResponse;
@@ -3390,9 +3401,7 @@ class Http2SecureServer extends TLSServer {
     this.timeout = 0;
     this.on('newListener', setupCompat);
     if (options.allowHTTP1 === true) {
-      this.headersTimeout = 60_000; // Minimum between 60 seconds or requestTimeout
-      this.requestTimeout = 300_000; // 5 minutes
-      this.connectionsCheckingInterval = 30_000; // 30 seconds
+      storeHTTPOptions.call(this, { ...options, ...options.http1Options });
       this.shouldUpgradeCallback = function() {
         return this.listenerCount('upgrade') > 0;
       };

filename: test/parallel/test-http2-https-fallback-http-server-options.js
patch: @@ -20,6 +20,10 @@ const ca = fixtures.readKey('fake-startcom-root-cert.pem');
 function onRequest(request, response) {
   const { socket: { alpnProtocol } } = request.httpVersion === '2.0' ?
     request.stream.session : request;
+  // Verify that http1Options are applied when allowHTTP1 is true
+  if (request.httpVersion === '1.1') {
+    assert.strictEqual(request.socket.server.keepAliveTimeout, 10000);
+  }
   response.status(200);
   response.end(JSON.stringify({
     alpnProtocol,
@@ -46,8 +50,11 @@ class MyServerResponse extends http.ServerResponse {
     {
       cert,
       key, allowHTTP1: true,
-      Http1IncomingMessage: MyIncomingMessage,
-      Http1ServerResponse: MyServerResponse
+      http1Options: {
+        IncomingMessage: MyIncomingMessage,
+        ServerResponse: MyServerResponse,
+        keepAliveTimeout: 10000,
+      },
     },
     common.mustCall(onRequest, 1)
   );