소스 검색

Add NIOPosix server transport implementation (#1883)

Motivation:

We want to provide some commonly used server transport implementations out of the box.

Modifications:

This PR adds a `ServerTransport` implementation based on `NIOPosix`.

Result:

A `NIOPosix` implementation of a server transport.
Gustavo Cairo 1 년 전
부모
커밋
3272c32b42

+ 1 - 1
Sources/GRPCCore/Streaming/RPCWriter+Closable.swift

@@ -24,7 +24,7 @@ extension RPCWriter {
     ///
     /// - Parameter other: The writer to wrap.
     @inlinable
-    init(wrapping other: some ClosableRPCWriterProtocol<Element>) {
+    public init(wrapping other: some ClosableRPCWriterProtocol<Element>) {
       self.writer = other
     }
 

+ 2 - 2
Sources/GRPCHTTP2Core/Client/Connection/Connection.swift

@@ -391,7 +391,7 @@ extension Connection {
 
 @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
 extension Connection {
-  private enum State {
+  private enum State: Sendable {
     /// The connection is idle or connecting.
     case notConnected
     /// A TCP connection has been established with the remote peer. However, the connection may not
@@ -402,7 +402,7 @@ extension Connection {
     /// The connection has closed. This is a terminal state.
     case closed
 
-    struct Connected {
+    struct Connected: Sendable {
       /// The connection channel.
       var channel: NIOAsyncChannel<ClientConnectionEvent, Void>
       /// Multiplexer for creating HTTP/2 streams.

+ 102 - 0
Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift

@@ -0,0 +1,102 @@
+/*
+ * Copyright 2024, gRPC Authors All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import GRPCCore
+import NIOCore
+import NIOHPACK
+import NIOHTTP2
+
+@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
+extension ChannelPipeline.SynchronousOperations {
+  @_spi(Package) public typealias HTTP2ConnectionChannel = NIOAsyncChannel<HTTP2Frame, HTTP2Frame>
+  @_spi(Package) public typealias HTTP2StreamMultiplexer = NIOHTTP2Handler.AsyncStreamMultiplexer<
+    (NIOAsyncChannel<RPCRequestPart, RPCResponsePart>, EventLoopFuture<MethodDescriptor>)
+  >
+
+  @_spi(Package)
+  public func configureGRPCServerPipeline(
+    channel: any Channel,
+    compressionConfig: HTTP2ServerTransport.Config.Compression,
+    keepaliveConfig: HTTP2ServerTransport.Config.Keepalive,
+    connectionConfig: HTTP2ServerTransport.Config.Connection,
+    http2Config: HTTP2ServerTransport.Config.HTTP2,
+    rpcConfig: HTTP2ServerTransport.Config.RPC,
+    useTLS: Bool
+  ) throws -> (HTTP2ConnectionChannel, HTTP2StreamMultiplexer) {
+    let serverConnectionHandler = ServerConnectionManagementHandler(
+      eventLoop: self.eventLoop,
+      maxIdleTime: connectionConfig.maxIdleTime.map { TimeAmount($0) },
+      maxAge: connectionConfig.maxAge.map { TimeAmount($0) },
+      maxGraceTime: connectionConfig.maxGraceTime.map { TimeAmount($0) },
+      keepaliveTime: TimeAmount(keepaliveConfig.time),
+      keepaliveTimeout: TimeAmount(keepaliveConfig.timeout),
+      allowKeepaliveWithoutCalls: keepaliveConfig.permitWithoutCalls,
+      minPingIntervalWithoutCalls: TimeAmount(keepaliveConfig.minPingIntervalWithoutCalls)
+    )
+    let flushNotificationHandler = GRPCServerFlushNotificationHandler(
+      serverConnectionManagementHandler: serverConnectionHandler
+    )
+    try self.addHandler(flushNotificationHandler)
+
+    var http2HandlerConnectionConfiguration = NIOHTTP2Handler.ConnectionConfiguration()
+    var http2HandlerHTTP2Settings = HTTP2Settings([
+      HTTP2Setting(parameter: .initialWindowSize, value: http2Config.targetWindowSize),
+      HTTP2Setting(parameter: .maxFrameSize, value: http2Config.maxFrameSize),
+      HTTP2Setting(parameter: .maxHeaderListSize, value: HPACKDecoder.defaultMaxHeaderListSize),
+    ])
+    if let maxConcurrentStreams = http2Config.maxConcurrentStreams {
+      http2HandlerHTTP2Settings.append(
+        HTTP2Setting(parameter: .maxConcurrentStreams, value: maxConcurrentStreams)
+      )
+    }
+    http2HandlerConnectionConfiguration.initialSettings = http2HandlerHTTP2Settings
+
+    var http2HandlerStreamConfiguration = NIOHTTP2Handler.StreamConfiguration()
+    http2HandlerStreamConfiguration.targetWindowSize = http2Config.targetWindowSize
+
+    let streamMultiplexer = try self.configureAsyncHTTP2Pipeline(
+      mode: .server,
+      configuration: NIOHTTP2Handler.Configuration(
+        connection: http2HandlerConnectionConfiguration,
+        stream: http2HandlerStreamConfiguration
+      )
+    ) { streamChannel in
+      return streamChannel.eventLoop.makeCompletedFuture {
+        let methodDescriptorPromise = streamChannel.eventLoop.makePromise(of: MethodDescriptor.self)
+        let streamHandler = GRPCServerStreamHandler(
+          scheme: useTLS ? .https : .http,
+          acceptedEncodings: compressionConfig.enabledAlgorithms,
+          maximumPayloadSize: rpcConfig.maxRequestPayloadSize,
+          methodDescriptorPromise: methodDescriptorPromise
+        )
+        try streamChannel.pipeline.syncOperations.addHandler(streamHandler)
+
+        let asyncStreamChannel = try NIOAsyncChannel<RPCRequestPart, RPCResponsePart>(
+          wrappingChannelSynchronously: streamChannel
+        )
+        return (asyncStreamChannel, methodDescriptorPromise.futureResult)
+      }
+    }
+
+    try self.addHandler(serverConnectionHandler)
+
+    let connectionChannel = try NIOAsyncChannel<HTTP2Frame, HTTP2Frame>(
+      wrappingChannelSynchronously: channel
+    )
+
+    return (connectionChannel, streamMultiplexer)
+  }
+}

+ 52 - 0
Sources/GRPCHTTP2Core/Server/Connection/ServerConnection.swift

@@ -0,0 +1,52 @@
+/*
+ * Copyright 2024, gRPC Authors All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import GRPCCore
+import NIOCore
+
+@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
+public enum ServerConnection {
+  public enum Stream {
+    @_spi(Package)
+    public struct Outbound: ClosableRPCWriterProtocol {
+      public typealias Element = RPCResponsePart
+
+      private let responseWriter: NIOAsyncChannelOutboundWriter<RPCResponsePart>
+      private let http2Stream: NIOAsyncChannel<RPCRequestPart, RPCResponsePart>
+
+      public init(
+        responseWriter: NIOAsyncChannelOutboundWriter<RPCResponsePart>,
+        http2Stream: NIOAsyncChannel<RPCRequestPart, RPCResponsePart>
+      ) {
+        self.responseWriter = responseWriter
+        self.http2Stream = http2Stream
+      }
+
+      public func write(contentsOf elements: some Sequence<Self.Element>) async throws {
+        try await self.responseWriter.write(contentsOf: elements)
+      }
+
+      public func finish() {
+        self.responseWriter.finish()
+      }
+
+      public func finish(throwing error: any Error) {
+        // Fire the error inbound; this fails the inbound writer.
+        self.http2Stream.channel.pipeline.fireErrorCaught(error)
+      }
+    }
+  }
+}

+ 163 - 0
Sources/GRPCHTTP2Core/Server/HTTP2ServerTransport.swift

@@ -0,0 +1,163 @@
+/*
+ * Copyright 2024, gRPC Authors All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import GRPCCore
+import NIOHTTP2
+
+/// A namespace for the HTTP/2 server transport.
+public enum HTTP2ServerTransport {}
+
+extension HTTP2ServerTransport {
+  /// A namespace for HTTP/2 server transport configuration.
+  public enum Config {}
+}
+
+extension HTTP2ServerTransport.Config {
+  public struct Compression: Sendable {
+    /// Compression algorithms enabled for inbound messages.
+    ///
+    /// - Note: ``CompressionAlgorithm/none`` is always supported, even if it isn't set here.
+    public var enabledAlgorithms: CompressionAlgorithmSet
+
+    /// Creates a new compression configuration.
+    ///
+    /// - SeeAlso: ``defaults``.
+    public init(enabledAlgorithms: CompressionAlgorithmSet) {
+      self.enabledAlgorithms = enabledAlgorithms
+    }
+
+    /// Default values, compression is disabled.
+    public static var defaults: Self {
+      Self(enabledAlgorithms: .none)
+    }
+  }
+
+  @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
+  public struct Keepalive: Sendable {
+    /// The amount of time to wait after reading data before sending a keepalive ping.
+    public var time: Duration
+
+    /// The amount of time the server has to respond to a keepalive ping before the connection is closed.
+    public var timeout: Duration
+
+    /// Whether the server allows the client to send keepalive pings when there are no calls in progress.
+    public var permitWithoutCalls: Bool
+
+    /// The minimum allowed interval the client is allowed to send keep-alive pings.
+    /// Pings more frequent than this interval count as 'strikes' and the connection is closed if there are
+    /// too many strikes.
+    public var minPingIntervalWithoutCalls: Duration
+
+    /// Creates a new keepalive configuration.
+    public init(
+      time: Duration,
+      timeout: Duration,
+      permitWithoutCalls: Bool,
+      minPingIntervalWithoutCalls: Duration
+    ) {
+      self.time = time
+      self.timeout = timeout
+      self.permitWithoutCalls = permitWithoutCalls
+      self.minPingIntervalWithoutCalls = minPingIntervalWithoutCalls
+    }
+
+    /// Default values. The time after reading data a ping should be sent defaults to 2 hours, the timeout for
+    /// keepalive pings defaults to 20 seconds, pings are not permitted when no calls are in progress, and
+    /// the minimum allowed interval for clients to send pings defaults to 5 minutes.
+    public static var defaults: Self {
+      Self(
+        time: .seconds(2 * 60 * 60),  // 2 hours
+        timeout: .seconds(20),
+        permitWithoutCalls: false,
+        minPingIntervalWithoutCalls: .seconds(5 * 60)  // 5 minutes
+      )
+    }
+  }
+
+  @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
+  public struct Connection: Sendable {
+    /// The maximum amount of time a connection may exist before being gracefully closed.
+    public var maxAge: Duration?
+
+    /// The maximum amount of time that the connection has to close gracefully.
+    public var maxGraceTime: Duration?
+
+    /// The maximum amount of time a connection may be idle before it's closed.
+    public var maxIdleTime: Duration?
+
+    public init(
+      maxAge: Duration?,
+      maxGraceTime: Duration?,
+      maxIdleTime: Duration?
+    ) {
+      self.maxAge = maxAge
+      self.maxGraceTime = maxGraceTime
+      self.maxIdleTime = maxIdleTime
+    }
+
+    /// Default values. All the max connection age, max grace time, and max idle time default to infinite.
+    public static var defaults: Self {
+      Self(maxAge: nil, maxGraceTime: nil, maxIdleTime: nil)
+    }
+  }
+
+  public struct HTTP2: Sendable {
+    /// The maximum frame size to be used in an HTTP/2 connection.
+    public var maxFrameSize: Int
+
+    /// The target window size for this connection.
+    ///
+    /// - Note: This will also be set as the initial window size for the connection.
+    public var targetWindowSize: Int
+
+    /// The number of concurrent streams on the HTTP/2 connection.
+    public var maxConcurrentStreams: Int?
+
+    public init(
+      maxFrameSize: Int,
+      targetWindowSize: Int,
+      maxConcurrentStreams: Int?
+    ) {
+      self.maxFrameSize = maxFrameSize
+      self.targetWindowSize = targetWindowSize
+      self.maxConcurrentStreams = maxConcurrentStreams
+    }
+
+    /// Default values. The max frame size defaults to 2^14, the target window size defaults to 2^16-1, and
+    /// the max concurrent streams default to infinite.
+    public static var defaults: Self {
+      Self(
+        maxFrameSize: 1 << 14,
+        targetWindowSize: (1 << 16) - 1,
+        maxConcurrentStreams: nil
+      )
+    }
+  }
+
+  public struct RPC: Sendable {
+    /// The maximum request payload size.
+    public var maxRequestPayloadSize: Int
+
+    public init(maxRequestPayloadSize: Int) {
+      self.maxRequestPayloadSize = maxRequestPayloadSize
+    }
+
+    /// Default values. Maximum request payload size defaults to 4MiB.
+    public static var defaults: Self {
+      Self(maxRequestPayloadSize: 4 * 1024 * 1024)
+    }
+  }
+}

+ 201 - 2
Sources/GRPCHTTP2TransportNIOPosix/GRPCHTTP2TransportNIOPosix.swift

@@ -15,7 +15,206 @@
  */
 
 import GRPCCore
+@_spi(Package) import GRPCHTTP2Core
+import NIOCore
+import NIOExtras
+import NIOPosix
 
-@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
-public struct GRPCHTTP2TransportNIOPosix {
+extension HTTP2ServerTransport {
+  @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
+  public struct Posix: ServerTransport {
+    private let address: GRPCHTTP2Core.SocketAddress
+    private let config: Config
+    private let eventLoopGroup: MultiThreadedEventLoopGroup
+    private let serverQuiescingHelper: ServerQuiescingHelper
+
+    public init(
+      address: GRPCHTTP2Core.SocketAddress,
+      config: Config,
+      eventLoopGroup: MultiThreadedEventLoopGroup = .singletonMultiThreadedEventLoopGroup
+    ) {
+      self.address = address
+      self.config = config
+      self.eventLoopGroup = eventLoopGroup
+      self.serverQuiescingHelper = ServerQuiescingHelper(group: self.eventLoopGroup)
+    }
+
+    public func listen(
+      _ streamHandler: @escaping (RPCStream<Inbound, Outbound>) async -> Void
+    ) async throws {
+      let serverChannel = try await ServerBootstrap(group: self.eventLoopGroup)
+        .serverChannelInitializer { channel in
+          let quiescingHandler = self.serverQuiescingHelper.makeServerChannelHandler(
+            channel: channel
+          )
+          return channel.pipeline.addHandler(quiescingHandler)
+        }
+        .bind(to: self.address) { channel in
+          channel.eventLoop.makeCompletedFuture {
+            return try channel.pipeline.syncOperations.configureGRPCServerPipeline(
+              channel: channel,
+              compressionConfig: self.config.compression,
+              keepaliveConfig: self.config.keepalive,
+              connectionConfig: self.config.connection,
+              http2Config: self.config.http2,
+              rpcConfig: self.config.rpc,
+              useTLS: false
+            )
+          }
+        }
+
+      try await serverChannel.executeThenClose { inbound in
+        try await withThrowingDiscardingTaskGroup { serverTaskGroup in
+          for try await (connectionChannel, streamMultiplexer) in inbound {
+            serverTaskGroup.addTask {
+              try await connectionChannel
+                .executeThenClose { connectionInbound, connectionOutbound in
+                  await withDiscardingTaskGroup { connectionTaskGroup in
+                    connectionTaskGroup.addTask {
+                      do {
+                        for try await _ in connectionInbound {}
+                      } catch {
+                        // We don't want to close the channel if one connection throws.
+                        return
+                      }
+                    }
+
+                    connectionTaskGroup.addTask {
+                      await withDiscardingTaskGroup { streamTaskGroup in
+                        do {
+                          for try await (http2Stream, methodDescriptor) in streamMultiplexer.inbound
+                          {
+                            streamTaskGroup.addTask {
+                              // It's okay to ignore these errors:
+                              // - If we get an error because the http2Stream failed to close, then there's nothing we can do
+                              // - If we get an error because the inner closure threw, then the only possible scenario in which
+                              // that could happen is if methodDescriptor.get() throws - in which case, it means we never got
+                              // the RPC metadata, which means we can't do anything either and it's okay to just kill the stream.
+                              try? await http2Stream.executeThenClose { inbound, outbound in
+                                guard let descriptor = try? await methodDescriptor.get() else {
+                                  return
+                                }
+                                let rpcStream = RPCStream(
+                                  descriptor: descriptor,
+                                  inbound: RPCAsyncSequence(wrapping: inbound),
+                                  outbound: RPCWriter.Closable(
+                                    wrapping: ServerConnection.Stream.Outbound(
+                                      responseWriter: outbound,
+                                      http2Stream: http2Stream
+                                    )
+                                  )
+                                )
+                                await streamHandler(rpcStream)
+                              }
+                            }
+                          }
+                        } catch {
+                          // We don't want to close the whole connection if one stream throws.
+                          return
+                        }
+                      }
+                    }
+                  }
+                }
+            }
+          }
+        }
+      }
+    }
+
+    public func stopListening() {
+      self.serverQuiescingHelper.initiateShutdown(promise: nil)
+    }
+  }
+
+}
+
+@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
+extension HTTP2ServerTransport.Posix {
+  /// Configuration for the ``GRPCHTTP2TransportNIOPosix/GRPCHTTP2Core/HTTP2ServerTransport/Posix``.
+  public struct Config: Sendable {
+    /// Compression configuration.
+    public var compression: HTTP2ServerTransport.Config.Compression
+    /// Keepalive configuration.
+    public var keepalive: HTTP2ServerTransport.Config.Keepalive
+    /// Connection configuration.
+    public var connection: HTTP2ServerTransport.Config.Connection
+    /// HTTP2 configuration.
+    public var http2: HTTP2ServerTransport.Config.HTTP2
+    /// RPC configuration.
+    public var rpc: HTTP2ServerTransport.Config.RPC
+
+    /// Construct a new `Config`.
+    /// - Parameters:
+    ///   - compression: Compression configuration.
+    ///   - keepalive: Keepalive configuration.
+    ///   - connection: Connection configuration.
+    ///   - http2: HTTP2 configuration.
+    ///   - rpc: RPC configuration.
+    public init(
+      compression: HTTP2ServerTransport.Config.Compression,
+      keepalive: HTTP2ServerTransport.Config.Keepalive,
+      connection: HTTP2ServerTransport.Config.Connection,
+      http2: HTTP2ServerTransport.Config.HTTP2,
+      rpc: HTTP2ServerTransport.Config.RPC
+    ) {
+      self.compression = compression
+      self.keepalive = keepalive
+      self.connection = connection
+      self.http2 = http2
+      self.rpc = rpc
+    }
+
+    /// Default values for the different configurations.
+    public static var defaults: Self {
+      Self(
+        compression: .defaults,
+        keepalive: .defaults,
+        connection: .defaults,
+        http2: .defaults,
+        rpc: .defaults
+      )
+    }
+  }
+}
+
+extension NIOCore.SocketAddress {
+  fileprivate init(_ socketAddress: GRPCHTTP2Core.SocketAddress) throws {
+    if let ipv4 = socketAddress.ipv4 {
+      self = try Self(ipAddress: ipv4.host, port: ipv4.port)
+    } else if let ipv6 = socketAddress.ipv6 {
+      self = try Self(ipAddress: ipv6.host, port: ipv6.port)
+    } else if let unixDomainSocket = socketAddress.unixDomainSocket {
+      self = try Self(unixDomainSocketPath: unixDomainSocket.path)
+    } else {
+      throw RPCError(
+        code: .internalError,
+        message:
+          "Unsupported mapping to NIOCore/SocketAddress for GRPCHTTP2Core/SocketAddress: \(socketAddress)."
+      )
+    }
+  }
+}
+
+extension ServerBootstrap {
+  @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
+  fileprivate func bind<Output: Sendable>(
+    to address: GRPCHTTP2Core.SocketAddress,
+    childChannelInitializer: @escaping @Sendable (Channel) -> EventLoopFuture<Output>
+  ) async throws -> NIOAsyncChannel<Output, Never> {
+    if let virtualSocket = address.virtualSocket {
+      return try await self.bind(
+        to: VsockAddress(
+          cid: VsockAddress.ContextID(rawValue: virtualSocket.contextID.rawValue),
+          port: VsockAddress.Port(rawValue: virtualSocket.port.rawValue)
+        ),
+        childChannelInitializer: childChannelInitializer
+      )
+    } else {
+      return try await self.bind(
+        to: NIOCore.SocketAddress(address),
+        childChannelInitializer: childChannelInitializer
+      )
+    }
+  }
 }

+ 0 - 0
Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHasness+ServerBehavior.swift → Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+ServerBehavior.swift


+ 53 - 0
Tests/GRPCHTTP2CoreTests/Server/HTTP2ServerTransportConfigTests.swift

@@ -0,0 +1,53 @@
+/*
+ * Copyright 2024, gRPC Authors All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import GRPCHTTP2Core
+import XCTest
+
+@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
+final class HTTP2ServerTransportConfigTests: XCTestCase {
+  func testCompressionDefaults() {
+    let config = HTTP2ServerTransport.Config.Compression.defaults
+    XCTAssertEqual(config.enabledAlgorithms, .none)
+  }
+
+  func testKeepaliveDefaults() {
+    let config = HTTP2ServerTransport.Config.Keepalive.defaults
+    XCTAssertEqual(config.time, .seconds(7200))
+    XCTAssertEqual(config.timeout, .seconds(20))
+    XCTAssertEqual(config.permitWithoutCalls, false)
+    XCTAssertEqual(config.minPingIntervalWithoutCalls, .seconds(300))
+  }
+
+  func testConnectionDefaults() {
+    let config = HTTP2ServerTransport.Config.Connection.defaults
+    XCTAssertNil(config.maxAge)
+    XCTAssertNil(config.maxGraceTime)
+    XCTAssertNil(config.maxIdleTime)
+  }
+
+  func testHTTP2Defaults() {
+    let config = HTTP2ServerTransport.Config.HTTP2.defaults
+    XCTAssertEqual(config.maxFrameSize, 16_384)
+    XCTAssertEqual(config.targetWindowSize, 65_535)
+    XCTAssertNil(config.maxConcurrentStreams)
+  }
+
+  func testRPCDefaults() {
+    let config = HTTP2ServerTransport.Config.RPC.defaults
+    XCTAssertEqual(config.maxRequestPayloadSize, 4_194_304)
+  }
+}