From a663a27a76303cabd1cdb10293417868f19c95f4 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Thu, 1 Feb 2024 17:03:53 +0000 Subject: [PATCH 01/51] Add client-side logic of GRPCStreamStateMachine --- .../GRPCStreamStateMachine.swift | 222 ++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift new file mode 100644 index 000000000..63f5a14f0 --- /dev/null +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -0,0 +1,222 @@ +/* + * 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 + +// TODO: this is all done from client's perspective for now - work on server-side later. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +struct GRPCStreamStateMachine { + struct Configuration { + let maximumPayloadSize: Int + let compressor: Zlib.Compressor? + let decompressor: Zlib.Decompressor? + } + + private var state: State + + enum State { + case clientIdleServerIdle(Configuration) + case clientOpenServerIdle(ClientOpenState) + case clientOpenServerOpen(ClientOpenState) + case clientOpenServerClosed(ClientOpenState) + case clientClosedServerIdle + case clientClosedServerOpen + case clientClosedServerClosed + } + + struct ClientOpenState { + var framer: GRPCMessageFramer + let deframer: NIOSingleStepByteToMessageProcessor + var compressor: Zlib.Compressor? + + init( + maximumPayloadSize: Int, + compressor: Zlib.Compressor?, + decompressor: Zlib.Decompressor? + ) { + self.framer = GRPCMessageFramer() + let messageDeframer = GRPCMessageDeframer( + maximumPayloadSize: maximumPayloadSize, + decompressor: decompressor + ) + self.deframer = NIOSingleStepByteToMessageProcessor(messageDeframer) + self.compressor = compressor + } + } + + init(configuration: Configuration) { + self.state = .clientIdleServerIdle(configuration) + } + + mutating func send(metadata: Metadata) throws { + // Client only sends metadata when opening. + try self.clientSentMetadata() + } + + mutating func send(message: [UInt8]) { + // Client sends message. + switch self.state { + case .clientIdleServerIdle(let configuration): + preconditionFailure("Client not yet open") + case .clientOpenServerIdle(var clientOpenState): + clientOpenState.framer.append(message) + self.state = .clientOpenServerClosed(clientOpenState) + case .clientOpenServerOpen(var clientOpenState): + clientOpenState.framer.append(message) + self.state = .clientOpenServerClosed(clientOpenState) + case .clientOpenServerClosed: + // Nothing to do: no point in sending a message if the server's closed. + () + case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: + preconditionFailure("Client is closed, cannot send a message") + } + } + + mutating func send(status: String, trailingMetadata: Metadata) throws { + // Only server does this. + } + + /// Returns the client's next request to the server. + /// - Returns: The request to be made to the server. + mutating func nextRequest() throws -> ByteBuffer? { + switch self.state { + case .clientIdleServerIdle(let configuration): + preconditionFailure("Client is not open yet.") + case .clientOpenServerIdle(var clientOpenState): + let request = try clientOpenState.framer.next(compressor: clientOpenState.compressor) + self.state = .clientOpenServerIdle(clientOpenState) + return request + case .clientOpenServerOpen(var clientOpenState): + let request = try clientOpenState.framer.next(compressor: clientOpenState.compressor) + self.state = .clientOpenServerOpen(clientOpenState) + return request + case .clientOpenServerClosed: + // Nothing to do: no point in sending request if server is closed. + return nil + case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: + preconditionFailure("Can't send request if client is closed") + } + } + + mutating func receive(metadata: Metadata, endStream: Bool) { + // This is metadata received by the client from the server. + // It can be initial, which confirms that the server is now open; + // or an END_STREAM trailer, meaning the response is over. + if endStream { + self.clientReceivedEndHeader() + } else { + self.clientReceivedMetadata() + } + } + + mutating func receive(message: ByteBuffer, messageProcessor: ([UInt8]) throws -> Void) throws { + // This is a message received by the client, from the server. + switch self.state { + case .clientIdleServerIdle(let configuration): + preconditionFailure("Cannot have received anything from server if client is not yet open.") + case .clientOpenServerIdle(let clientOpenState): + preconditionFailure("Server cannot have sent a message before sending the initial metadata.") + case .clientOpenServerOpen(let clientOpenState): + try clientOpenState.deframer.process(buffer: message, messageProcessor) + case .clientOpenServerClosed: + preconditionFailure("Cannot have received anything from a closed server.") + case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: + preconditionFailure("Shouldn't receive anything if client's closed.") + } + } + + // - MARK: Client-opening transitions + + mutating func clientSentMetadata() throws { + // Client sends metadata only when opening the stream. + // They send grpc-timeout and method name along with it. + // TODO: should these things be validated in the handler or here? + switch self.state { + case .clientIdleServerIdle(let configuration): + let clientOpenState = ClientOpenState( + maximumPayloadSize: configuration.maximumPayloadSize, + compressor: configuration.compressor, + decompressor: configuration.decompressor + ) + self.state = .clientOpenServerIdle(clientOpenState) + case .clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed: + // Client is already open: we shouldn't be sending metadata. + preconditionFailure("Invalid state: client is already open") + case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: + // Client is closed: we shouldn't be sending metadata again. + preconditionFailure("Invalid state: client is closed") + } + } + + // - MARK: Client-closing transitions + + mutating func clientSentEnd() throws { + switch self.state { + case .clientIdleServerIdle: + self.state = .clientClosedServerIdle + case .clientOpenServerIdle: + self.state = .clientClosedServerIdle + case .clientOpenServerOpen: + self.state = .clientClosedServerOpen + case .clientOpenServerClosed: + self.state = .clientClosedServerClosed + case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: + // TODO: think what to do + preconditionFailure("Client cannot have sent anything if it's closed.") + } + } + + // - MARK: Server-opening transitions + + mutating func clientReceivedMetadata() { + switch self.state { + case .clientIdleServerIdle: + preconditionFailure("Server cannot have sent metadata if the client is idle.") + case .clientOpenServerIdle(let clientOpenState): + self.state = .clientOpenServerOpen(clientOpenState) + case .clientOpenServerOpen: + // Do nothing + () + case .clientOpenServerClosed, .clientClosedServerClosed: + preconditionFailure("Server is closed, nothing could have been sent.") + case .clientClosedServerIdle, .clientClosedServerOpen: + preconditionFailure("Client is closed, cannot have received anything.") + () + } + } + + // - MARK: Server-closing transitions + + mutating func clientReceivedEndHeader() { + switch self.state { + case .clientIdleServerIdle: + preconditionFailure("Client can't have received a stream end trailer if both client and server are idle.") + case .clientOpenServerIdle: + preconditionFailure("Server cannot have sent an end stream header if it is still idle.") + case .clientOpenServerOpen(let clientOpenState): + self.state = .clientOpenServerClosed(clientOpenState) + case .clientOpenServerClosed: + preconditionFailure("Server is already closed, can't have received the end stream trailer twice.") + case .clientClosedServerIdle: + preconditionFailure("Server cannot have sent end stream trailer if it is idle.") + case .clientClosedServerOpen: + self.state = .clientClosedServerClosed + case .clientClosedServerClosed: + preconditionFailure("Server cannot have sent end stream trailer if it is already closed.") + } + } +} From 31f91f0f006f7518e7693259da8e74a4c441e6ab Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Tue, 6 Feb 2024 13:37:38 +0000 Subject: [PATCH 02/51] Add server-side logic to GRPCStreamStateMachine --- Sources/GRPCCore/Internal/Metadata+GRPC.swift | 2 +- .../GRPCStreamStateMachine.swift | 480 ++++++++++++------ 2 files changed, 324 insertions(+), 158 deletions(-) diff --git a/Sources/GRPCCore/Internal/Metadata+GRPC.swift b/Sources/GRPCCore/Internal/Metadata+GRPC.swift index 9bff423e3..9b5d88c8e 100644 --- a/Sources/GRPCCore/Internal/Metadata+GRPC.swift +++ b/Sources/GRPCCore/Internal/Metadata+GRPC.swift @@ -38,7 +38,7 @@ extension Metadata { } @inlinable - var timeout: Duration? { + public var timeout: Duration? { get { self.firstString(forKey: .timeout).flatMap { Timeout(decoding: $0)?.duration } } diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 63f5a14f0..4f28dcd1c 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -17,27 +17,40 @@ import GRPCCore import NIOCore -// TODO: this is all done from client's perspective for now - work on server-side later. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -struct GRPCStreamStateMachine { - struct Configuration { - let maximumPayloadSize: Int - let compressor: Zlib.Compressor? - let decompressor: Zlib.Decompressor? - } +fileprivate protocol GRPCStreamStateMachineProtocol { + var state: GRPCStreamStateMachineState { get set } - private var state: State + mutating func send(metadata: Metadata) + mutating func send(message: [UInt8], endStream: Bool) + mutating func send(status: String, trailingMetadata: Metadata) - enum State { - case clientIdleServerIdle(Configuration) - case clientOpenServerIdle(ClientOpenState) - case clientOpenServerOpen(ClientOpenState) - case clientOpenServerClosed(ClientOpenState) - case clientClosedServerIdle - case clientClosedServerOpen - case clientClosedServerClosed + mutating func receive(metadata: Metadata, endStream: Bool) + mutating func receive(message: ByteBuffer, endStream: Bool) + + mutating func nextRequest() throws -> ByteBuffer? +} + +struct GRPCStreamStateMachineConfiguration { + enum Party { + case client + case server } + let party: Party + let maximumPayloadSize: Int + let compressor: Zlib.Compressor? + let decompressor: Zlib.Decompressor? +} + +fileprivate enum GRPCStreamStateMachineState { + case clientIdleServerIdle(GRPCStreamStateMachineConfiguration) + case clientOpenServerIdle(ClientOpenState) + case clientOpenServerOpen(ClientOpenState) + case clientOpenServerClosed(ClientOpenState) + case clientClosedServerIdle(ClientOpenState) + case clientClosedServerOpen(ClientOpenState) + case clientClosedServerClosed + struct ClientOpenState { var framer: GRPCMessageFramer let deframer: NIOSingleStepByteToMessageProcessor @@ -57,166 +70,319 @@ struct GRPCStreamStateMachine { self.compressor = compressor } } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +struct GRPCStreamStateMachine { - init(configuration: Configuration) { - self.state = .clientIdleServerIdle(configuration) - } + private var _stateMachine: GRPCStreamStateMachineProtocol - mutating func send(metadata: Metadata) throws { - // Client only sends metadata when opening. - try self.clientSentMetadata() + init(configuration: GRPCStreamStateMachineConfiguration) { + switch configuration.party { + case .client: + self._stateMachine = Client(configuration: configuration) + case .server: + self._stateMachine = Server(configuration: configuration) + } } - mutating func send(message: [UInt8]) { - // Client sends message. - switch self.state { - case .clientIdleServerIdle(let configuration): - preconditionFailure("Client not yet open") - case .clientOpenServerIdle(var clientOpenState): - clientOpenState.framer.append(message) - self.state = .clientOpenServerClosed(clientOpenState) - case .clientOpenServerOpen(var clientOpenState): - clientOpenState.framer.append(message) - self.state = .clientOpenServerClosed(clientOpenState) - case .clientOpenServerClosed: - // Nothing to do: no point in sending a message if the server's closed. - () - case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - preconditionFailure("Client is closed, cannot send a message") - } + mutating func send(metadata: Metadata) { + self._stateMachine.send(metadata: metadata) } - mutating func send(status: String, trailingMetadata: Metadata) throws { - // Only server does this. + mutating func send(message: [UInt8], endStream: Bool) { + self._stateMachine.send(message: message, endStream: endStream) } - /// Returns the client's next request to the server. - /// - Returns: The request to be made to the server. - mutating func nextRequest() throws -> ByteBuffer? { - switch self.state { - case .clientIdleServerIdle(let configuration): - preconditionFailure("Client is not open yet.") - case .clientOpenServerIdle(var clientOpenState): - let request = try clientOpenState.framer.next(compressor: clientOpenState.compressor) - self.state = .clientOpenServerIdle(clientOpenState) - return request - case .clientOpenServerOpen(var clientOpenState): - let request = try clientOpenState.framer.next(compressor: clientOpenState.compressor) - self.state = .clientOpenServerOpen(clientOpenState) - return request - case .clientOpenServerClosed: - // Nothing to do: no point in sending request if server is closed. - return nil - case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - preconditionFailure("Can't send request if client is closed") - } + mutating func send(status: String, trailingMetadata: Metadata) { + self._stateMachine.send(status: status, trailingMetadata: trailingMetadata) } mutating func receive(metadata: Metadata, endStream: Bool) { - // This is metadata received by the client from the server. - // It can be initial, which confirms that the server is now open; - // or an END_STREAM trailer, meaning the response is over. - if endStream { - self.clientReceivedEndHeader() - } else { - self.clientReceivedMetadata() - } + self._stateMachine.receive(metadata: metadata, endStream: endStream) } - mutating func receive(message: ByteBuffer, messageProcessor: ([UInt8]) throws -> Void) throws { - // This is a message received by the client, from the server. - switch self.state { - case .clientIdleServerIdle(let configuration): - preconditionFailure("Cannot have received anything from server if client is not yet open.") - case .clientOpenServerIdle(let clientOpenState): - preconditionFailure("Server cannot have sent a message before sending the initial metadata.") - case .clientOpenServerOpen(let clientOpenState): - try clientOpenState.deframer.process(buffer: message, messageProcessor) - case .clientOpenServerClosed: - preconditionFailure("Cannot have received anything from a closed server.") - case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - preconditionFailure("Shouldn't receive anything if client's closed.") - } + mutating func receive(message: ByteBuffer, endStream: Bool) { + self._stateMachine.receive(message: message, endStream: endStream) } - // - MARK: Client-opening transitions + mutating func nextRequest() throws -> ByteBuffer? { + try self._stateMachine.nextRequest() + } +} - mutating func clientSentMetadata() throws { - // Client sends metadata only when opening the stream. - // They send grpc-timeout and method name along with it. - // TODO: should these things be validated in the handler or here? - switch self.state { - case .clientIdleServerIdle(let configuration): - let clientOpenState = ClientOpenState( - maximumPayloadSize: configuration.maximumPayloadSize, - compressor: configuration.compressor, - decompressor: configuration.decompressor - ) - self.state = .clientOpenServerIdle(clientOpenState) - case .clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed: - // Client is already open: we shouldn't be sending metadata. - preconditionFailure("Invalid state: client is already open") - case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - // Client is closed: we shouldn't be sending metadata again. - preconditionFailure("Invalid state: client is closed") +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension GRPCStreamStateMachine { + struct Client: GRPCStreamStateMachineProtocol { + fileprivate var state: GRPCStreamStateMachineState + + init(configuration: GRPCStreamStateMachineConfiguration) { + self.state = .clientIdleServerIdle(configuration) } - } - - // - MARK: Client-closing transitions - - mutating func clientSentEnd() throws { - switch self.state { - case .clientIdleServerIdle: - self.state = .clientClosedServerIdle - case .clientOpenServerIdle: - self.state = .clientClosedServerIdle - case .clientOpenServerOpen: - self.state = .clientClosedServerOpen - case .clientOpenServerClosed: - self.state = .clientClosedServerClosed - case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - // TODO: think what to do - preconditionFailure("Client cannot have sent anything if it's closed.") + + mutating func send(metadata: Metadata) { + // Client sends metadata only when opening the stream. + // They send grpc-timeout and method name along with it. + // TODO: should these things be validated in the handler or here? + switch self.state { + case .clientIdleServerIdle(let configuration): + let clientOpenState = GRPCStreamStateMachineState.ClientOpenState( + maximumPayloadSize: configuration.maximumPayloadSize, + compressor: configuration.compressor, + decompressor: configuration.decompressor + ) + self.state = .clientOpenServerIdle(clientOpenState) + case .clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed: + // Client is already open: we shouldn't be sending metadata. + preconditionFailure("Invalid state: client is already open") + case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: + // Client is closed: we shouldn't be sending metadata again. + preconditionFailure("Invalid state: client is closed") + } } - } - - // - MARK: Server-opening transitions - - mutating func clientReceivedMetadata() { - switch self.state { - case .clientIdleServerIdle: - preconditionFailure("Server cannot have sent metadata if the client is idle.") - case .clientOpenServerIdle(let clientOpenState): - self.state = .clientOpenServerOpen(clientOpenState) - case .clientOpenServerOpen: - // Do nothing - () - case .clientOpenServerClosed, .clientClosedServerClosed: - preconditionFailure("Server is closed, nothing could have been sent.") - case .clientClosedServerIdle, .clientClosedServerOpen: - preconditionFailure("Client is closed, cannot have received anything.") - () + + mutating func send(message: [UInt8], endStream: Bool) { + // Client sends message. + switch self.state { + case .clientIdleServerIdle(let configuration): + preconditionFailure("Client not yet open") + case .clientOpenServerIdle(var clientOpenState): + clientOpenState.framer.append(message) + if endStream { + self.state = .clientClosedServerIdle(clientOpenState) + } else { + self.state = .clientOpenServerIdle(clientOpenState) + } + case .clientOpenServerOpen(var clientOpenState): + clientOpenState.framer.append(message) + if endStream { + self.state = .clientClosedServerOpen(clientOpenState) + } else { + self.state = .clientOpenServerOpen(clientOpenState) + } + case .clientOpenServerClosed: + // The server has closed, so it makes no sense to send the rest of the request. + // Do nothing. + () + case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: + preconditionFailure("Client is closed, cannot send a message") + } + } + + mutating func send(status: String, trailingMetadata: Metadata) { + // Nothing to do: only server send status and trailing metadata. + } + + /// Returns the client's next request to the server. + /// - Returns: The request to be made to the server. + mutating func nextRequest() throws -> ByteBuffer? { + switch self.state { + case .clientIdleServerIdle(let configuration): + preconditionFailure("Client is not open yet.") + case .clientOpenServerIdle(var clientOpenState): + let request = try clientOpenState.framer.next(compressor: clientOpenState.compressor) + self.state = .clientOpenServerIdle(clientOpenState) + return request + case .clientOpenServerOpen(var clientOpenState): + let request = try clientOpenState.framer.next(compressor: clientOpenState.compressor) + self.state = .clientOpenServerOpen(clientOpenState) + return request + case .clientOpenServerClosed: + // Nothing to do: no point in sending request if server is closed. + return nil + case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: + preconditionFailure("Can't send request if client is closed") + } + } + + mutating func receive(metadata: Metadata, endStream: Bool) { + // This is metadata received by the client from the server. + // It can be initial, which confirms that the server is now open; + // or an END_STREAM trailer, meaning the response is over. + if endStream { + self.clientReceivedEndHeader() + } else { + self.clientReceivedMetadata() + } + } + + mutating func clientReceivedEndHeader() { + switch self.state { + case .clientIdleServerIdle: + preconditionFailure("Client can't have received a stream end trailer if both client and server are idle.") + case .clientOpenServerIdle: + preconditionFailure("Server cannot have sent an end stream header if it is still idle.") + case .clientOpenServerOpen(let clientOpenState): + self.state = .clientOpenServerClosed(clientOpenState) + case .clientOpenServerClosed: + preconditionFailure("Server is already closed, can't have received the end stream trailer twice.") + case .clientClosedServerIdle: + preconditionFailure("Server cannot have sent end stream trailer if it is idle.") + case .clientClosedServerOpen: + self.state = .clientClosedServerClosed + case .clientClosedServerClosed: + preconditionFailure("Server cannot have sent end stream trailer if it is already closed.") + } + } + + mutating func clientReceivedMetadata() { + switch self.state { + case .clientIdleServerIdle: + preconditionFailure("Server cannot have sent metadata if the client is idle.") + case .clientOpenServerIdle(let clientOpenState): + self.state = .clientOpenServerOpen(clientOpenState) + case .clientOpenServerOpen: + // This state is valid: server can send trailing metadata without END_STREAM + // set, and follow it with an empty message frame where the flag *is* set. + // Do nothing in this case. + () + case .clientOpenServerClosed, .clientClosedServerClosed: + preconditionFailure("Server is closed, nothing could have been sent.") + case .clientClosedServerIdle, .clientClosedServerOpen: + preconditionFailure("Client is closed, cannot have received anything.") + () + } + } + + mutating func receive(message: ByteBuffer, endStream: Bool) { + // This is a message received by the client, from the server. + switch self.state { + case .clientIdleServerIdle: + preconditionFailure("Cannot have received anything from server if client is not yet open.") + case .clientOpenServerIdle: + preconditionFailure("Server cannot have sent a message before sending the initial metadata.") + case .clientOpenServerOpen(let clientOpenState): + // TODO: figure out how to do this + try? clientOpenState.deframer.process(buffer: message, { _ in }) + if endStream { + self.state = .clientOpenServerClosed(clientOpenState) + } + case .clientOpenServerClosed: + preconditionFailure("Cannot have received anything from a closed server.") + case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: + preconditionFailure("Shouldn't receive anything if client's closed.") + } } } - - // - MARK: Server-closing transitions - - mutating func clientReceivedEndHeader() { - switch self.state { - case .clientIdleServerIdle: - preconditionFailure("Client can't have received a stream end trailer if both client and server are idle.") - case .clientOpenServerIdle: - preconditionFailure("Server cannot have sent an end stream header if it is still idle.") - case .clientOpenServerOpen(let clientOpenState): - self.state = .clientOpenServerClosed(clientOpenState) - case .clientOpenServerClosed: - preconditionFailure("Server is already closed, can't have received the end stream trailer twice.") - case .clientClosedServerIdle: - preconditionFailure("Server cannot have sent end stream trailer if it is idle.") - case .clientClosedServerOpen: - self.state = .clientClosedServerClosed - case .clientClosedServerClosed: - preconditionFailure("Server cannot have sent end stream trailer if it is already closed.") +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension GRPCStreamStateMachine { + struct Server: GRPCStreamStateMachineProtocol { + fileprivate var state: GRPCStreamStateMachineState + + init(configuration: GRPCStreamStateMachineConfiguration) { + self.state = .clientIdleServerIdle(configuration) + } + + mutating func send(metadata: Metadata) { + // Server sends initial metadata. This transitions server to open. + switch self.state { + case .clientIdleServerIdle: + preconditionFailure("Client cannot be idle if server is sending initial metadata: it must have opened.") + case .clientOpenServerIdle(let clientOpenState): + self.state = .clientOpenServerOpen(clientOpenState) + case .clientOpenServerOpen: + preconditionFailure("Server has already sent initial metadata.") + case .clientOpenServerClosed: + preconditionFailure("Server cannot send metadata if closed.") + case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: + preconditionFailure("No point in sending initial metadata if client is closed.") + } + } + + mutating func send(message: [UInt8], endStream: Bool) { + switch self.state { + case .clientIdleServerIdle(let configuration): + preconditionFailure("Cannot send a message when idle.") + case .clientOpenServerIdle(let clientOpenState): + preconditionFailure("Server must have sent initial metadata before sending a message.") + case .clientOpenServerOpen(var clientOpenState): + clientOpenState.framer.append(message) + self.state = .clientOpenServerOpen(clientOpenState) + case .clientOpenServerClosed(let clientOpenState): + preconditionFailure("Server can't send a message if it's closed.") + case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: + preconditionFailure("Server can't send a message to a closed client.") + } + } + + mutating func send(status: String, trailingMetadata: Metadata) { + // Close the server. + switch self.state { + case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: + preconditionFailure("Server can't send anything if idle.") + case .clientOpenServerOpen(let clientOpenState): + self.state = .clientOpenServerClosed(clientOpenState) + case .clientOpenServerClosed: + preconditionFailure("Server is closed, can't send anything else.") + case .clientClosedServerOpen: + self.state = .clientClosedServerClosed + case .clientClosedServerClosed: + preconditionFailure("Server can't send anything if closed.") + } + } + + mutating func receive(metadata: Metadata, endStream: Bool) { + // We validate the received headers: compression must be valid if set, and + // grpc-timeout and method name must be present. + // If end stream is set, the client will be closed - otherwise, it will be opened. + guard self.hasValidHeaders(metadata) else { + self.state = .clientClosedServerClosed + return + } + + switch self.state { + case .clientIdleServerIdle(let configuration): + let state = GRPCStreamStateMachineState.ClientOpenState( + maximumPayloadSize: configuration.maximumPayloadSize, + compressor: configuration.compressor, + decompressor: configuration.decompressor + ) + if endStream { + self.state = .clientClosedServerIdle(state) + } else { + self.state = .clientOpenServerIdle(state) + } + case .clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed: + preconditionFailure("Client shouldn't have sent metadata twice.") + case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: + preconditionFailure("Client can't have sent metadata if closed.") + } + } + + private func hasValidHeaders(_ metadata: Metadata) -> Bool { + // TODO: validate grpc-timeout and method name are present, content type, compression if present. + return false + } + + mutating func receive(message: ByteBuffer, endStream: Bool) { + switch self.state { + case .clientIdleServerIdle(let configuration): + preconditionFailure("Can't have received a message if client is idle.") + case .clientOpenServerIdle(let clientOpenState): + // TODO: figure out how to do this. + try? clientOpenState.deframer.process(buffer: message, { _ in }) + if endStream { + self.state = .clientClosedServerIdle(clientOpenState) + } + case .clientOpenServerOpen(let clientOpenState): + // TODO: figure out how to do this. + try? clientOpenState.deframer.process(buffer: message, { _ in }) + if endStream { + self.state = .clientClosedServerIdle(clientOpenState) + } + case .clientOpenServerClosed(let clientOpenState): + // Client is not done sending request, but server has already closed. + // Ignore the rest of the request: do nothing. + () + case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: + preconditionFailure("Client can't send a message if closed.") + } + } + + mutating func nextRequest() throws -> ByteBuffer? { + return nil } } } From 6b5af81639a24f36bf944e2dc9541dcf99698fda Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Wed, 7 Feb 2024 11:19:02 +0000 Subject: [PATCH 03/51] Add individual state structs for each state --- Sources/GRPCCore/Internal/Metadata+GRPC.swift | 15 ++ .../GRPCStreamStateMachine.swift | 253 ++++++++++++------ 2 files changed, 186 insertions(+), 82 deletions(-) diff --git a/Sources/GRPCCore/Internal/Metadata+GRPC.swift b/Sources/GRPCCore/Internal/Metadata+GRPC.swift index 9b5d88c8e..f80b51a1e 100644 --- a/Sources/GRPCCore/Internal/Metadata+GRPC.swift +++ b/Sources/GRPCCore/Internal/Metadata+GRPC.swift @@ -50,12 +50,27 @@ extension Metadata { } } } + + @inlinable + public var encoding: String? { + get { + self.firstString(forKey: .encoding) + } + set { + if let newValue { + self.replaceOrAddString(newValue, forKey: .encoding) + } else { + self.removeAllValues(forKey: .encoding) + } + } + } } extension Metadata { @usableFromInline enum GRPCKey: String, Sendable, Hashable { case timeout = "grpc-timeout" + case encoding = "grpc-encoding" case retryPushbackMs = "grpc-retry-pushback-ms" case previousRPCAttempts = "grpc-previous-rpc-attempts" } diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 4f28dcd1c..a722e050b 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -30,59 +30,143 @@ fileprivate protocol GRPCStreamStateMachineProtocol { mutating func nextRequest() throws -> ByteBuffer? } -struct GRPCStreamStateMachineConfiguration { - enum Party { - case client - case server +enum GRPCStreamStateMachineConfiguration { + enum CompressionAlgorithm: String { + case identity + case deflate + case gzip + + func getZlibMethod() -> Zlib.Method? { + switch self { + case .identity: + return nil + case .deflate: + return .deflate + case .gzip: + return .gzip + } + } } - - let party: Party - let maximumPayloadSize: Int - let compressor: Zlib.Compressor? - let decompressor: Zlib.Decompressor? + + case client(maximumPayloadSize: Int) + case server( + maximumPayloadSize: Int, + supportedCompressionAlgorithms: [CompressionAlgorithm] + ) } fileprivate enum GRPCStreamStateMachineState { - case clientIdleServerIdle(GRPCStreamStateMachineConfiguration) - case clientOpenServerIdle(ClientOpenState) - case clientOpenServerOpen(ClientOpenState) - case clientOpenServerClosed(ClientOpenState) - case clientClosedServerIdle(ClientOpenState) - case clientClosedServerOpen(ClientOpenState) + case clientIdleServerIdle(ClientIdleServerIdleState) + case clientOpenServerIdle(ClientOpenServerIdleState) + case clientOpenServerOpen(ClientOpenServerOpenState) + case clientOpenServerClosed(ClientOpenServerClosedState) + case clientClosedServerIdle(ClientClosedServerIdleState) + case clientClosedServerOpen(ClientClosedServerOpenState) case clientClosedServerClosed - struct ClientOpenState { + struct ClientIdleServerIdleState { + let maximumPayloadSize: Int + } + + struct ClientOpenServerIdleState { var framer: GRPCMessageFramer - let deframer: NIOSingleStepByteToMessageProcessor var compressor: Zlib.Compressor? + let deframer: NIOSingleStepByteToMessageProcessor + var decompressor: Zlib.Decompressor? + init( - maximumPayloadSize: Int, - compressor: Zlib.Compressor?, - decompressor: Zlib.Decompressor? + previousState: ClientIdleServerIdleState, + compressionAlgorithm: GRPCStreamStateMachineConfiguration.CompressionAlgorithm? ) { + if let zlibMethod = compressionAlgorithm?.getZlibMethod() { + self.compressor = Zlib.Compressor(method: zlibMethod) + self.decompressor = Zlib.Decompressor(method: zlibMethod) + } + self.framer = GRPCMessageFramer() - let messageDeframer = GRPCMessageDeframer( - maximumPayloadSize: maximumPayloadSize, - decompressor: decompressor + let decoder = GRPCMessageDeframer( + maximumPayloadSize: previousState.maximumPayloadSize, + decompressor: self.decompressor ) - self.deframer = NIOSingleStepByteToMessageProcessor(messageDeframer) - self.compressor = compressor + self.deframer = NIOSingleStepByteToMessageProcessor(decoder) + } + } + + struct ClientOpenServerOpenState { + var framer: GRPCMessageFramer + var compressor: Zlib.Compressor? + + let deframer: NIOSingleStepByteToMessageProcessor + var decompressor: Zlib.Decompressor? + + init(previousState: ClientOpenServerIdleState) { + self.framer = previousState.framer + self.compressor = previousState.compressor + self.deframer = previousState.deframer + self.decompressor = previousState.decompressor + } + } + + struct ClientOpenServerClosedState { + var framer: GRPCMessageFramer + var compressor: Zlib.Compressor? + + let deframer: NIOSingleStepByteToMessageProcessor + var decompressor: Zlib.Decompressor? + + init(previousState: ClientOpenServerOpenState) { + self.framer = previousState.framer + self.compressor = previousState.compressor + self.deframer = previousState.deframer + self.decompressor = previousState.decompressor + } + } + + struct ClientClosedServerIdleState { + var framer: GRPCMessageFramer + var compressor: Zlib.Compressor? + + let deframer: NIOSingleStepByteToMessageProcessor + var decompressor: Zlib.Decompressor? + + init(previousState: ClientOpenServerIdleState) { + self.framer = previousState.framer + self.compressor = previousState.compressor + self.deframer = previousState.deframer + self.decompressor = previousState.decompressor + } + } + + struct ClientClosedServerOpenState { + var framer: GRPCMessageFramer + var compressor: Zlib.Compressor? + + let deframer: NIOSingleStepByteToMessageProcessor + var decompressor: Zlib.Decompressor? + + init(previousState: ClientOpenServerOpenState) { + self.framer = previousState.framer + self.compressor = previousState.compressor + self.deframer = previousState.deframer + self.decompressor = previousState.decompressor } } } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) struct GRPCStreamStateMachine { - private var _stateMachine: GRPCStreamStateMachineProtocol init(configuration: GRPCStreamStateMachineConfiguration) { - switch configuration.party { - case .client: - self._stateMachine = Client(configuration: configuration) - case .server: - self._stateMachine = Server(configuration: configuration) + switch configuration { + case .client(let maximumPayloadSize): + self._stateMachine = Client(maximumPayloadSize: maximumPayloadSize) + case .server(let maximumPayloadSize, let supportedCompressionAlgorithms): + self._stateMachine = Server( + maximumPayloadSize: maximumPayloadSize, + supportedCompressionAlgorithms: supportedCompressionAlgorithms + ) } } @@ -116,22 +200,23 @@ extension GRPCStreamStateMachine { struct Client: GRPCStreamStateMachineProtocol { fileprivate var state: GRPCStreamStateMachineState - init(configuration: GRPCStreamStateMachineConfiguration) { - self.state = .clientIdleServerIdle(configuration) + init(maximumPayloadSize: Int) { + self.state = .clientIdleServerIdle(.init(maximumPayloadSize: maximumPayloadSize)) } mutating func send(metadata: Metadata) { // Client sends metadata only when opening the stream. // They send grpc-timeout and method name along with it. // TODO: should these things be validated in the handler or here? + + let compressionAlgorithm = GRPCStreamStateMachineConfiguration.CompressionAlgorithm(rawValue: metadata.encoding ?? "") + switch self.state { - case .clientIdleServerIdle(let configuration): - let clientOpenState = GRPCStreamStateMachineState.ClientOpenState( - maximumPayloadSize: configuration.maximumPayloadSize, - compressor: configuration.compressor, - decompressor: configuration.decompressor - ) - self.state = .clientOpenServerIdle(clientOpenState) + case .clientIdleServerIdle(let state): + self.state = .clientOpenServerIdle(.init( + previousState: state, + compressionAlgorithm: compressionAlgorithm + )) case .clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed: // Client is already open: we shouldn't be sending metadata. preconditionFailure("Invalid state: client is already open") @@ -144,21 +229,21 @@ extension GRPCStreamStateMachine { mutating func send(message: [UInt8], endStream: Bool) { // Client sends message. switch self.state { - case .clientIdleServerIdle(let configuration): + case .clientIdleServerIdle: preconditionFailure("Client not yet open") - case .clientOpenServerIdle(var clientOpenState): - clientOpenState.framer.append(message) + case .clientOpenServerIdle(var state): + state.framer.append(message) if endStream { - self.state = .clientClosedServerIdle(clientOpenState) + self.state = .clientClosedServerIdle(.init(previousState: state)) } else { - self.state = .clientOpenServerIdle(clientOpenState) + self.state = .clientOpenServerIdle(state) } - case .clientOpenServerOpen(var clientOpenState): - clientOpenState.framer.append(message) + case .clientOpenServerOpen(var state): + state.framer.append(message) if endStream { - self.state = .clientClosedServerOpen(clientOpenState) + self.state = .clientClosedServerOpen(.init(previousState: state)) } else { - self.state = .clientOpenServerOpen(clientOpenState) + self.state = .clientOpenServerOpen(state) } case .clientOpenServerClosed: // The server has closed, so it makes no sense to send the rest of the request. @@ -212,8 +297,8 @@ extension GRPCStreamStateMachine { preconditionFailure("Client can't have received a stream end trailer if both client and server are idle.") case .clientOpenServerIdle: preconditionFailure("Server cannot have sent an end stream header if it is still idle.") - case .clientOpenServerOpen(let clientOpenState): - self.state = .clientOpenServerClosed(clientOpenState) + case .clientOpenServerOpen(let state): + self.state = .clientOpenServerClosed(.init(previousState: state)) case .clientOpenServerClosed: preconditionFailure("Server is already closed, can't have received the end stream trailer twice.") case .clientClosedServerIdle: @@ -229,8 +314,8 @@ extension GRPCStreamStateMachine { switch self.state { case .clientIdleServerIdle: preconditionFailure("Server cannot have sent metadata if the client is idle.") - case .clientOpenServerIdle(let clientOpenState): - self.state = .clientOpenServerOpen(clientOpenState) + case .clientOpenServerIdle(let state): + self.state = .clientOpenServerOpen(.init(previousState: state)) case .clientOpenServerOpen: // This state is valid: server can send trailing metadata without END_STREAM // set, and follow it with an empty message frame where the flag *is* set. @@ -251,11 +336,11 @@ extension GRPCStreamStateMachine { preconditionFailure("Cannot have received anything from server if client is not yet open.") case .clientOpenServerIdle: preconditionFailure("Server cannot have sent a message before sending the initial metadata.") - case .clientOpenServerOpen(let clientOpenState): + case .clientOpenServerOpen(let state): // TODO: figure out how to do this - try? clientOpenState.deframer.process(buffer: message, { _ in }) + try? state.deframer.process(buffer: message, { _ in }) if endStream { - self.state = .clientOpenServerClosed(clientOpenState) + self.state = .clientOpenServerClosed(.init(previousState: state)) } case .clientOpenServerClosed: preconditionFailure("Cannot have received anything from a closed server.") @@ -269,10 +354,16 @@ extension GRPCStreamStateMachine { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension GRPCStreamStateMachine { struct Server: GRPCStreamStateMachineProtocol { + typealias SupportedCompressionAlgorithms = [GRPCStreamStateMachineConfiguration.CompressionAlgorithm] fileprivate var state: GRPCStreamStateMachineState + let supportedCompressionAlgorithms: SupportedCompressionAlgorithms - init(configuration: GRPCStreamStateMachineConfiguration) { - self.state = .clientIdleServerIdle(configuration) + init( + maximumPayloadSize: Int, + supportedCompressionAlgorithms: SupportedCompressionAlgorithms + ) { + self.state = .clientIdleServerIdle(.init(maximumPayloadSize: maximumPayloadSize)) + self.supportedCompressionAlgorithms = supportedCompressionAlgorithms } mutating func send(metadata: Metadata) { @@ -280,8 +371,8 @@ extension GRPCStreamStateMachine { switch self.state { case .clientIdleServerIdle: preconditionFailure("Client cannot be idle if server is sending initial metadata: it must have opened.") - case .clientOpenServerIdle(let clientOpenState): - self.state = .clientOpenServerOpen(clientOpenState) + case .clientOpenServerIdle(let state): + self.state = .clientOpenServerOpen(.init(previousState: state)) case .clientOpenServerOpen: preconditionFailure("Server has already sent initial metadata.") case .clientOpenServerClosed: @@ -293,14 +384,14 @@ extension GRPCStreamStateMachine { mutating func send(message: [UInt8], endStream: Bool) { switch self.state { - case .clientIdleServerIdle(let configuration): + case .clientIdleServerIdle: preconditionFailure("Cannot send a message when idle.") - case .clientOpenServerIdle(let clientOpenState): + case .clientOpenServerIdle: preconditionFailure("Server must have sent initial metadata before sending a message.") - case .clientOpenServerOpen(var clientOpenState): - clientOpenState.framer.append(message) - self.state = .clientOpenServerOpen(clientOpenState) - case .clientOpenServerClosed(let clientOpenState): + case .clientOpenServerOpen(var state): + state.framer.append(message) + self.state = .clientOpenServerOpen(state) + case .clientOpenServerClosed: preconditionFailure("Server can't send a message if it's closed.") case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: preconditionFailure("Server can't send a message to a closed client.") @@ -312,8 +403,8 @@ extension GRPCStreamStateMachine { switch self.state { case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: preconditionFailure("Server can't send anything if idle.") - case .clientOpenServerOpen(let clientOpenState): - self.state = .clientOpenServerClosed(clientOpenState) + case .clientOpenServerOpen(let state): + self.state = .clientOpenServerClosed(.init(previousState: state)) case .clientOpenServerClosed: preconditionFailure("Server is closed, can't send anything else.") case .clientClosedServerOpen: @@ -333,16 +424,14 @@ extension GRPCStreamStateMachine { } switch self.state { - case .clientIdleServerIdle(let configuration): - let state = GRPCStreamStateMachineState.ClientOpenState( - maximumPayloadSize: configuration.maximumPayloadSize, - compressor: configuration.compressor, - decompressor: configuration.decompressor - ) + case .clientIdleServerIdle(let state): if endStream { - self.state = .clientClosedServerIdle(state) + preconditionFailure("Client should have opened before ending the stream: stream shouldn't have been closed when sending initial metadata.") } else { - self.state = .clientOpenServerIdle(state) + self.state = .clientOpenServerIdle(.init( + previousState: state, + compressionAlgorithm: GRPCStreamStateMachineConfiguration.CompressionAlgorithm(rawValue: metadata.encoding ?? "") + )) } case .clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed: preconditionFailure("Client shouldn't have sent metadata twice.") @@ -360,17 +449,17 @@ extension GRPCStreamStateMachine { switch self.state { case .clientIdleServerIdle(let configuration): preconditionFailure("Can't have received a message if client is idle.") - case .clientOpenServerIdle(let clientOpenState): + case .clientOpenServerIdle(let state): // TODO: figure out how to do this. - try? clientOpenState.deframer.process(buffer: message, { _ in }) + try? state.deframer.process(buffer: message, { _ in }) if endStream { - self.state = .clientClosedServerIdle(clientOpenState) + self.state = .clientClosedServerIdle(.init(previousState: state)) } - case .clientOpenServerOpen(let clientOpenState): + case .clientOpenServerOpen(let state): // TODO: figure out how to do this. - try? clientOpenState.deframer.process(buffer: message, { _ in }) + try? state.deframer.process(buffer: message, { _ in }) if endStream { - self.state = .clientClosedServerIdle(clientOpenState) + self.state = .clientClosedServerOpen(.init(previousState: state)) } case .clientOpenServerClosed(let clientOpenState): // Client is not done sending request, but server has already closed. From 83fa5a8fdae08fdac6512c632ad3f08e2c733ae2 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Wed, 7 Feb 2024 14:21:17 +0000 Subject: [PATCH 04/51] Buffer inbound messages --- .../GRPCStreamStateMachine.swift | 155 ++++++++++++++---- 1 file changed, 124 insertions(+), 31 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index a722e050b..20136a3d5 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -25,9 +25,10 @@ fileprivate protocol GRPCStreamStateMachineProtocol { mutating func send(status: String, trailingMetadata: Metadata) mutating func receive(metadata: Metadata, endStream: Bool) - mutating func receive(message: ByteBuffer, endStream: Bool) + mutating func receive(message: ByteBuffer, endStream: Bool) throws - mutating func nextRequest() throws -> ByteBuffer? + mutating func nextOutboundMessage() throws -> ByteBuffer? + mutating func nextInboundMessage() -> [UInt8]? } enum GRPCStreamStateMachineConfiguration { @@ -75,6 +76,8 @@ fileprivate enum GRPCStreamStateMachineState { let deframer: NIOSingleStepByteToMessageProcessor var decompressor: Zlib.Decompressor? + var messageBuffer: OneOrManyQueue<[UInt8]> + init( previousState: ClientIdleServerIdleState, compressionAlgorithm: GRPCStreamStateMachineConfiguration.CompressionAlgorithm? @@ -90,6 +93,8 @@ fileprivate enum GRPCStreamStateMachineState { decompressor: self.decompressor ) self.deframer = NIOSingleStepByteToMessageProcessor(decoder) + + self.messageBuffer = .init() } } @@ -100,11 +105,14 @@ fileprivate enum GRPCStreamStateMachineState { let deframer: NIOSingleStepByteToMessageProcessor var decompressor: Zlib.Decompressor? + var messageBuffer: OneOrManyQueue<[UInt8]> + init(previousState: ClientOpenServerIdleState) { self.framer = previousState.framer self.compressor = previousState.compressor self.deframer = previousState.deframer self.decompressor = previousState.decompressor + self.messageBuffer = previousState.messageBuffer } } @@ -115,11 +123,14 @@ fileprivate enum GRPCStreamStateMachineState { let deframer: NIOSingleStepByteToMessageProcessor var decompressor: Zlib.Decompressor? + var messageBuffer: OneOrManyQueue<[UInt8]> + init(previousState: ClientOpenServerOpenState) { self.framer = previousState.framer self.compressor = previousState.compressor self.deframer = previousState.deframer self.decompressor = previousState.decompressor + self.messageBuffer = previousState.messageBuffer } } @@ -145,11 +156,14 @@ fileprivate enum GRPCStreamStateMachineState { let deframer: NIOSingleStepByteToMessageProcessor var decompressor: Zlib.Decompressor? + var messageBuffer: OneOrManyQueue<[UInt8]> + init(previousState: ClientOpenServerOpenState) { self.framer = previousState.framer self.compressor = previousState.compressor self.deframer = previousState.deframer self.decompressor = previousState.decompressor + self.messageBuffer = previousState.messageBuffer } } } @@ -186,12 +200,16 @@ struct GRPCStreamStateMachine { self._stateMachine.receive(metadata: metadata, endStream: endStream) } - mutating func receive(message: ByteBuffer, endStream: Bool) { - self._stateMachine.receive(message: message, endStream: endStream) + mutating func receive(message: ByteBuffer, endStream: Bool) throws { + try self._stateMachine.receive(message: message, endStream: endStream) + } + + mutating func nextOutboundMessage() throws -> ByteBuffer? { + try self._stateMachine.nextOutboundMessage() } - mutating func nextRequest() throws -> ByteBuffer? { - try self._stateMachine.nextRequest() + mutating func nextInboundMessage() -> [UInt8]? { + self._stateMachine.nextInboundMessage() } } @@ -260,9 +278,9 @@ extension GRPCStreamStateMachine { /// Returns the client's next request to the server. /// - Returns: The request to be made to the server. - mutating func nextRequest() throws -> ByteBuffer? { + mutating func nextOutboundMessage() throws -> ByteBuffer? { switch self.state { - case .clientIdleServerIdle(let configuration): + case .clientIdleServerIdle: preconditionFailure("Client is not open yet.") case .clientOpenServerIdle(var clientOpenState): let request = try clientOpenState.framer.next(compressor: clientOpenState.compressor) @@ -329,18 +347,21 @@ extension GRPCStreamStateMachine { } } - mutating func receive(message: ByteBuffer, endStream: Bool) { + mutating func receive(message: ByteBuffer, endStream: Bool) throws { // This is a message received by the client, from the server. switch self.state { case .clientIdleServerIdle: preconditionFailure("Cannot have received anything from server if client is not yet open.") case .clientOpenServerIdle: preconditionFailure("Server cannot have sent a message before sending the initial metadata.") - case .clientOpenServerOpen(let state): - // TODO: figure out how to do this - try? state.deframer.process(buffer: message, { _ in }) + case .clientOpenServerOpen(var state): + try state.deframer.process(buffer: message) { deframedMessage in + state.messageBuffer.append(deframedMessage) + } if endStream { self.state = .clientOpenServerClosed(.init(previousState: state)) + } else { + self.state = .clientOpenServerOpen(state) } case .clientOpenServerClosed: preconditionFailure("Cannot have received anything from a closed server.") @@ -348,6 +369,25 @@ extension GRPCStreamStateMachine { preconditionFailure("Shouldn't receive anything if client's closed.") } } + + mutating func nextInboundMessage() -> [UInt8]? { + switch self.state { + case .clientOpenServerOpen(var state): + let message = state.messageBuffer.pop() + self.state = .clientOpenServerOpen(state) + return message + case .clientOpenServerClosed(var state): + let message = state.messageBuffer.pop() + self.state = .clientOpenServerClosed(state) + return message + case .clientOpenServerIdle, + .clientIdleServerIdle, + .clientClosedServerIdle, + .clientClosedServerOpen, + .clientClosedServerClosed: + return nil + } + } } } @@ -415,6 +455,10 @@ extension GRPCStreamStateMachine { } mutating func receive(metadata: Metadata, endStream: Bool) { + if endStream { + preconditionFailure("Client should have opened before ending the stream: stream shouldn't have been closed when sending initial metadata.") + } + // We validate the received headers: compression must be valid if set, and // grpc-timeout and method name must be present. // If end stream is set, the client will be closed - otherwise, it will be opened. @@ -425,14 +469,10 @@ extension GRPCStreamStateMachine { switch self.state { case .clientIdleServerIdle(let state): - if endStream { - preconditionFailure("Client should have opened before ending the stream: stream shouldn't have been closed when sending initial metadata.") - } else { - self.state = .clientOpenServerIdle(.init( - previousState: state, - compressionAlgorithm: GRPCStreamStateMachineConfiguration.CompressionAlgorithm(rawValue: metadata.encoding ?? "") - )) - } + self.state = .clientOpenServerIdle(.init( + previousState: state, + compressionAlgorithm: GRPCStreamStateMachineConfiguration.CompressionAlgorithm(rawValue: metadata.encoding ?? "") + )) case .clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed: preconditionFailure("Client shouldn't have sent metadata twice.") case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: @@ -445,23 +485,31 @@ extension GRPCStreamStateMachine { return false } - mutating func receive(message: ByteBuffer, endStream: Bool) { + mutating func receive(message: ByteBuffer, endStream: Bool) throws { switch self.state { - case .clientIdleServerIdle(let configuration): + case .clientIdleServerIdle: preconditionFailure("Can't have received a message if client is idle.") - case .clientOpenServerIdle(let state): - // TODO: figure out how to do this. - try? state.deframer.process(buffer: message, { _ in }) + case .clientOpenServerIdle(var state): + try state.deframer.process(buffer: message) { deframedMessage in + state.messageBuffer.append(deframedMessage) + } + if endStream { self.state = .clientClosedServerIdle(.init(previousState: state)) + } else { + self.state = .clientOpenServerIdle(state) } - case .clientOpenServerOpen(let state): - // TODO: figure out how to do this. - try? state.deframer.process(buffer: message, { _ in }) + case .clientOpenServerOpen(var state): + try state.deframer.process(buffer: message) { deframedMessage in + state.messageBuffer.append(deframedMessage) + } + if endStream { self.state = .clientClosedServerOpen(.init(previousState: state)) + } else { + self.state = .clientOpenServerOpen(state) } - case .clientOpenServerClosed(let clientOpenState): + case .clientOpenServerClosed: // Client is not done sending request, but server has already closed. // Ignore the rest of the request: do nothing. () @@ -470,8 +518,53 @@ extension GRPCStreamStateMachine { } } - mutating func nextRequest() throws -> ByteBuffer? { - return nil + mutating func nextOutboundMessage() throws -> ByteBuffer? { + switch self.state { + case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: + throw assertionFailureAndCreateRPCError( + errorCode: .failedPrecondition, + message: "Server is not open yet." + ) + case .clientOpenServerOpen(var state): + let response = try state.framer.next(compressor: state.compressor) + self.state = .clientOpenServerOpen(state) + return response + case .clientClosedServerOpen: + // No point in sending response if client is closed: do nothing. + return nil + case .clientOpenServerClosed, .clientClosedServerClosed: + throw assertionFailureAndCreateRPCError( + errorCode: .failedPrecondition, + message: "Can't send response if server is closed." + ) + } + } + + mutating func nextInboundMessage() -> [UInt8]? { + switch self.state { + case .clientOpenServerIdle(var state): + let request = state.messageBuffer.pop() + self.state = .clientOpenServerIdle(state) + return request + case .clientOpenServerOpen(var state): + let request = state.messageBuffer.pop() + self.state = .clientOpenServerOpen(state) + return request + case .clientClosedServerOpen(var state): + let request = state.messageBuffer.pop() + self.state = .clientClosedServerOpen(state) + return request + case .clientClosedServerIdle, + .clientIdleServerIdle, + .clientOpenServerClosed, + .clientClosedServerClosed: + return nil + } } } } + +fileprivate func assertionFailureAndCreateRPCError(errorCode: RPCError.Code, message: String) -> RPCError { + assertionFailure(message) + return RPCError(code: errorCode, message: message) +} From 3169e8b29600bf0047fdee364eab29676cee4a22 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Wed, 7 Feb 2024 14:31:32 +0000 Subject: [PATCH 05/51] Replace preconditionFailures with assertionFailure+error --- .../GRPCStreamStateMachine.swift | 140 +++++++++--------- 1 file changed, 68 insertions(+), 72 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 20136a3d5..fbd34766e 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -20,11 +20,11 @@ import NIOCore fileprivate protocol GRPCStreamStateMachineProtocol { var state: GRPCStreamStateMachineState { get set } - mutating func send(metadata: Metadata) - mutating func send(message: [UInt8], endStream: Bool) - mutating func send(status: String, trailingMetadata: Metadata) + mutating func send(metadata: Metadata) throws + mutating func send(message: [UInt8], endStream: Bool) throws + mutating func send(status: String, trailingMetadata: Metadata) throws - mutating func receive(metadata: Metadata, endStream: Bool) + mutating func receive(metadata: Metadata, endStream: Bool) throws mutating func receive(message: ByteBuffer, endStream: Bool) throws mutating func nextOutboundMessage() throws -> ByteBuffer? @@ -184,20 +184,20 @@ struct GRPCStreamStateMachine { } } - mutating func send(metadata: Metadata) { - self._stateMachine.send(metadata: metadata) + mutating func send(metadata: Metadata) throws { + try self._stateMachine.send(metadata: metadata) } - mutating func send(message: [UInt8], endStream: Bool) { - self._stateMachine.send(message: message, endStream: endStream) + mutating func send(message: [UInt8], endStream: Bool) throws { + try self._stateMachine.send(message: message, endStream: endStream) } - mutating func send(status: String, trailingMetadata: Metadata) { - self._stateMachine.send(status: status, trailingMetadata: trailingMetadata) + mutating func send(status: String, trailingMetadata: Metadata) throws { + try self._stateMachine.send(status: status, trailingMetadata: trailingMetadata) } - mutating func receive(metadata: Metadata, endStream: Bool) { - self._stateMachine.receive(metadata: metadata, endStream: endStream) + mutating func receive(metadata: Metadata, endStream: Bool) throws { + try self._stateMachine.receive(metadata: metadata, endStream: endStream) } mutating func receive(message: ByteBuffer, endStream: Bool) throws { @@ -222,7 +222,7 @@ extension GRPCStreamStateMachine { self.state = .clientIdleServerIdle(.init(maximumPayloadSize: maximumPayloadSize)) } - mutating func send(metadata: Metadata) { + mutating func send(metadata: Metadata) throws { // Client sends metadata only when opening the stream. // They send grpc-timeout and method name along with it. // TODO: should these things be validated in the handler or here? @@ -236,19 +236,17 @@ extension GRPCStreamStateMachine { compressionAlgorithm: compressionAlgorithm )) case .clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed: - // Client is already open: we shouldn't be sending metadata. - preconditionFailure("Invalid state: client is already open") + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Client is already open: shouldn't be sending metadata.") case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - // Client is closed: we shouldn't be sending metadata again. - preconditionFailure("Invalid state: client is closed") + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Client is closed: can't send metadata.") } } - mutating func send(message: [UInt8], endStream: Bool) { + mutating func send(message: [UInt8], endStream: Bool) throws { // Client sends message. switch self.state { case .clientIdleServerIdle: - preconditionFailure("Client not yet open") + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Client not yet open.") case .clientOpenServerIdle(var state): state.framer.append(message) if endStream { @@ -268,12 +266,12 @@ extension GRPCStreamStateMachine { // Do nothing. () case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - preconditionFailure("Client is closed, cannot send a message") + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Client is closed, cannot send a message.") } } - mutating func send(status: String, trailingMetadata: Metadata) { - // Nothing to do: only server send status and trailing metadata. + mutating func send(status: String, trailingMetadata: Metadata) throws { + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Client cannot send status and trailer.") } /// Returns the client's next request to the server. @@ -281,7 +279,7 @@ extension GRPCStreamStateMachine { mutating func nextOutboundMessage() throws -> ByteBuffer? { switch self.state { case .clientIdleServerIdle: - preconditionFailure("Client is not open yet.") + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Client is not open yet.") case .clientOpenServerIdle(var clientOpenState): let request = try clientOpenState.framer.next(compressor: clientOpenState.compressor) self.state = .clientOpenServerIdle(clientOpenState) @@ -294,44 +292,44 @@ extension GRPCStreamStateMachine { // Nothing to do: no point in sending request if server is closed. return nil case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - preconditionFailure("Can't send request if client is closed") + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Can't send request if client is closed.") } } - mutating func receive(metadata: Metadata, endStream: Bool) { + mutating func receive(metadata: Metadata, endStream: Bool) throws { // This is metadata received by the client from the server. // It can be initial, which confirms that the server is now open; // or an END_STREAM trailer, meaning the response is over. if endStream { - self.clientReceivedEndHeader() + try self.clientReceivedEndHeader() } else { - self.clientReceivedMetadata() + try self.clientReceivedMetadata() } } - mutating func clientReceivedEndHeader() { + mutating func clientReceivedEndHeader() throws { switch self.state { case .clientIdleServerIdle: - preconditionFailure("Client can't have received a stream end trailer if both client and server are idle.") + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Client can't have received a stream end trailer if both client and server are idle.") case .clientOpenServerIdle: - preconditionFailure("Server cannot have sent an end stream header if it is still idle.") + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server cannot have sent an end stream header if it is still idle.") case .clientOpenServerOpen(let state): self.state = .clientOpenServerClosed(.init(previousState: state)) case .clientOpenServerClosed: - preconditionFailure("Server is already closed, can't have received the end stream trailer twice.") + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server is already closed, can't have received the end stream trailer twice.") case .clientClosedServerIdle: - preconditionFailure("Server cannot have sent end stream trailer if it is idle.") + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server cannot have sent end stream trailer if it is idle.") case .clientClosedServerOpen: self.state = .clientClosedServerClosed case .clientClosedServerClosed: - preconditionFailure("Server cannot have sent end stream trailer if it is already closed.") + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server cannot have sent end stream trailer if it is already closed.") } } - mutating func clientReceivedMetadata() { + mutating func clientReceivedMetadata() throws { switch self.state { case .clientIdleServerIdle: - preconditionFailure("Server cannot have sent metadata if the client is idle.") + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server cannot have sent metadata if the client is idle.") case .clientOpenServerIdle(let state): self.state = .clientOpenServerOpen(.init(previousState: state)) case .clientOpenServerOpen: @@ -340,10 +338,9 @@ extension GRPCStreamStateMachine { // Do nothing in this case. () case .clientOpenServerClosed, .clientClosedServerClosed: - preconditionFailure("Server is closed, nothing could have been sent.") + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server is closed, nothing could have been sent.") case .clientClosedServerIdle, .clientClosedServerOpen: - preconditionFailure("Client is closed, cannot have received anything.") - () + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Client is closed, cannot have received anything.") } } @@ -351,9 +348,9 @@ extension GRPCStreamStateMachine { // This is a message received by the client, from the server. switch self.state { case .clientIdleServerIdle: - preconditionFailure("Cannot have received anything from server if client is not yet open.") + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Cannot have received anything from server if client is not yet open.") case .clientOpenServerIdle: - preconditionFailure("Server cannot have sent a message before sending the initial metadata.") + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server cannot have sent a message before sending the initial metadata.") case .clientOpenServerOpen(var state): try state.deframer.process(buffer: message) { deframedMessage in state.messageBuffer.append(deframedMessage) @@ -364,9 +361,9 @@ extension GRPCStreamStateMachine { self.state = .clientOpenServerOpen(state) } case .clientOpenServerClosed: - preconditionFailure("Cannot have received anything from a closed server.") + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Cannot have received anything from a closed server.") case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - preconditionFailure("Shouldn't receive anything if client's closed.") + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Shouldn't receive anything if client's closed.") } } @@ -406,57 +403,62 @@ extension GRPCStreamStateMachine { self.supportedCompressionAlgorithms = supportedCompressionAlgorithms } - mutating func send(metadata: Metadata) { + mutating func send(metadata: Metadata) throws { // Server sends initial metadata. This transitions server to open. switch self.state { case .clientIdleServerIdle: - preconditionFailure("Client cannot be idle if server is sending initial metadata: it must have opened.") + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Client cannot be idle if server is sending initial metadata: it must have opened.") case .clientOpenServerIdle(let state): self.state = .clientOpenServerOpen(.init(previousState: state)) case .clientOpenServerOpen: - preconditionFailure("Server has already sent initial metadata.") + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server has already sent initial metadata.") case .clientOpenServerClosed: - preconditionFailure("Server cannot send metadata if closed.") + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server cannot send metadata if closed.") case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - preconditionFailure("No point in sending initial metadata if client is closed.") + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("No point in sending initial metadata if client is closed.") } } - mutating func send(message: [UInt8], endStream: Bool) { + mutating func send(message: [UInt8], endStream: Bool) throws { switch self.state { case .clientIdleServerIdle: - preconditionFailure("Cannot send a message when idle.") + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Cannot send a message when idle.") case .clientOpenServerIdle: - preconditionFailure("Server must have sent initial metadata before sending a message.") + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server must have sent initial metadata before sending a message.") case .clientOpenServerOpen(var state): state.framer.append(message) self.state = .clientOpenServerOpen(state) case .clientOpenServerClosed: - preconditionFailure("Server can't send a message if it's closed.") + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server can't send a message if it's closed.") case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - preconditionFailure("Server can't send a message to a closed client.") + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server can't send a message to a closed client.") } } - mutating func send(status: String, trailingMetadata: Metadata) { + mutating func send(status: String, trailingMetadata: Metadata) throws { // Close the server. switch self.state { case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: - preconditionFailure("Server can't send anything if idle.") + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server can't send anything if idle.") case .clientOpenServerOpen(let state): self.state = .clientOpenServerClosed(.init(previousState: state)) case .clientOpenServerClosed: - preconditionFailure("Server is closed, can't send anything else.") + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server is closed, can't send anything else.") case .clientClosedServerOpen: self.state = .clientClosedServerClosed case .clientClosedServerClosed: - preconditionFailure("Server can't send anything if closed.") + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server can't send anything if closed.") } } - mutating func receive(metadata: Metadata, endStream: Bool) { + mutating func receive(metadata: Metadata, endStream: Bool) throws { if endStream { - preconditionFailure("Client should have opened before ending the stream: stream shouldn't have been closed when sending initial metadata.") + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition( + """ + Client should have opened before ending the stream: + stream shouldn't have been closed when sending initial metadata. + """ + ) } // We validate the received headers: compression must be valid if set, and @@ -474,9 +476,9 @@ extension GRPCStreamStateMachine { compressionAlgorithm: GRPCStreamStateMachineConfiguration.CompressionAlgorithm(rawValue: metadata.encoding ?? "") )) case .clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed: - preconditionFailure("Client shouldn't have sent metadata twice.") + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Client shouldn't have sent metadata twice.") case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - preconditionFailure("Client can't have sent metadata if closed.") + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Client can't have sent metadata if closed.") } } @@ -488,7 +490,7 @@ extension GRPCStreamStateMachine { mutating func receive(message: ByteBuffer, endStream: Bool) throws { switch self.state { case .clientIdleServerIdle: - preconditionFailure("Can't have received a message if client is idle.") + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Can't have received a message if client is idle.") case .clientOpenServerIdle(var state): try state.deframer.process(buffer: message) { deframedMessage in state.messageBuffer.append(deframedMessage) @@ -514,17 +516,14 @@ extension GRPCStreamStateMachine { // Ignore the rest of the request: do nothing. () case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - preconditionFailure("Client can't send a message if closed.") + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Client can't send a message if closed.") } } mutating func nextOutboundMessage() throws -> ByteBuffer? { switch self.state { case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: - throw assertionFailureAndCreateRPCError( - errorCode: .failedPrecondition, - message: "Server is not open yet." - ) + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server is not open yet.") case .clientOpenServerOpen(var state): let response = try state.framer.next(compressor: state.compressor) self.state = .clientOpenServerOpen(state) @@ -533,10 +532,7 @@ extension GRPCStreamStateMachine { // No point in sending response if client is closed: do nothing. return nil case .clientOpenServerClosed, .clientClosedServerClosed: - throw assertionFailureAndCreateRPCError( - errorCode: .failedPrecondition, - message: "Can't send response if server is closed." - ) + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Can't send response if server is closed.") } } @@ -564,7 +560,7 @@ extension GRPCStreamStateMachine { } } -fileprivate func assertionFailureAndCreateRPCError(errorCode: RPCError.Code, message: String) -> RPCError { +fileprivate func assertionFailureAndCreateRPCErrorOnFailedPrecondition(_ message: String) -> RPCError { assertionFailure(message) - return RPCError(code: errorCode, message: message) + return RPCError(code: .failedPrecondition, message: message) } From 406a5a9ee4941ff83a03f9fdeec931f69ad62c9c Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Wed, 7 Feb 2024 15:11:36 +0000 Subject: [PATCH 06/51] Close (de)compressor when closing --- .../GRPCStreamStateMachine.swift | 47 +++++++++++-------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index fbd34766e..f0b1b813f 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -56,7 +56,7 @@ enum GRPCStreamStateMachineConfiguration { ) } -fileprivate enum GRPCStreamStateMachineState { +enum GRPCStreamStateMachineState { case clientIdleServerIdle(ClientIdleServerIdleState) case clientOpenServerIdle(ClientOpenServerIdleState) case clientOpenServerOpen(ClientOpenServerOpenState) @@ -76,7 +76,7 @@ fileprivate enum GRPCStreamStateMachineState { let deframer: NIOSingleStepByteToMessageProcessor var decompressor: Zlib.Decompressor? - var messageBuffer: OneOrManyQueue<[UInt8]> + var inboundMessageBuffer: OneOrManyQueue<[UInt8]> init( previousState: ClientIdleServerIdleState, @@ -94,7 +94,7 @@ fileprivate enum GRPCStreamStateMachineState { ) self.deframer = NIOSingleStepByteToMessageProcessor(decoder) - self.messageBuffer = .init() + self.inboundMessageBuffer = .init() } } @@ -105,14 +105,14 @@ fileprivate enum GRPCStreamStateMachineState { let deframer: NIOSingleStepByteToMessageProcessor var decompressor: Zlib.Decompressor? - var messageBuffer: OneOrManyQueue<[UInt8]> + var inboundMessageBuffer: OneOrManyQueue<[UInt8]> init(previousState: ClientOpenServerIdleState) { self.framer = previousState.framer self.compressor = previousState.compressor self.deframer = previousState.deframer self.decompressor = previousState.decompressor - self.messageBuffer = previousState.messageBuffer + self.inboundMessageBuffer = previousState.inboundMessageBuffer } } @@ -123,14 +123,14 @@ fileprivate enum GRPCStreamStateMachineState { let deframer: NIOSingleStepByteToMessageProcessor var decompressor: Zlib.Decompressor? - var messageBuffer: OneOrManyQueue<[UInt8]> + var inboundMessageBuffer: OneOrManyQueue<[UInt8]> init(previousState: ClientOpenServerOpenState) { self.framer = previousState.framer self.compressor = previousState.compressor self.deframer = previousState.deframer self.decompressor = previousState.decompressor - self.messageBuffer = previousState.messageBuffer + self.inboundMessageBuffer = previousState.inboundMessageBuffer } } @@ -141,11 +141,14 @@ fileprivate enum GRPCStreamStateMachineState { let deframer: NIOSingleStepByteToMessageProcessor var decompressor: Zlib.Decompressor? + var inboundMessageBuffer: OneOrManyQueue<[UInt8]> + init(previousState: ClientOpenServerIdleState) { self.framer = previousState.framer self.compressor = previousState.compressor self.deframer = previousState.deframer self.decompressor = previousState.decompressor + self.inboundMessageBuffer = previousState.inboundMessageBuffer } } @@ -156,14 +159,14 @@ fileprivate enum GRPCStreamStateMachineState { let deframer: NIOSingleStepByteToMessageProcessor var decompressor: Zlib.Decompressor? - var messageBuffer: OneOrManyQueue<[UInt8]> + var inboundMessageBuffer: OneOrManyQueue<[UInt8]> init(previousState: ClientOpenServerOpenState) { self.framer = previousState.framer self.compressor = previousState.compressor self.deframer = previousState.deframer self.decompressor = previousState.decompressor - self.messageBuffer = previousState.messageBuffer + self.inboundMessageBuffer = previousState.inboundMessageBuffer } } } @@ -319,7 +322,9 @@ extension GRPCStreamStateMachine { throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server is already closed, can't have received the end stream trailer twice.") case .clientClosedServerIdle: throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server cannot have sent end stream trailer if it is idle.") - case .clientClosedServerOpen: + case .clientClosedServerOpen(let state): + state.compressor?.end() + state.decompressor?.end() self.state = .clientClosedServerClosed case .clientClosedServerClosed: throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server cannot have sent end stream trailer if it is already closed.") @@ -353,7 +358,7 @@ extension GRPCStreamStateMachine { throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server cannot have sent a message before sending the initial metadata.") case .clientOpenServerOpen(var state): try state.deframer.process(buffer: message) { deframedMessage in - state.messageBuffer.append(deframedMessage) + state.inboundMessageBuffer.append(deframedMessage) } if endStream { self.state = .clientOpenServerClosed(.init(previousState: state)) @@ -370,11 +375,11 @@ extension GRPCStreamStateMachine { mutating func nextInboundMessage() -> [UInt8]? { switch self.state { case .clientOpenServerOpen(var state): - let message = state.messageBuffer.pop() + let message = state.inboundMessageBuffer.pop() self.state = .clientOpenServerOpen(state) return message case .clientOpenServerClosed(var state): - let message = state.messageBuffer.pop() + let message = state.inboundMessageBuffer.pop() self.state = .clientOpenServerClosed(state) return message case .clientOpenServerIdle, @@ -444,7 +449,9 @@ extension GRPCStreamStateMachine { self.state = .clientOpenServerClosed(.init(previousState: state)) case .clientOpenServerClosed: throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server is closed, can't send anything else.") - case .clientClosedServerOpen: + case .clientClosedServerOpen(let state): + state.compressor?.end() + state.decompressor?.end() self.state = .clientClosedServerClosed case .clientClosedServerClosed: throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server can't send anything if closed.") @@ -460,7 +467,7 @@ extension GRPCStreamStateMachine { """ ) } - + // We validate the received headers: compression must be valid if set, and // grpc-timeout and method name must be present. // If end stream is set, the client will be closed - otherwise, it will be opened. @@ -493,7 +500,7 @@ extension GRPCStreamStateMachine { throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Can't have received a message if client is idle.") case .clientOpenServerIdle(var state): try state.deframer.process(buffer: message) { deframedMessage in - state.messageBuffer.append(deframedMessage) + state.inboundMessageBuffer.append(deframedMessage) } if endStream { @@ -503,7 +510,7 @@ extension GRPCStreamStateMachine { } case .clientOpenServerOpen(var state): try state.deframer.process(buffer: message) { deframedMessage in - state.messageBuffer.append(deframedMessage) + state.inboundMessageBuffer.append(deframedMessage) } if endStream { @@ -539,15 +546,15 @@ extension GRPCStreamStateMachine { mutating func nextInboundMessage() -> [UInt8]? { switch self.state { case .clientOpenServerIdle(var state): - let request = state.messageBuffer.pop() + let request = state.inboundMessageBuffer.pop() self.state = .clientOpenServerIdle(state) return request case .clientOpenServerOpen(var state): - let request = state.messageBuffer.pop() + let request = state.inboundMessageBuffer.pop() self.state = .clientOpenServerOpen(state) return request case .clientClosedServerOpen(var state): - let request = state.messageBuffer.pop() + let request = state.inboundMessageBuffer.pop() self.state = .clientClosedServerOpen(state) return request case .clientClosedServerIdle, From 55118b4668b4fb419dfe6f7c2436d642b7bede1c Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Wed, 7 Feb 2024 17:27:27 +0000 Subject: [PATCH 07/51] Add some tests and multiple refactors --- Sources/GRPCCore/Encoding.swift | 21 + Sources/GRPCCore/Internal/Metadata+GRPC.swift | 6 +- .../GRPCStreamStateMachine.swift | 119 +++-- .../GRPCStreamStateMachineTests.swift | 446 ++++++++++++++++++ 4 files changed, 547 insertions(+), 45 deletions(-) create mode 100644 Sources/GRPCCore/Encoding.swift create mode 100644 Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift diff --git a/Sources/GRPCCore/Encoding.swift b/Sources/GRPCCore/Encoding.swift new file mode 100644 index 000000000..eef54eb3a --- /dev/null +++ b/Sources/GRPCCore/Encoding.swift @@ -0,0 +1,21 @@ +/* + * 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. + */ + +public enum Encoding: String, Equatable { + case identity + case deflate + case gzip +} diff --git a/Sources/GRPCCore/Internal/Metadata+GRPC.swift b/Sources/GRPCCore/Internal/Metadata+GRPC.swift index f80b51a1e..13e9db122 100644 --- a/Sources/GRPCCore/Internal/Metadata+GRPC.swift +++ b/Sources/GRPCCore/Internal/Metadata+GRPC.swift @@ -52,13 +52,13 @@ extension Metadata { } @inlinable - public var encoding: String? { + public var encoding: Encoding? { get { - self.firstString(forKey: .encoding) + self.firstString(forKey: .encoding).flatMap { Encoding(rawValue: $0) } } set { if let newValue { - self.replaceOrAddString(newValue, forKey: .encoding) + self.replaceOrAddString(newValue.rawValue, forKey: .encoding) } else { self.removeAllValues(forKey: .encoding) } diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index f0b1b813f..742d0e3e0 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -19,7 +19,7 @@ import NIOCore fileprivate protocol GRPCStreamStateMachineProtocol { var state: GRPCStreamStateMachineState { get set } - + mutating func send(metadata: Metadata) throws mutating func send(message: [UInt8], endStream: Bool) throws mutating func send(status: String, trailingMetadata: Metadata) throws @@ -32,27 +32,10 @@ fileprivate protocol GRPCStreamStateMachineProtocol { } enum GRPCStreamStateMachineConfiguration { - enum CompressionAlgorithm: String { - case identity - case deflate - case gzip - - func getZlibMethod() -> Zlib.Method? { - switch self { - case .identity: - return nil - case .deflate: - return .deflate - case .gzip: - return .gzip - } - } - } - case client(maximumPayloadSize: Int) case server( maximumPayloadSize: Int, - supportedCompressionAlgorithms: [CompressionAlgorithm] + supportedCompressionAlgorithms: [Encoding] ) } @@ -80,9 +63,9 @@ enum GRPCStreamStateMachineState { init( previousState: ClientIdleServerIdleState, - compressionAlgorithm: GRPCStreamStateMachineConfiguration.CompressionAlgorithm? + compressionAlgorithm: Encoding? ) { - if let zlibMethod = compressionAlgorithm?.getZlibMethod() { + if let zlibMethod = Zlib.Method(encoding: compressionAlgorithm) { self.compressor = Zlib.Compressor(method: zlibMethod) self.decompressor = Zlib.Decompressor(method: zlibMethod) } @@ -175,14 +158,18 @@ enum GRPCStreamStateMachineState { struct GRPCStreamStateMachine { private var _stateMachine: GRPCStreamStateMachineProtocol - init(configuration: GRPCStreamStateMachineConfiguration) { + init( + configuration: GRPCStreamStateMachineConfiguration, + skipAssertions: Bool = false + ) { switch configuration { case .client(let maximumPayloadSize): - self._stateMachine = Client(maximumPayloadSize: maximumPayloadSize) + self._stateMachine = Client(maximumPayloadSize: maximumPayloadSize, skipAssertions: skipAssertions) case .server(let maximumPayloadSize, let supportedCompressionAlgorithms): self._stateMachine = Server( maximumPayloadSize: maximumPayloadSize, - supportedCompressionAlgorithms: supportedCompressionAlgorithms + supportedCompressionAlgorithms: supportedCompressionAlgorithms, + skipAssertions: skipAssertions ) } } @@ -220,23 +207,27 @@ struct GRPCStreamStateMachine { extension GRPCStreamStateMachine { struct Client: GRPCStreamStateMachineProtocol { fileprivate var state: GRPCStreamStateMachineState + private let skipAssertions: Bool - init(maximumPayloadSize: Int) { + init(maximumPayloadSize: Int, skipAssertions: Bool) { self.state = .clientIdleServerIdle(.init(maximumPayloadSize: maximumPayloadSize)) + self.skipAssertions = skipAssertions } mutating func send(metadata: Metadata) throws { // Client sends metadata only when opening the stream. - // They send grpc-timeout and method name along with it. - // TODO: should these things be validated in the handler or here? - - let compressionAlgorithm = GRPCStreamStateMachineConfiguration.CompressionAlgorithm(rawValue: metadata.encoding ?? "") - switch self.state { case .clientIdleServerIdle(let state): + guard metadata.endpoint != nil else { + throw RPCError( + code: .invalidArgument, + message: "Endpoint is missing: client cannot send initial metadata without it." + ) + } + self.state = .clientOpenServerIdle(.init( previousState: state, - compressionAlgorithm: compressionAlgorithm + compressionAlgorithm: metadata.encoding )) case .clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed: throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Client is already open: shouldn't be sending metadata.") @@ -340,12 +331,12 @@ extension GRPCStreamStateMachine { case .clientOpenServerOpen: // This state is valid: server can send trailing metadata without END_STREAM // set, and follow it with an empty message frame where the flag *is* set. - // Do nothing in this case. + // TODO: set some flag that we're expecting empty data frame with end stream () - case .clientOpenServerClosed, .clientClosedServerClosed: + case .clientOpenServerClosed: throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server is closed, nothing could have been sent.") - case .clientClosedServerIdle, .clientClosedServerOpen: - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Client is closed, cannot have received anything.") + case .clientClosedServerClosed, .clientClosedServerIdle, .clientClosedServerOpen: + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Client is closed, shouldn't have received anything.") } } @@ -390,22 +381,31 @@ extension GRPCStreamStateMachine { return nil } } + + private func assertionFailureAndCreateRPCErrorOnFailedPrecondition(_ message: String) -> RPCError { + if !self.skipAssertions { + assertionFailure(message) + } + return RPCError(code: .failedPrecondition, message: message) + } } } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension GRPCStreamStateMachine { struct Server: GRPCStreamStateMachineProtocol { - typealias SupportedCompressionAlgorithms = [GRPCStreamStateMachineConfiguration.CompressionAlgorithm] fileprivate var state: GRPCStreamStateMachineState - let supportedCompressionAlgorithms: SupportedCompressionAlgorithms + let supportedCompressionAlgorithms: [Encoding] + private let skipAssertions: Bool init( maximumPayloadSize: Int, - supportedCompressionAlgorithms: SupportedCompressionAlgorithms + supportedCompressionAlgorithms: [Encoding], + skipAssertions: Bool ) { self.state = .clientIdleServerIdle(.init(maximumPayloadSize: maximumPayloadSize)) self.supportedCompressionAlgorithms = supportedCompressionAlgorithms + self.skipAssertions = skipAssertions } mutating func send(metadata: Metadata) throws { @@ -480,7 +480,7 @@ extension GRPCStreamStateMachine { case .clientIdleServerIdle(let state): self.state = .clientOpenServerIdle(.init( previousState: state, - compressionAlgorithm: GRPCStreamStateMachineConfiguration.CompressionAlgorithm(rawValue: metadata.encoding ?? "") + compressionAlgorithm: metadata.encoding )) case .clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed: throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Client shouldn't have sent metadata twice.") @@ -564,10 +564,45 @@ extension GRPCStreamStateMachine { return nil } } + + private func assertionFailureAndCreateRPCErrorOnFailedPrecondition(_ message: String) -> RPCError { + if !self.skipAssertions { + assertionFailure(message) + } + return RPCError(code: .failedPrecondition, message: message) + } + } +} + +extension MethodDescriptor { + init?(fullyQualifiedMethod: String) { + let split = fullyQualifiedMethod.split(separator: "/") + guard split.count == 2 else { + return nil + } + self.init(service: String(split[0]), method: String(split[1])) } } -fileprivate func assertionFailureAndCreateRPCErrorOnFailedPrecondition(_ message: String) -> RPCError { - assertionFailure(message) - return RPCError(code: .failedPrecondition, message: message) +extension Metadata { + public var endpoint: MethodDescriptor? { + get { + self[stringValues: ":path"] + .first(where: { _ in true }) + .flatMap { MethodDescriptor(fullyQualifiedMethod: $0) } + } + } +} + +extension Zlib.Method { + init?(encoding: Encoding?) { + switch encoding { + case .none, .identity: + return nil + case .deflate: + self = .deflate + case .gzip: + self = .gzip + } + } } diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift new file mode 100644 index 000000000..cb1a6c24a --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -0,0 +1,446 @@ +/* + * 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 XCTest + +@testable import GRPCHTTP2Core + +final class GRPCStreamClientStateMachineTests: XCTestCase { + private let testMetadata = Metadata(dictionaryLiteral: (":path", "test/test")) + private func makeClientStateMachine() -> GRPCStreamStateMachine { + return GRPCStreamStateMachine(configuration: .client(maximumPayloadSize: 100), skipAssertions: true) + } + + func testSendMetadataWhenIdle() throws { + var stateMachine = makeClientStateMachine() + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + } + + func testSendMetadataWhenOpen() throws { + var stateMachine = makeClientStateMachine() + + // Open the stream + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + + // Try sending metadata again: should throw + XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client is already open: shouldn't be sending metadata.") + } + } + + func testSendMetadataWhenClosed() throws { + var stateMachine = makeClientStateMachine() + + // Open the stream... + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + + // ...and then close it. + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + + // Try sending metadata again: should throw + XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client is closed: can't send metadata.") + } + } + + func testSendMessageWhenIdle() { + var stateMachine = makeClientStateMachine() + + // Try to send a message without opening (i.e. without sending initial metadata) + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.send(message: [], endStream: false)) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client not yet open.") + } + } + + func testSendMessageWhenOpen() { + var stateMachine = makeClientStateMachine() + + // Open the stream... + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + + // Now send a message + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: false)) + } + + func testSendMessageWhenClosed() { + var stateMachine = makeClientStateMachine() + + // Open the stream... + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + + // Send a message successfully + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: false)) + + // ...and then close it by setting END_STREAM. + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + + // Try sending another message: it should fail + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.send(message: [], endStream: false)) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client is closed, cannot send a message.") + } + } + + func testSendStatusAndTrailersWhenIdle() { + var stateMachine = makeClientStateMachine() + + // This operation is never allowed on the client. + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.send(status: "test", trailingMetadata: .init())) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client cannot send status and trailer.") + } + } + + func testSendStatusAndTrailersWhenOpen() { + var stateMachine = makeClientStateMachine() + + // Open stream + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + + // This operation is never allowed on the client. + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.send(status: "test", trailingMetadata: .init())) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client cannot send status and trailer.") + } + } + + func testSendStatusAndTrailersWhenClosed() { + var stateMachine = makeClientStateMachine() + + // Open the stream... + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + + // ...and then close it. + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + + // This operation is never allowed on the client. + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.send(status: "test", trailingMetadata: .init())) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client cannot send status and trailer.") + } + } + + func testReceiveInitialMetadataWhenIdle() { + var stateMachine = makeClientStateMachine() + + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.receive(metadata: .init(), endStream: false)) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Server cannot have sent metadata if the client is idle.") + } + } + + func testReceiveInitialMetadataWhenOpen() { + var stateMachine = makeClientStateMachine() + + // Open the stream... + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + + // Server should open now + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + } + + func testReceiveInitialMetadataWhenClosed() { + var stateMachine = makeClientStateMachine() + + // Open the stream... + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + + // ...and then close it. + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.receive(metadata: .init(), endStream: false)) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client is closed, shouldn't have received anything.") + } + } + + func testReceiveEndTrailerWhenIdle() { + var stateMachine = makeClientStateMachine() + + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.receive(metadata: .init(), endStream: true)) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client can't have received a stream end trailer if both client and server are idle.") + } + } + + func testReceiveEndTrailerWhenOpen() { + var stateMachine = makeClientStateMachine() + + // Open the stream... + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) + } + + func testReceiveEndTrailerWhenClosed() { + var stateMachine = makeClientStateMachine() + + // Open the stream... + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + + // ...and then close it. + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.receive(metadata: .init(), endStream: true)) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client is closed, shouldn't have received anything.") + } + } + + func testReceiveMessageWhenIdle() { + + } + + func testReceiveMessageWhenOpen() { + + } + + func testReceiveMessageWhenClosed() { + + } + + func testNextOutboundMessageWhenIdle() { + + } + + func testNextOutboundMessageWhenOpen() { + + } + + func testNextOutboundMessageWhenClosed() { + + } + + func testNextInboundMessageWhenIdle() { + + } + + func testNextInboundMessageWhenOpen() { + + } + + func testNextInboundMessageWhenClosed() { + + } +} + +final class GRPCStreamServerStateMachineTests: XCTestCase { + private func makeServerStateMachine() -> GRPCStreamStateMachine { + return GRPCStreamStateMachine(configuration: .server(maximumPayloadSize: 100, supportedCompressionAlgorithms: []), skipAssertions: true) + } + + func testSendMetadataWhenIdle() throws { + var stateMachine = makeServerStateMachine() + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + } + + func testSendMetadataWhenOpen() throws { + var stateMachine = makeServerStateMachine() + + // Open the stream + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + + // Try sending metadata again: should throw + XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client is already open: shouldn't be sending metadata.") + } + } + + func testSendMetadataWhenClosed() throws { + var stateMachine = makeServerStateMachine() + + // Open the stream... + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + + // ...and then close it. + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + + // Try sending metadata again: should throw + XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client is closed: can't send metadata.") + } + } + + func testSendMessageWhenIdle() { + var stateMachine = makeServerStateMachine() + + // Try to send a message without opening (i.e. without sending initial metadata) + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.send(message: [], endStream: false)) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client not yet open.") + } + } + + func testSendMessageWhenOpen() { + var stateMachine = makeServerStateMachine() + + // Open the stream... + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + + // Now send a message + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: false)) + } + + func testSendMessageWhenClosed() { + var stateMachine = makeServerStateMachine() + + // Open the stream... + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + + // Send a message successfully + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: false)) + + // ...and then close it by setting END_STREAM. + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + + // Try sending another message: it should fail + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.send(message: [], endStream: false)) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client is closed, cannot send a message.") + } + } + + func testSendStatusAndTrailersWhenIdle() { + var stateMachine = makeServerStateMachine() + + // This operation is never allowed on the client. + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.send(status: "test", trailingMetadata: .init())) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client cannot send status and trailer.") + } + } + + func testSendStatusAndTrailersWhenOpen() { + var stateMachine = makeServerStateMachine() + + // Open stream + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + + // This operation is never allowed on the client. + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.send(status: "test", trailingMetadata: .init())) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client cannot send status and trailer.") + } + } + + func testSendStatusAndTrailersWhenClosed() { + var stateMachine = makeServerStateMachine() + + // Open the stream... + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + + // ...and then close it. + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + + // This operation is never allowed on the client. + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.send(status: "test", trailingMetadata: .init())) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client cannot send status and trailer.") + } + } + + func testReceiveInitialMetadataWhenIdle() { + var stateMachine = makeServerStateMachine() + + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.receive(metadata: .init(), endStream: false)) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Server cannot have sent metadata if the client is idle.") + } + } + + func testReceiveInitialMetadataWhenOpen() { + var stateMachine = makeServerStateMachine() + + // Open the stream... + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + + // Server should open now + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + } + + func testReceiveInitialMetadataWhenClosed() { + var stateMachine = makeServerStateMachine() + + // Open the stream... + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + + // ...and then close it. + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.receive(metadata: .init(), endStream: false)) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client is closed, shouldn't have received anything.") + } + } + + func testReceiveMessageWhenIdle() { + + } + + func testReceiveMessageWhenOpen() { + + } + + func testReceiveMessageWhenClosed() { + + } + + func testNextOutboundMessageWhenIdle() { + + } + + func testNextOutboundMessageWhenOpen() { + + } + + func testNextOutboundMessageWhenClosed() { + + } + + func testNextInboundMessageWhenIdle() { + + } + + func testNextInboundMessageWhenOpen() { + + } + + func testNextInboundMessageWhenClosed() { + + } +} From 5a29e5150d315ac9d6940452d36928e104713853 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Thu, 8 Feb 2024 10:49:17 +0000 Subject: [PATCH 08/51] Add validations to server-side receive metadata --- .../GRPCStreamStateMachine.swift | 113 +++++++++++++++--- .../GRPCHTTP2Core/Internal/ContentType.swift | 57 +++++++++ 2 files changed, 153 insertions(+), 17 deletions(-) create mode 100644 Sources/GRPCHTTP2Core/Internal/ContentType.swift diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 742d0e3e0..b4955e35b 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -17,6 +17,11 @@ import GRPCCore import NIOCore +enum OnMetadataReceived { + case reject(status: Status, trailers: Metadata) + case doNothing +} + fileprivate protocol GRPCStreamStateMachineProtocol { var state: GRPCStreamStateMachineState { get set } @@ -24,7 +29,7 @@ fileprivate protocol GRPCStreamStateMachineProtocol { mutating func send(message: [UInt8], endStream: Bool) throws mutating func send(status: String, trailingMetadata: Metadata) throws - mutating func receive(metadata: Metadata, endStream: Bool) throws + mutating func receive(metadata: Metadata, endStream: Bool) throws -> OnMetadataReceived mutating func receive(message: ByteBuffer, endStream: Bool) throws mutating func nextOutboundMessage() throws -> ByteBuffer? @@ -186,7 +191,7 @@ struct GRPCStreamStateMachine { try self._stateMachine.send(status: status, trailingMetadata: trailingMetadata) } - mutating func receive(metadata: Metadata, endStream: Bool) throws { + mutating func receive(metadata: Metadata, endStream: Bool) throws -> OnMetadataReceived { try self._stateMachine.receive(metadata: metadata, endStream: endStream) } @@ -290,7 +295,7 @@ extension GRPCStreamStateMachine { } } - mutating func receive(metadata: Metadata, endStream: Bool) throws { + mutating func receive(metadata: Metadata, endStream: Bool) throws -> OnMetadataReceived { // This is metadata received by the client from the server. // It can be initial, which confirms that the server is now open; // or an END_STREAM trailer, meaning the response is over. @@ -299,6 +304,7 @@ extension GRPCStreamStateMachine { } else { try self.clientReceivedMetadata() } + return .doNothing } mutating func clientReceivedEndHeader() throws { @@ -331,8 +337,11 @@ extension GRPCStreamStateMachine { case .clientOpenServerOpen: // This state is valid: server can send trailing metadata without END_STREAM // set, and follow it with an empty message frame where the flag *is* set. - // TODO: set some flag that we're expecting empty data frame with end stream () + // TODO: I believe we should set some flag in the state to signal that + // we're expecting an empty data frame with END_STREAM set; otherwise, + // we could get an infinite number of metadata frames from the server - + // not sure this should be allowed. case .clientOpenServerClosed: throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server is closed, nothing could have been sent.") case .clientClosedServerClosed, .clientClosedServerIdle, .clientClosedServerOpen: @@ -457,8 +466,8 @@ extension GRPCStreamStateMachine { throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server can't send anything if closed.") } } - - mutating func receive(metadata: Metadata, endStream: Bool) throws { + + mutating func receive(metadata: Metadata, endStream: Bool) throws -> OnMetadataReceived { if endStream { throw assertionFailureAndCreateRPCErrorOnFailedPrecondition( """ @@ -467,13 +476,65 @@ extension GRPCStreamStateMachine { """ ) } + + guard let contentType = metadata.contentType else { + throw RPCError(code: .invalidArgument, message: "Invalid or empty content-type.") + } + + guard let endpoint = metadata.endpoint else { + throw RPCError(code: .unimplemented, message: "No :path header has been set.") + } + + // TODO: Should we verify the RPCRouter can handle this endpoint here, + // or should we verify that in the handler? + + let encodingValues = metadata[stringValues: "grpc-encoding"] + var encodingValuesIterator = encodingValues.makeIterator() + if let rawEncoding = encodingValuesIterator.next() { + guard encodingValuesIterator.next() == nil else { + throw RPCError( + code: .invalidArgument, + message: "grpc-encoding must contain no more than one value" + ) + } + guard let encoding = Encoding(rawValue: rawEncoding) else { + let status = Status( + code: .unimplemented, + message: "\(rawEncoding) compression is not supported; supported algorithms are listed in grpc-accept-encoding" + ) + let trailers = Metadata(dictionaryLiteral: ( + "grpc-accept-encoding", + .string(self.supportedCompressionAlgorithms + .map({ $0.rawValue }) + .joined(separator: ",") + ) + )) + return .reject(status: status, trailers: trailers) + } - // We validate the received headers: compression must be valid if set, and - // grpc-timeout and method name must be present. - // If end stream is set, the client will be closed - otherwise, it will be opened. - guard self.hasValidHeaders(metadata) else { - self.state = .clientClosedServerClosed - return + guard self.supportedCompressionAlgorithms.contains(where: { $0 == encoding }) else { + if self.supportedCompressionAlgorithms.isEmpty { + throw RPCError( + code: .unimplemented, + message: "Compression is not supported" + ) + } else { + let status = Status( + code: .unimplemented, + message: "\(encoding) compression is not supported; supported algorithms are listed in grpc-accept-encoding" + ) + let trailers = Metadata(dictionaryLiteral: ( + "grpc-accept-encoding", + .string(self.supportedCompressionAlgorithms + .map({ $0.rawValue }) + .joined(separator: ",") + ) + )) + return .reject(status: status, trailers: trailers) + } + } + + // All good } switch self.state { @@ -482,6 +543,7 @@ extension GRPCStreamStateMachine { previousState: state, compressionAlgorithm: metadata.encoding )) + return .doNothing case .clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed: throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Client shouldn't have sent metadata twice.") case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: @@ -489,11 +551,6 @@ extension GRPCStreamStateMachine { } } - private func hasValidHeaders(_ metadata: Metadata) -> Bool { - // TODO: validate grpc-timeout and method name are present, content type, compression if present. - return false - } - mutating func receive(message: ByteBuffer, endStream: Bool) throws { switch self.state { case .clientIdleServerIdle: @@ -591,6 +648,28 @@ extension Metadata { .first(where: { _ in true }) .flatMap { MethodDescriptor(fullyQualifiedMethod: $0) } } + set { + if let newValue { + self.replaceOrAddString(newValue.fullyQualifiedMethod, forKey: ":path") + } else { + self.removeAllValues(forKey: ":path") + } + } + } + + public var contentType: ContentType? { + get { + self[stringValues: "content-type"] + .first(where: { _ in true }) + .flatMap { ContentType(value: $0) } + } + set { + if let newValue { + self.replaceOrAddString(newValue.canonicalValue, forKey: "content-type") + } else { + self.removeAllValues(forKey: "content-type") + } + } } } diff --git a/Sources/GRPCHTTP2Core/Internal/ContentType.swift b/Sources/GRPCHTTP2Core/Internal/ContentType.swift new file mode 100644 index 000000000..f46eb90e0 --- /dev/null +++ b/Sources/GRPCHTTP2Core/Internal/ContentType.swift @@ -0,0 +1,57 @@ +/* + * 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. + */ + +// See: +// - https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md +// - https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md +public enum ContentType { + case protobuf + case webProtobuf + case webTextProtobuf + + init?(value: String) { + switch value { + case "application/grpc", + "application/grpc+proto": + self = .protobuf + + case "application/grpc-web", + "application/grpc-web+proto": + self = .webProtobuf + + case "application/grpc-web-text", + "application/grpc-web-text+proto": + self = .webTextProtobuf + + default: + return nil + } + } + + var canonicalValue: String { + switch self { + case .protobuf: + // This is more widely supported than "application/grpc+proto" + return "application/grpc" + + case .webProtobuf: + return "application/grpc-web+proto" + + case .webTextProtobuf: + return "application/grpc-web-text+proto" + } + } +} From bf9ea2dd27c15157265fe25a063c0d5718dd8c70 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Fri, 9 Feb 2024 15:38:54 +0000 Subject: [PATCH 09/51] Tests for client side --- .../GRPCStreamStateMachine.swift | 81 +- .../GRPCStreamStateMachineTests.swift | 966 +++++++++++++++--- 2 files changed, 869 insertions(+), 178 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index b4955e35b..883cdaa4a 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -51,7 +51,7 @@ enum GRPCStreamStateMachineState { case clientOpenServerClosed(ClientOpenServerClosedState) case clientClosedServerIdle(ClientClosedServerIdleState) case clientClosedServerOpen(ClientClosedServerOpenState) - case clientClosedServerClosed + case clientClosedServerClosed(ClientClosedServerClosedState) struct ClientIdleServerIdleState { let maximumPayloadSize: Int @@ -157,6 +157,18 @@ enum GRPCStreamStateMachineState { self.inboundMessageBuffer = previousState.inboundMessageBuffer } } + + struct ClientClosedServerClosedState { + var inboundMessageBuffer: OneOrManyQueue<[UInt8]> + + init(previousState: ClientClosedServerOpenState) { + self.inboundMessageBuffer = previousState.inboundMessageBuffer + } + + init(previousState: ClientOpenServerClosedState) { + self.inboundMessageBuffer = previousState.inboundMessageBuffer + } + } } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) @@ -260,10 +272,12 @@ extension GRPCStreamStateMachine { } else { self.state = .clientOpenServerOpen(state) } - case .clientOpenServerClosed: + case .clientOpenServerClosed(let state): // The server has closed, so it makes no sense to send the rest of the request. - // Do nothing. - () + // However, do close if endStream is set. + if endStream { + self.state = .clientClosedServerClosed(.init(previousState: state)) + } case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Client is closed, cannot send a message.") } @@ -279,19 +293,25 @@ extension GRPCStreamStateMachine { switch self.state { case .clientIdleServerIdle: throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Client is not open yet.") - case .clientOpenServerIdle(var clientOpenState): - let request = try clientOpenState.framer.next(compressor: clientOpenState.compressor) - self.state = .clientOpenServerIdle(clientOpenState) + case .clientOpenServerIdle(var state): + let request = try state.framer.next(compressor: state.compressor) + self.state = .clientOpenServerIdle(state) return request - case .clientOpenServerOpen(var clientOpenState): - let request = try clientOpenState.framer.next(compressor: clientOpenState.compressor) - self.state = .clientOpenServerOpen(clientOpenState) + case .clientOpenServerOpen(var state): + let request = try state.framer.next(compressor: state.compressor) + self.state = .clientOpenServerOpen(state) return request - case .clientOpenServerClosed: + case .clientOpenServerClosed, .clientClosedServerClosed: // Nothing to do: no point in sending request if server is closed. return nil - case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Can't send request if client is closed.") + case .clientClosedServerIdle(var state): + let request = try state.framer.next(compressor: state.compressor) + self.state = .clientClosedServerIdle(state) + return request + case .clientClosedServerOpen(var state): + let request = try state.framer.next(compressor: state.compressor) + self.state = .clientClosedServerOpen(state) + return request } } @@ -322,7 +342,7 @@ extension GRPCStreamStateMachine { case .clientClosedServerOpen(let state): state.compressor?.end() state.decompressor?.end() - self.state = .clientClosedServerClosed + self.state = .clientClosedServerClosed(.init(previousState: state)) case .clientClosedServerClosed: throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server cannot have sent end stream trailer if it is already closed.") } @@ -354,7 +374,7 @@ extension GRPCStreamStateMachine { switch self.state { case .clientIdleServerIdle: throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Cannot have received anything from server if client is not yet open.") - case .clientOpenServerIdle: + case .clientOpenServerIdle, .clientClosedServerIdle: throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server cannot have sent a message before sending the initial metadata.") case .clientOpenServerOpen(var state): try state.deframer.process(buffer: message) { deframedMessage in @@ -365,10 +385,21 @@ extension GRPCStreamStateMachine { } else { self.state = .clientOpenServerOpen(state) } + case .clientClosedServerOpen(var state): + // The client may have sent the end stream and thus it's closed, + // but the server may still be responding. + try state.deframer.process(buffer: message) { deframedMessage in + state.inboundMessageBuffer.append(deframedMessage) + } + if endStream { + self.state = .clientClosedServerClosed(.init(previousState: state)) + } else { + self.state = .clientClosedServerOpen(state) + } case .clientOpenServerClosed: throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Cannot have received anything from a closed server.") - case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Shouldn't receive anything if client's closed.") + case .clientClosedServerClosed: + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Shouldn't have received anything if both client and server are closed.") } } @@ -382,11 +413,17 @@ extension GRPCStreamStateMachine { let message = state.inboundMessageBuffer.pop() self.state = .clientOpenServerClosed(state) return message - case .clientOpenServerIdle, + case .clientClosedServerOpen(var state): + let message = state.inboundMessageBuffer.pop() + self.state = .clientClosedServerOpen(state) + return message + case .clientClosedServerClosed(var state): + let message = state.inboundMessageBuffer.pop() + self.state = .clientClosedServerClosed(state) + return message + case .clientOpenServerIdle, .clientIdleServerIdle, - .clientClosedServerIdle, - .clientClosedServerOpen, - .clientClosedServerClosed: + .clientClosedServerIdle: return nil } } @@ -461,7 +498,7 @@ extension GRPCStreamStateMachine { case .clientClosedServerOpen(let state): state.compressor?.end() state.decompressor?.end() - self.state = .clientClosedServerClosed + self.state = .clientClosedServerClosed(.init(previousState: state)) case .clientClosedServerClosed: throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server can't send anything if closed.") } diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index cb1a6c24a..5d27eb2e6 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -16,26 +16,53 @@ import GRPCCore import XCTest +import NIOCore @testable import GRPCHTTP2Core final class GRPCStreamClientStateMachineTests: XCTestCase { - private let testMetadata = Metadata(dictionaryLiteral: (":path", "test/test")) + private let testMetadata: Metadata = [":path": "test/test"] + private let testMetadataWithDeflateCompression: Metadata = [ + ":path": "test/test", + "grpc-encoding": "deflate" + ] + private func makeClientStateMachine() -> GRPCStreamStateMachine { - return GRPCStreamStateMachine(configuration: .client(maximumPayloadSize: 100), skipAssertions: true) + GRPCStreamStateMachine( + configuration: .client(maximumPayloadSize: 100), + skipAssertions: true + ) + } + + // - MARK: Send Metadata + + func testSendMetadataWhenClientIdleAndServerIdle() throws { + var stateMachine = makeClientStateMachine() + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) } - func testSendMetadataWhenIdle() throws { + func testSendMetadataWhenClientOpenAndServerIdle() throws { var stateMachine = makeClientStateMachine() + + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + + // Try sending metadata again: should throw + XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client is already open: shouldn't be sending metadata.") + } } - func testSendMetadataWhenOpen() throws { + func testSendMetadataWhenClientOpenAndServerOpen() throws { var stateMachine = makeClientStateMachine() - // Open the stream + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + // Try sending metadata again: should throw XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in XCTAssertEqual(error.code, .failedPrecondition) @@ -43,13 +70,32 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { } } - func testSendMetadataWhenClosed() throws { + func testSendMetadataWhenClientOpenAndServerClosed() throws { var stateMachine = makeClientStateMachine() - // Open the stream... + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) - // ...and then close it. + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + + // Close server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) + + // Try sending metadata again: should throw + XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client is already open: shouldn't be sending metadata.") + } + } + + func testSendMetadataWhenClientClosedAndServerIdle() throws { + var stateMachine = makeClientStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + + // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) // Try sending metadata again: should throw @@ -59,7 +105,50 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { } } - func testSendMessageWhenIdle() { + func testSendMetadataWhenClientClosedAndServerOpen() throws { + var stateMachine = makeClientStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + + // Close client + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + + // Try sending metadata again: should throw + XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client is closed: can't send metadata.") + } + } + + func testSendMetadataWhenClientClosedAndServerClosed() throws { + var stateMachine = makeClientStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + + // Close client + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + + // Close server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) + + // Try sending metadata again: should throw + XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client is closed: can't send metadata.") + } + } + + // - MARK: Send Message + + func testSendMessageWhenClientIdleAndServerIdle() { var stateMachine = makeClientStateMachine() // Try to send a message without opening (i.e. without sending initial metadata) @@ -70,28 +159,97 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { } } - func testSendMessageWhenOpen() { + func testSendMessageWhenClientOpenAndServerIdle() { var stateMachine = makeClientStateMachine() - // Open the stream... + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) // Now send a message XCTAssertNoThrow(try stateMachine.send(message: [], endStream: false)) } - func testSendMessageWhenClosed() { + func testSendMessageWhenClientOpenAndServerOpen() { var stateMachine = makeClientStateMachine() - // Open the stream... + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) - // Send a message successfully + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + + // Now send a message + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: false)) + } + + func testSendMessageWhenClientOpenAndServerClosed() { + var stateMachine = makeClientStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + + // Close server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) + + // Now send a message XCTAssertNoThrow(try stateMachine.send(message: [], endStream: false)) + } + + func testSendMessageWhenClientClosedAndServerIdle() { + var stateMachine = makeClientStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + + // Close client + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + + // Try sending another message: it should fail + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.send(message: [], endStream: false)) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client is closed, cannot send a message.") + } + } + + func testSendMessageWhenClientClosedAndServerOpen() { + var stateMachine = makeClientStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + + // Close client + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + + // Try sending another message: it should fail + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.send(message: [], endStream: false)) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client is closed, cannot send a message.") + } + } + + func testSendMessageWhenClientClosedAndServerClosed() { + var stateMachine = makeClientStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) - // ...and then close it by setting END_STREAM. + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + + // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + // Close server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) + // Try sending another message: it should fail XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(message: [], endStream: false)) { error in @@ -100,7 +258,9 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { } } - func testSendStatusAndTrailersWhenIdle() { + // - MARK: Send Status and Trailers + + func testSendStatusAndTrailersWhenClientIdleAndServerIdle() { var stateMachine = makeClientStateMachine() // This operation is never allowed on the client. @@ -111,10 +271,10 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { } } - func testSendStatusAndTrailersWhenOpen() { + func testSendStatusAndTrailersWhenClientOpenAndServerIdle() { var stateMachine = makeClientStateMachine() - // Open stream + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) // This operation is never allowed on the client. @@ -125,14 +285,14 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { } } - func testSendStatusAndTrailersWhenClosed() { + func testSendStatusAndTrailersWhenClientOpenAndServerOpen() { var stateMachine = makeClientStateMachine() - // Open the stream... + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) - // ...and then close it. - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) // This operation is never allowed on the client. XCTAssertThrowsError(ofType: RPCError.self, @@ -142,305 +302,799 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { } } - func testReceiveInitialMetadataWhenIdle() { + func testSendStatusAndTrailersWhenClientOpenAndServerClosed() { var stateMachine = makeClientStateMachine() + // Open stream + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + + // Close server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) + + // This operation is never allowed on the client. XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: .init(), endStream: false)) { error in + try stateMachine.send(status: "test", trailingMetadata: .init())) { error in XCTAssertEqual(error.code, .failedPrecondition) - XCTAssertEqual(error.message, "Server cannot have sent metadata if the client is idle.") + XCTAssertEqual(error.message, "Client cannot send status and trailer.") + } + } + + func testSendStatusAndTrailersWhenClientClosedAndServerIdle() { + var stateMachine = makeClientStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + + // Close client + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + + // This operation is never allowed on the client. + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.send(status: "test", trailingMetadata: .init())) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client cannot send status and trailer.") } } - func testReceiveInitialMetadataWhenOpen() { + func testSendStatusAndTrailersWhenClientClosedAndServerOpen() { var stateMachine = makeClientStateMachine() - // Open the stream... + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) - // Server should open now + // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + + // Close client + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + + // This operation is never allowed on the client. + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.send(status: "test", trailingMetadata: .init())) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client cannot send status and trailer.") + } } - func testReceiveInitialMetadataWhenClosed() { + func testSendStatusAndTrailersWhenClientClosedAndServerClosed() { var stateMachine = makeClientStateMachine() - // Open the stream... + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) - // ...and then close it. + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + + // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + // Close server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) + + // This operation is never allowed on the client. XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: .init(), endStream: false)) { error in + try stateMachine.send(status: "test", trailingMetadata: .init())) { error in XCTAssertEqual(error.code, .failedPrecondition) - XCTAssertEqual(error.message, "Client is closed, shouldn't have received anything.") + XCTAssertEqual(error.message, "Client cannot send status and trailer.") } } - func testReceiveEndTrailerWhenIdle() { + // - MARK: Receive initial metadata + + func testReceiveInitialMetadataWhenClientIdleAndServerIdle() { var stateMachine = makeClientStateMachine() XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: .init(), endStream: true)) { error in + try stateMachine.receive(metadata: .init(), endStream: false)) { error in XCTAssertEqual(error.code, .failedPrecondition) - XCTAssertEqual(error.message, "Client can't have received a stream end trailer if both client and server are idle.") + XCTAssertEqual(error.message, "Server cannot have sent metadata if the client is idle.") + } + } + + func testReceiveInitialMetadataWhenClientOpenAndServerIdle() throws { + var stateMachine = makeClientStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + + // Open server + let action = try stateMachine.receive(metadata: .init(), endStream: false) + guard case .doNothing = action else { + XCTFail("Expected action to be doNothing") + return + } + } + + func testReceiveInitialMetadataWhenClientOpenAndServerOpen() throws { + var stateMachine = makeClientStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + + // Try opening server again + let action = try stateMachine.receive(metadata: .init(), endStream: false) + guard case .doNothing = action else { + XCTFail("Expected action to be doNothing") + return } } - func testReceiveEndTrailerWhenOpen() { + func testReceiveInitialMetadataWhenClientOpenAndServerClosed() { var stateMachine = makeClientStateMachine() - // Open the stream... + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + + // Close server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) + + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.receive(metadata: .init(), endStream: false)) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Server is closed, nothing could have been sent.") + } } - func testReceiveEndTrailerWhenClosed() { + func testReceiveInitialMetadataWhenClientClosedAndServerIdle() { var stateMachine = makeClientStateMachine() - // Open the stream... + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) - // ...and then close it. + // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: .init(), endStream: true)) { error in + try stateMachine.receive(metadata: .init(), endStream: false)) { error in XCTAssertEqual(error.code, .failedPrecondition) XCTAssertEqual(error.message, "Client is closed, shouldn't have received anything.") } } - func testReceiveMessageWhenIdle() { + func testReceiveInitialMetadataWhenClientClosedAndServerOpen() { + var stateMachine = makeClientStateMachine() - } - - func testReceiveMessageWhenOpen() { + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) - } - - func testReceiveMessageWhenClosed() { + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - } - - func testNextOutboundMessageWhenIdle() { + // Close client + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.receive(metadata: .init(), endStream: false)) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client is closed, shouldn't have received anything.") + } } - func testNextOutboundMessageWhenOpen() { + func testReceiveInitialMetadataWhenClientClosedAndServerClosed() { + var stateMachine = makeClientStateMachine() - } - - func testNextOutboundMessageWhenClosed() { + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) - } - - func testNextInboundMessageWhenIdle() { + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - } - - func testNextInboundMessageWhenOpen() { + // Close client + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + + // Close server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.receive(metadata: .init(), endStream: false)) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client is closed, shouldn't have received anything.") + } } - func testNextInboundMessageWhenClosed() { + // - MARK: Receive end trailers + + func testReceiveEndTrailerWhenClientIdleAndServerIdle() { + var stateMachine = makeClientStateMachine() - } -} - -final class GRPCStreamServerStateMachineTests: XCTestCase { - private func makeServerStateMachine() -> GRPCStreamStateMachine { - return GRPCStreamStateMachine(configuration: .server(maximumPayloadSize: 100, supportedCompressionAlgorithms: []), skipAssertions: true) + // Receive an end trailer + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.receive(metadata: .init(), endStream: true)) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client can't have received a stream end trailer if both client and server are idle.") + } } - func testSendMetadataWhenIdle() throws { - var stateMachine = makeServerStateMachine() - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + func testReceiveEndTrailerWhenClientOpenAndServerIdle() { + var stateMachine = makeClientStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + + // Receive an end trailer + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.receive(metadata: .init(), endStream: true)) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Server cannot have sent an end stream header if it is still idle.") + } } - func testSendMetadataWhenOpen() throws { - var stateMachine = makeServerStateMachine() + func testReceiveEndTrailerWhenClientOpenAndServerOpen() throws { + var stateMachine = makeClientStateMachine() - // Open the stream - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) - // Try sending metadata again: should throw - XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in - XCTAssertEqual(error.code, .failedPrecondition) - XCTAssertEqual(error.message, "Client is already open: shouldn't be sending metadata.") + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + + // Receive an end trailer + let action = try stateMachine.receive(metadata: .init(), endStream: true) + guard case .doNothing = action else { + XCTFail("Expected action to be doNothing") + return } } - func testSendMetadataWhenClosed() throws { - var stateMachine = makeServerStateMachine() + func testReceiveEndTrailerWhenClientOpenAndServerClosed() { + var stateMachine = makeClientStateMachine() - // Open the stream... - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) - // ...and then close it. - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - // Try sending metadata again: should throw - XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in + // Close server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) + + // Receive another end trailer + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.receive(metadata: .init(), endStream: true)) { error in XCTAssertEqual(error.code, .failedPrecondition) - XCTAssertEqual(error.message, "Client is closed: can't send metadata.") + XCTAssertEqual(error.message, "Server is already closed, can't have received the end stream trailer twice.") } } - func testSendMessageWhenIdle() { - var stateMachine = makeServerStateMachine() + func testReceiveEndTrailerWhenClientClosedAndServerIdle() { + var stateMachine = makeClientStateMachine() - // Try to send a message without opening (i.e. without sending initial metadata) + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + + // Close client + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + + // Closing the server now should throw XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(message: [], endStream: false)) { error in + try stateMachine.receive(metadata: .init(), endStream: true)) { error in XCTAssertEqual(error.code, .failedPrecondition) - XCTAssertEqual(error.message, "Client not yet open.") + XCTAssertEqual(error.message, "Server cannot have sent end stream trailer if it is idle.") } } - func testSendMessageWhenOpen() { - var stateMachine = makeServerStateMachine() + func testReceiveEndTrailerWhenClientClosedAndServerOpen() throws { + var stateMachine = makeClientStateMachine() - // Open the stream... - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) - // Now send a message - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: false)) + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + + // Close the client stream + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + + // Closing the server now should not throw + let action = try stateMachine.receive(metadata: .init(), endStream: true) + guard case .doNothing = action else { + XCTFail("Expected action to be doNothing") + return + } } - func testSendMessageWhenClosed() { - var stateMachine = makeServerStateMachine() + func testReceiveEndTrailerWhenClientClosedAndServerClosed() { + var stateMachine = makeClientStateMachine() - // Open the stream... - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) - // Send a message successfully - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: false)) + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - // ...and then close it by setting END_STREAM. + // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - // Try sending another message: it should fail + // Close server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) + + // Closing the server again should throw XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(message: [], endStream: false)) { error in + try stateMachine.receive(metadata: .init(), endStream: true)) { error in XCTAssertEqual(error.code, .failedPrecondition) - XCTAssertEqual(error.message, "Client is closed, cannot send a message.") + XCTAssertEqual(error.message, "Server cannot have sent end stream trailer if it is already closed.") } } - func testSendStatusAndTrailersWhenIdle() { - var stateMachine = makeServerStateMachine() + // - MARK: Receive message + + func testReceiveMessageWhenClientIdleAndServerIdle() { + var stateMachine = makeClientStateMachine() - // This operation is never allowed on the client. XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(status: "test", trailingMetadata: .init())) { error in + try stateMachine.receive(message: .init(), endStream: false)) { error in XCTAssertEqual(error.code, .failedPrecondition) - XCTAssertEqual(error.message, "Client cannot send status and trailer.") + XCTAssertEqual(error.message, "Cannot have received anything from server if client is not yet open.") } } - func testSendStatusAndTrailersWhenOpen() { - var stateMachine = makeServerStateMachine() + func testReceiveMessageWhenClientOpenAndServerIdle() { + var stateMachine = makeClientStateMachine() - // Open stream - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) - // This operation is never allowed on the client. XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(status: "test", trailingMetadata: .init())) { error in + try stateMachine.receive(message: .init(), endStream: false)) { error in XCTAssertEqual(error.code, .failedPrecondition) - XCTAssertEqual(error.message, "Client cannot send status and trailer.") + XCTAssertEqual(error.message, "Server cannot have sent a message before sending the initial metadata.") } } - func testSendStatusAndTrailersWhenClosed() { - var stateMachine = makeServerStateMachine() + func testReceiveMessageWhenClientOpenAndServerOpen() throws { + var stateMachine = makeClientStateMachine() - // Open the stream... - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) - // ...and then close it. - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + } + + func testReceiveMessageWhenClientOpenAndServerClosed() { + var stateMachine = makeClientStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + + // Close server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) - // This operation is never allowed on the client. XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(status: "test", trailingMetadata: .init())) { error in + try stateMachine.receive(message: .init(), endStream: false)) { error in XCTAssertEqual(error.code, .failedPrecondition) - XCTAssertEqual(error.message, "Client cannot send status and trailer.") + XCTAssertEqual(error.message, "Cannot have received anything from a closed server.") } } - func testReceiveInitialMetadataWhenIdle() { - var stateMachine = makeServerStateMachine() + func testReceiveMessageWhenClientClosedAndServerIdle() { + var stateMachine = makeClientStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + + // Close client + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: .init(), endStream: false)) { error in + try stateMachine.receive(message: .init(), endStream: false)) { error in XCTAssertEqual(error.code, .failedPrecondition) - XCTAssertEqual(error.message, "Server cannot have sent metadata if the client is idle.") + XCTAssertEqual(error.message, "Server cannot have sent a message before sending the initial metadata.") } } - func testReceiveInitialMetadataWhenOpen() { - var stateMachine = makeServerStateMachine() + func testReceiveMessageWhenClientClosedAndServerOpen() { + var stateMachine = makeClientStateMachine() - // Open the stream... - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) - // Server should open now + // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + + // Close client + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) } - func testReceiveInitialMetadataWhenClosed() { - var stateMachine = makeServerStateMachine() + func testReceiveMessageWhenClientClosedAndServerClosed() { + var stateMachine = makeClientStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) - // Open the stream... - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - // ...and then close it. + // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + // Close server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) + XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: .init(), endStream: false)) { error in + try stateMachine.receive(message: .init(), endStream: false)) { error in XCTAssertEqual(error.code, .failedPrecondition) - XCTAssertEqual(error.message, "Client is closed, shouldn't have received anything.") + XCTAssertEqual(error.message, "Shouldn't have received anything if both client and server are closed.") + } + } + + // - MARK: Next outbound message + + func testNextOutboundMessageWhenClientIdleAndServerIdle() { + var stateMachine = makeClientStateMachine() + + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.nextOutboundMessage()) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client is not open yet.") } } - func testReceiveMessageWhenIdle() { + func testNextOutboundMessageWhenClientOpenAndServerIdle() throws { + var stateMachine = makeClientStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + + var request = try stateMachine.nextOutboundMessage() + XCTAssertNil(request) + + XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) + request = try XCTUnwrap(stateMachine.nextOutboundMessage()) + + let expectedBytes: [UInt8] = [ + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42 // original message + ] + XCTAssertEqual(Array(buffer: request!), expectedBytes) + } + + func testNextOutboundMessageWhenClientOpenAndServerIdle_WithCompression() throws { + var stateMachine = makeClientStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadataWithDeflateCompression)) + + var request = try stateMachine.nextOutboundMessage() + XCTAssertNil(request) + + let originalMessage = [UInt8]([42, 42, 43, 43]) + XCTAssertNoThrow(try stateMachine.send(message: originalMessage, endStream: false)) + request = try XCTUnwrap(stateMachine.nextOutboundMessage()) + + var framer = GRPCMessageFramer() + let compressor = Zlib.Compressor(method: .deflate) + defer { compressor.end() } + framer.append(originalMessage) + + let framedMessage = try XCTUnwrap(framer.next(compressor: compressor)) + let expectedBytes = Array(buffer: framedMessage) + XCTAssertEqual(Array(buffer: request!), expectedBytes) + } + + func testNextOutboundMessageWhenClientOpenAndServerOpen() throws { + var stateMachine = makeClientStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + + var request = try stateMachine.nextOutboundMessage() + XCTAssertNil(request) + + XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) + request = try XCTUnwrap(stateMachine.nextOutboundMessage()) + + let expectedBytes: [UInt8] = [ + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42 // original message + ] + XCTAssertEqual(Array(buffer: request!), expectedBytes) + } + + func testNextOutboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { + var stateMachine = makeClientStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadataWithDeflateCompression)) + + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + + var request = try stateMachine.nextOutboundMessage() + XCTAssertNil(request) + + let originalMessage = [UInt8]([42, 42, 43, 43]) + XCTAssertNoThrow(try stateMachine.send(message: originalMessage, endStream: false)) + request = try XCTUnwrap(stateMachine.nextOutboundMessage()) + + var framer = GRPCMessageFramer() + let compressor = Zlib.Compressor(method: .deflate) + defer { compressor.end() } + framer.append(originalMessage) + + let framedMessage = try XCTUnwrap(framer.next(compressor: compressor)) + let expectedBytes = Array(buffer: framedMessage) + XCTAssertEqual(Array(buffer: request!), expectedBytes) + } + + func testNextOutboundMessageWhenClientOpenAndServerClosed() throws { + var stateMachine = makeClientStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + + // Close server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) + + // No messages to send, so make sure nil is returned + XCTAssertNil(try stateMachine.nextOutboundMessage()) + + // Queue a message, but assert the next outbound message is nil nevertheless, + // because the server is closed. + XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) + XCTAssertNil(try stateMachine.nextOutboundMessage()) + } + + func testNextOutboundMessageWhenClientClosedAndServerIdle() throws { + var stateMachine = makeClientStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + + // Send a message and close client + XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: true)) + + // Make sure that getting the next outbound message _does_ return the message + // we have enqueued. + let request = try XCTUnwrap(stateMachine.nextOutboundMessage()) + let expectedBytes: [UInt8] = [ + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42 // original message + ] + XCTAssertEqual(Array(buffer: request), expectedBytes) + + // And then make sure that nothing else is returned anymore + XCTAssertNil(try stateMachine.nextOutboundMessage()) + } + + func testNextOutboundMessageWhenClientClosedAndServerOpen() throws { + var stateMachine = makeClientStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + // Send a message and close client + XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: true)) + + // Make sure that getting the next outbound message _does_ return the message + // we have enqueued. + let request = try XCTUnwrap(stateMachine.nextOutboundMessage()) + let expectedBytes: [UInt8] = [ + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42 // original message + ] + XCTAssertEqual(Array(buffer: request), expectedBytes) + + // And then make sure that nothing else is returned anymore + XCTAssertNil(try stateMachine.nextOutboundMessage()) } - func testReceiveMessageWhenOpen() { + func testNextOutboundMessageWhenClientClosedAndServerClosed() { + var stateMachine = makeClientStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + // Send a message + XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) + + // Close server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) + + // Close client + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + + // Even though we have enqueued a message, don't send it, because the server + // is closed. + XCTAssertNil(try stateMachine.nextOutboundMessage()) + } + + // - MARK: Next inbound message + + func testNextInboundMessageWhenClientIdleAndServerIdle() { + var stateMachine = makeClientStateMachine() + XCTAssertNil(stateMachine.nextInboundMessage()) } - func testReceiveMessageWhenClosed() { + func testNextInboundMessageWhenClientOpenAndServerIdle() { + var stateMachine = makeClientStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNil(stateMachine.nextInboundMessage()) } - func testNextOutboundMessageWhenIdle() { + func testNextInboundMessageWhenClientOpenAndServerOpen() throws { + var stateMachine = makeClientStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + + let receivedBytes = ByteBuffer(bytes: [ + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42 // original message + ]) + try stateMachine.receive(message: receivedBytes, endStream: false) + + let receivedMessage = try XCTUnwrap(stateMachine.nextInboundMessage()) + XCTAssertEqual(receivedMessage, [42, 42]) + + XCTAssertNil(stateMachine.nextInboundMessage()) } - func testNextOutboundMessageWhenOpen() { + func testNextInboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { + var stateMachine = makeClientStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadataWithDeflateCompression)) + + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + + let originalMessage = [UInt8]([42, 42, 43, 43]) + var framer = GRPCMessageFramer() + let compressor = Zlib.Compressor(method: .deflate) + defer { compressor.end() } + framer.append(originalMessage) + let receivedBytes = try framer.next(compressor: compressor)! + + try stateMachine.receive(message: receivedBytes, endStream: false) + + let receivedMessage = try XCTUnwrap(stateMachine.nextInboundMessage()) + XCTAssertEqual(receivedMessage, originalMessage) + XCTAssertNil(stateMachine.nextInboundMessage()) } - func testNextOutboundMessageWhenClosed() { + func testNextInboundMessageWhenClientOpenAndServerClosed() throws { + var stateMachine = makeClientStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + + let receivedBytes = ByteBuffer(bytes: [ + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42 // original message + ]) + try stateMachine.receive(message: receivedBytes, endStream: false) + + // Close server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) + + let receivedMessage = try XCTUnwrap(stateMachine.nextInboundMessage()) + XCTAssertEqual(receivedMessage, [42, 42]) + + XCTAssertNil(stateMachine.nextInboundMessage()) } - func testNextInboundMessageWhenIdle() { + func testNextInboundMessageWhenClientClosedAndServerIdle() { + var stateMachine = makeClientStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + + // Close client + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + // If server is idle it means we never got any messages, assert no inbound + // message is present. + XCTAssertNil(stateMachine.nextInboundMessage()) } - func testNextInboundMessageWhenOpen() { + func testNextInboundMessageWhenClientClosedAndServerOpen() throws { + var stateMachine = makeClientStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + let receivedBytes = ByteBuffer(bytes: [ + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42 // original message + ]) + try stateMachine.receive(message: receivedBytes, endStream: false) + + // Close client + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + + // Even though the client is closed, because it received a message while open, + // we must get the message now. + let receivedMessage = try XCTUnwrap(stateMachine.nextInboundMessage()) + XCTAssertEqual(receivedMessage, [42, 42]) + + XCTAssertNil(stateMachine.nextInboundMessage()) } - func testNextInboundMessageWhenClosed() { + func testNextInboundMessageWhenClientClosedAndServerClosed() throws { + var stateMachine = makeClientStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + + let receivedBytes = ByteBuffer(bytes: [ + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42 // original message + ]) + try stateMachine.receive(message: receivedBytes, endStream: false) + + // Close server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) + + // Close client + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + + // Even though the client is closed, because it received a message while open, + // we must get the message now. + let receivedMessage = try XCTUnwrap(stateMachine.nextInboundMessage()) + XCTAssertEqual(receivedMessage, [42, 42]) + + XCTAssertNil(stateMachine.nextInboundMessage()) } } + +final class GRPCStreamServerStateMachineTests: XCTestCase { + private func makeServerStateMachine() -> GRPCStreamStateMachine { + return GRPCStreamStateMachine(configuration: .server(maximumPayloadSize: 100, supportedCompressionAlgorithms: []), skipAssertions: true) + } + +} From 1e78321e1330f0d575b234b081ea987571b97d83 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Mon, 12 Feb 2024 17:42:16 +0000 Subject: [PATCH 10/51] Tests for server side --- .../GRPCStreamStateMachine.swift | 156 +-- .../GRPCStreamStateMachineTests.swift | 1023 ++++++++++++++++- 2 files changed, 1108 insertions(+), 71 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 883cdaa4a..2a4557b44 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -156,6 +156,14 @@ enum GRPCStreamStateMachineState { self.decompressor = previousState.decompressor self.inboundMessageBuffer = previousState.inboundMessageBuffer } + + init(previousState: ClientClosedServerIdleState) { + self.framer = previousState.framer + self.compressor = previousState.compressor + self.deframer = previousState.deframer + self.decompressor = previousState.decompressor + self.inboundMessageBuffer = previousState.inboundMessageBuffer + } } struct ClientClosedServerClosedState { @@ -461,28 +469,36 @@ extension GRPCStreamStateMachine { throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Client cannot be idle if server is sending initial metadata: it must have opened.") case .clientOpenServerIdle(let state): self.state = .clientOpenServerOpen(.init(previousState: state)) - case .clientOpenServerOpen: - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server has already sent initial metadata.") - case .clientOpenServerClosed: + case .clientOpenServerClosed, .clientClosedServerClosed: throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server cannot send metadata if closed.") - case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("No point in sending initial metadata if client is closed.") + case .clientClosedServerIdle(let state): + self.state = .clientClosedServerOpen(.init(previousState: state)) + case .clientOpenServerOpen, .clientClosedServerOpen: + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server has already sent initial metadata.") + } } mutating func send(message: [UInt8], endStream: Bool) throws { switch self.state { - case .clientIdleServerIdle: - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Cannot send a message when idle.") - case .clientOpenServerIdle: + case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server must have sent initial metadata before sending a message.") case .clientOpenServerOpen(var state): state.framer.append(message) - self.state = .clientOpenServerOpen(state) - case .clientOpenServerClosed: + if endStream { + self.state = .clientOpenServerClosed(.init(previousState: state)) + } else { + self.state = .clientOpenServerOpen(state) + } + case .clientClosedServerOpen(var state): + state.framer.append(message) + if endStream { + self.state = .clientClosedServerClosed(.init(previousState: state)) + } else { + self.state = .clientClosedServerOpen(state) + } + case .clientOpenServerClosed, .clientClosedServerClosed: throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server can't send a message if it's closed.") - case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server can't send a message to a closed client.") } } @@ -490,22 +506,20 @@ extension GRPCStreamStateMachine { // Close the server. switch self.state { case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server can't send anything if idle.") + throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server can't send status if idle.") case .clientOpenServerOpen(let state): self.state = .clientOpenServerClosed(.init(previousState: state)) - case .clientOpenServerClosed: - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server is closed, can't send anything else.") case .clientClosedServerOpen(let state): state.compressor?.end() state.decompressor?.end() self.state = .clientClosedServerClosed(.init(previousState: state)) - case .clientClosedServerClosed: + case .clientOpenServerClosed, .clientClosedServerClosed: throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server can't send anything if closed.") } } mutating func receive(metadata: Metadata, endStream: Bool) throws -> OnMetadataReceived { - if endStream { + if endStream, case .clientIdleServerIdle = self.state { throw assertionFailureAndCreateRPCErrorOnFailedPrecondition( """ Client should have opened before ending the stream: @@ -514,51 +528,37 @@ extension GRPCStreamStateMachine { ) } - guard let contentType = metadata.contentType else { - throw RPCError(code: .invalidArgument, message: "Invalid or empty content-type.") - } - - guard let endpoint = metadata.endpoint else { - throw RPCError(code: .unimplemented, message: "No :path header has been set.") - } - - // TODO: Should we verify the RPCRouter can handle this endpoint here, - // or should we verify that in the handler? - - let encodingValues = metadata[stringValues: "grpc-encoding"] - var encodingValuesIterator = encodingValues.makeIterator() - if let rawEncoding = encodingValuesIterator.next() { - guard encodingValuesIterator.next() == nil else { - throw RPCError( - code: .invalidArgument, - message: "grpc-encoding must contain no more than one value" - ) + switch self.state { + case .clientIdleServerIdle(let state): + self.state = .clientOpenServerIdle(.init( + previousState: state, + compressionAlgorithm: metadata.encoding + )) + + guard let contentType = metadata.contentType else { + throw RPCError(code: .invalidArgument, message: "Invalid or empty content-type.") } - guard let encoding = Encoding(rawValue: rawEncoding) else { - let status = Status( - code: .unimplemented, - message: "\(rawEncoding) compression is not supported; supported algorithms are listed in grpc-accept-encoding" - ) - let trailers = Metadata(dictionaryLiteral: ( - "grpc-accept-encoding", - .string(self.supportedCompressionAlgorithms - .map({ $0.rawValue }) - .joined(separator: ",") - ) - )) - return .reject(status: status, trailers: trailers) + + guard let endpoint = metadata.endpoint else { + throw RPCError(code: .unimplemented, message: "No :path header has been set.") } - - guard self.supportedCompressionAlgorithms.contains(where: { $0 == encoding }) else { - if self.supportedCompressionAlgorithms.isEmpty { + + // TODO: Should we verify the RPCRouter can handle this endpoint here, + // or should we verify that in the handler? + + let encodingValues = metadata[stringValues: "grpc-encoding"] + var encodingValuesIterator = encodingValues.makeIterator() + if let rawEncoding = encodingValuesIterator.next() { + guard encodingValuesIterator.next() == nil else { throw RPCError( - code: .unimplemented, - message: "Compression is not supported" + code: .invalidArgument, + message: "grpc-encoding must contain no more than one value" ) - } else { + } + guard let encoding = Encoding(rawValue: rawEncoding) else { let status = Status( code: .unimplemented, - message: "\(encoding) compression is not supported; supported algorithms are listed in grpc-accept-encoding" + message: "\(rawEncoding) compression is not supported; supported algorithms are listed in grpc-accept-encoding" ) let trailers = Metadata(dictionaryLiteral: ( "grpc-accept-encoding", @@ -569,17 +569,29 @@ extension GRPCStreamStateMachine { )) return .reject(status: status, trailers: trailers) } - } - - // All good - } - switch self.state { - case .clientIdleServerIdle(let state): - self.state = .clientOpenServerIdle(.init( - previousState: state, - compressionAlgorithm: metadata.encoding - )) + guard self.supportedCompressionAlgorithms.contains(where: { $0 == encoding }) else { + if self.supportedCompressionAlgorithms.isEmpty { + throw RPCError( + code: .unimplemented, + message: "Compression is not supported" + ) + } else { + let status = Status( + code: .unimplemented, + message: "\(encoding) compression is not supported; supported algorithms are listed in grpc-accept-encoding" + ) + let trailers = Metadata(dictionaryLiteral: ( + "grpc-accept-encoding", + .string(self.supportedCompressionAlgorithms + .map({ $0.rawValue }) + .joined(separator: ",") + ) + )) + return .reject(status: status, trailers: trailers) + } + } + } return .doNothing case .clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed: throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Client shouldn't have sent metadata twice.") @@ -629,9 +641,10 @@ extension GRPCStreamStateMachine { let response = try state.framer.next(compressor: state.compressor) self.state = .clientOpenServerOpen(state) return response - case .clientClosedServerOpen: - // No point in sending response if client is closed: do nothing. - return nil + case .clientClosedServerOpen(var state): + let response = try state.framer.next(compressor: state.compressor) + self.state = .clientClosedServerOpen(state) + return response case .clientOpenServerClosed, .clientClosedServerClosed: throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Can't send response if server is closed.") } @@ -647,13 +660,16 @@ extension GRPCStreamStateMachine { let request = state.inboundMessageBuffer.pop() self.state = .clientOpenServerOpen(state) return request + case .clientOpenServerClosed(var state): + let request = state.inboundMessageBuffer.pop() + self.state = .clientOpenServerClosed(state) + return request case .clientClosedServerOpen(var state): let request = state.inboundMessageBuffer.pop() self.state = .clientClosedServerOpen(state) return request case .clientClosedServerIdle, .clientIdleServerIdle, - .clientOpenServerClosed, .clientClosedServerClosed: return nil } diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index 5d27eb2e6..a9b63a895 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -1093,8 +1093,1029 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { } final class GRPCStreamServerStateMachineTests: XCTestCase { + private let testMetadata: Metadata = [ + ":path": "test/test", + "content-type": "application/grpc" + ] + private let testMetadataWithDeflateCompression: Metadata = [ + ":path": "test/test", + "content-type": "application/grpc", + "grpc-encoding": "deflate" + ] + private let testMetadataWithoutContentType: Metadata = [":path": "test/test"] + private let testMetadataWithInvalidContentType: Metadata = ["content-type": "invalid/invalid"] + private let testMetadataWithoutEndpoint: Metadata = ["content-type": "application/grpc"] + private func makeServerStateMachine() -> GRPCStreamStateMachine { - return GRPCStreamStateMachine(configuration: .server(maximumPayloadSize: 100, supportedCompressionAlgorithms: []), skipAssertions: true) + GRPCStreamStateMachine( + configuration: .server( + maximumPayloadSize: 100, + supportedCompressionAlgorithms: [] + ), + skipAssertions: true + ) + } + + private func makeServerStateMachineWithCompression() -> GRPCStreamStateMachine { + GRPCStreamStateMachine( + configuration: .server( + maximumPayloadSize: 100, + supportedCompressionAlgorithms: [.deflate] + ), + skipAssertions: true + ) + } + + // - MARK: Send Metadata + + func testSendMetadataWhenClientIdleAndServerIdle() throws { + var stateMachine = makeServerStateMachine() + + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.send(metadata: .init())) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client cannot be idle if server is sending initial metadata: it must have opened.") + } + } + + func testSendMetadataWhenClientOpenAndServerIdle() throws { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + } + + func testSendMetadataWhenClientOpenAndServerOpen() throws { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + // Open server + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + + // Try sending metadata again: should throw + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.send(metadata: .init())) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Server has already sent initial metadata.") + } + } + + func testSendMetadataWhenClientOpenAndServerClosed() throws { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + // Open server + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + + // Close server + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + + // Try sending metadata again: should throw + XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Server cannot send metadata if closed.") + } + } + + func testSendMetadataWhenClientClosedAndServerIdle() throws { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + // Close client + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + + // We should be allowed to send initial metadata if client is closed: + // client may be finished sending request but may still be awaiting response. + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + } + + func testSendMetadataWhenClientClosedAndServerOpen() throws { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + // Open server + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + + // Close client + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + + // Try sending metadata again: should throw + XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Server has already sent initial metadata.") + } + } + + func testSendMetadataWhenClientClosedAndServerClosed() throws { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + // Open server + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + + // Close client + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + + // Close server + XCTAssertNoThrow(try stateMachine.send(message: .init(), endStream: true)) + + // Try sending metadata again: should throw + XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Server cannot send metadata if closed.") + } + } + + // - MARK: Send Message + + func testSendMessageWhenClientIdleAndServerIdle() { + var stateMachine = makeServerStateMachine() + + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.send(message: [], endStream: false)) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Server must have sent initial metadata before sending a message.") + } + } + + func testSendMessageWhenClientOpenAndServerIdle() { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + // Now send a message + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.send(message: [], endStream: false)) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Server must have sent initial metadata before sending a message.") + } + } + + func testSendMessageWhenClientOpenAndServerOpen() { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + // Open server + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + + // Now send a message + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: false)) } + func testSendMessageWhenClientOpenAndServerClosed() { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + // Open server + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + + // Close server + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + + // Try sending another message: it should fail + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.send(message: [], endStream: false)) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Server can't send a message if it's closed.") + } + } + + func testSendMessageWhenClientClosedAndServerIdle() { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + // Close client + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.send(message: [], endStream: false)) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Server must have sent initial metadata before sending a message.") + } + } + + func testSendMessageWhenClientClosedAndServerOpen() { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + // Open server + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + + // Close client + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + + // Try sending a message: even though client is closed, we should send it + // because it may be expecting a response. + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: false)) + } + + func testSendMessageWhenClientClosedAndServerClosed() { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + // Open server + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + + // Close client + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + + // Close server + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + + // Try sending another message: it should fail + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.send(message: [], endStream: false)) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Server can't send a message if it's closed.") + } + } + + // - MARK: Send Status and Trailers + + func testSendStatusAndTrailersWhenClientIdleAndServerIdle() { + var stateMachine = makeServerStateMachine() + + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.send(status: "test", trailingMetadata: .init())) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Server can't send status if idle.") + } + } + + func testSendStatusAndTrailersWhenClientOpenAndServerIdle() { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.send(status: "test", trailingMetadata: .init())) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Server can't send status if idle.") + } + } + + func testSendStatusAndTrailersWhenClientOpenAndServerOpen() { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + // Open server + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + + XCTAssertNoThrow(try stateMachine.send(status: "test", trailingMetadata: .init())) + + // Try sending another message: it should fail because server is now closed. + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.send(message: [], endStream: false)) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Server can't send a message if it's closed.") + } + } + + func testSendStatusAndTrailersWhenClientOpenAndServerClosed() { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + // Open server + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + + // Close server + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.send(status: "test", trailingMetadata: .init())) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Server can't send anything if closed.") + } + } + + func testSendStatusAndTrailersWhenClientClosedAndServerIdle() { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + // Close client + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.send(status: "test", trailingMetadata: .init())) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Server can't send status if idle.") + } + } + + func testSendStatusAndTrailersWhenClientClosedAndServerOpen() { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + // Open server + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + + // Close client + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + + // Client is closed but may still be awaiting response, so we should be able to send it. + XCTAssertNoThrow(try stateMachine.send(status: "test", trailingMetadata: .init())) + } + + func testSendStatusAndTrailersWhenClientClosedAndServerClosed() { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + // Open server + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + + // Close client + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + + // Close server + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.send(status: "test", trailingMetadata: .init())) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Server can't send anything if closed.") + } + } + + // - MARK: Receive metadata + + func testReceiveMetadataWhenClientIdleAndServerIdle() throws { + var stateMachine = makeServerStateMachine() + + let action = try stateMachine.receive(metadata: self.testMetadata, endStream: false) + guard case .doNothing = action else { + XCTFail("Expected action to be doNothing") + return + } + } + + func testReceiveMetadataWhenClientIdleAndServerIdle_WithEndStream() { + var stateMachine = makeServerStateMachine() + + // If endStream is set, we should fail, because the client can only close by + // sending a message with endStream set. If they send metadata it has to be + // to open the stream (initial metadata). + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.receive(metadata: self.testMetadata, endStream: true)) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual( + error.message, + """ + Client should have opened before ending the stream: + stream shouldn't have been closed when sending initial metadata. + """ + ) + } + } + + func testReceiveMetadataWhenClientIdleAndServerIdle_MissingContentType() { + var stateMachine = makeServerStateMachine() + + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.receive(metadata: self.testMetadataWithoutContentType, endStream: false)) { error in + XCTAssertEqual(error.code, .invalidArgument) + XCTAssertEqual(error.message, "Invalid or empty content-type.") + } + } + + func testReceiveMetadataWhenClientIdleAndServerIdle_InvalidContentType() { + var stateMachine = makeServerStateMachine() + + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.receive(metadata: self.testMetadataWithInvalidContentType, endStream: false)) { error in + XCTAssertEqual(error.code, .invalidArgument) + XCTAssertEqual(error.message, "Invalid or empty content-type.") + } + } + + func testReceiveMetadataWhenClientIdleAndServerIdle_MissingPath() { + var stateMachine = makeServerStateMachine() + + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.receive(metadata: self.testMetadataWithoutEndpoint, endStream: false)) { error in + XCTAssertEqual(error.code, .unimplemented) + XCTAssertEqual(error.message, "No :path header has been set.") + } + } + + func testReceiveMetadataWhenClientIdleAndServerIdle_Encoding() { + var noCompressionStateMachine = makeServerStateMachine() + + // Try opening client if no compression has been configured in the server: + // should fail. + XCTAssertThrowsError(ofType: RPCError.self, + try noCompressionStateMachine.receive(metadata: self.testMetadataWithDeflateCompression, endStream: false)) { error in + XCTAssertEqual(error.code, .unimplemented) + XCTAssertEqual(error.message, "Compression is not supported") + } + + var stateMachine = makeServerStateMachineWithCompression() + //TODO: add tests for encoding validation + } + + func testReceiveMetadataWhenClientOpenAndServerIdle() throws { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + // Try receiving initial metadata again - should fail + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.receive(metadata: self.testMetadata, endStream: false)) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client shouldn't have sent metadata twice.") + } + } + + func testReceiveMetadataWhenClientOpenAndServerOpen() throws { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + // Open server + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.receive(metadata: self.testMetadata, endStream: false)) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client shouldn't have sent metadata twice.") + } + } + + func testReceiveMetadataWhenClientOpenAndServerClosed() { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + // Open server + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + + // Close server + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.receive(metadata: self.testMetadata, endStream: false)) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client shouldn't have sent metadata twice.") + } + } + + func testReceiveMetadataWhenClientClosedAndServerIdle() { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + // Close client + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.receive(metadata: self.testMetadata, endStream: false)) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client can't have sent metadata if closed.") + } + } + + func testReceiveMetadataWhenClientClosedAndServerOpen() { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + // Open server + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + + // Close client + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + + + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.receive(metadata: self.testMetadata, endStream: false)) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client can't have sent metadata if closed.") + } + } + + func testReceiveMetadataWhenClientClosedAndServerClosed() { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + // Open server + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + + // Close client + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + + // Close server + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.receive(metadata: self.testMetadata, endStream: false)) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client can't have sent metadata if closed.") + } + } + + // - MARK: Receive message + + func testReceiveMessageWhenClientIdleAndServerIdle() { + var stateMachine = makeServerStateMachine() + + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.receive(message: .init(), endStream: false)) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Can't have received a message if client is idle.") + } + } + + func testReceiveMessageWhenClientOpenAndServerIdle() { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + // Receive messages successfully: the second one should close client. + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + + // Verify client is now closed + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.receive(message: .init(), endStream: false)) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client can't send a message if closed.") + } + } + + func testReceiveMessageWhenClientOpenAndServerOpen() throws { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + // Open server + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + + // Receive messages successfully: the second one should close client. + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + + // Verify client is now closed + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.receive(message: .init(), endStream: false)) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client can't send a message if closed.") + } + } + + func testReceiveMessageWhenClientOpenAndServerClosed() { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + // Open server + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + + // Close server + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + + // Client is not done sending request, don't fail. + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: false)) + } + + func testReceiveMessageWhenClientClosedAndServerIdle() { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + // Close client + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.receive(message: .init(), endStream: false)) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client can't send a message if closed.") + } + } + + func testReceiveMessageWhenClientClosedAndServerOpen() { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + // Open server + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + + // Close client + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.receive(message: .init(), endStream: false)) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client can't send a message if closed.") + } + } + + func testReceiveMessageWhenClientClosedAndServerClosed() { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + // Open server + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + + // Close client + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + + // Close server + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.receive(message: .init(), endStream: false)) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Client can't send a message if closed.") + } + } + + // - MARK: Next outbound message + + func testNextOutboundMessageWhenClientIdleAndServerIdle() { + var stateMachine = makeServerStateMachine() + + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.nextOutboundMessage()) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Server is not open yet.") + } + } + + func testNextOutboundMessageWhenClientOpenAndServerIdle() throws { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.nextOutboundMessage()) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Server is not open yet.") + } + } + + func testNextOutboundMessageWhenClientOpenAndServerIdle_WithCompression() throws { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.nextOutboundMessage()) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Server is not open yet.") + } + } + + func testNextOutboundMessageWhenClientOpenAndServerOpen() throws { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + // Open server + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + + var request = try stateMachine.nextOutboundMessage() + XCTAssertNil(request) + + XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) + request = try XCTUnwrap(stateMachine.nextOutboundMessage()) + + let expectedBytes: [UInt8] = [ + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42 // original message + ] + XCTAssertEqual(Array(buffer: request!), expectedBytes) + } + + func testNextOutboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { + var stateMachine = makeServerStateMachineWithCompression() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadataWithDeflateCompression, endStream: false)) + + // Open server + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + + var request = try stateMachine.nextOutboundMessage() + XCTAssertNil(request) + + let originalMessage = [UInt8]([42, 42, 43, 43]) + XCTAssertNoThrow(try stateMachine.send(message: originalMessage, endStream: false)) + request = try XCTUnwrap(stateMachine.nextOutboundMessage()) + + var framer = GRPCMessageFramer() + let compressor = Zlib.Compressor(method: .deflate) + defer { compressor.end() } + framer.append(originalMessage) + + let framedMessage = try XCTUnwrap(framer.next(compressor: compressor)) + let expectedBytes = Array(buffer: framedMessage) + XCTAssertEqual(Array(buffer: request!), expectedBytes) + } + + func testNextOutboundMessageWhenClientOpenAndServerClosed() throws { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + // Open server + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + + // Close server + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.nextOutboundMessage()) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Can't send response if server is closed.") + } + } + + func testNextOutboundMessageWhenClientClosedAndServerIdle() throws { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + // Close client + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.nextOutboundMessage()) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Server is not open yet.") + } + } + + func testNextOutboundMessageWhenClientClosedAndServerOpen() throws { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + // Open server + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + + // Send a message + XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) + + // Close client + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + + // Send another message + XCTAssertNoThrow(try stateMachine.send(message: [43, 43], endStream: false)) + + // Make sure that getting the next outbound message _does_ return the message + // we have enqueued. + let request = try XCTUnwrap(stateMachine.nextOutboundMessage()) + let expectedBytes: [UInt8] = [ + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42, // original message + // End of first message - beginning of second + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 43, 43 // original message + ] + XCTAssertEqual(Array(buffer: request), expectedBytes) + + // And then make sure that nothing else is returned anymore + XCTAssertNil(try stateMachine.nextOutboundMessage()) + } + + func testNextOutboundMessageWhenClientClosedAndServerClosed() { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + // Open server + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + + // Send a message + XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) + + // Close client + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + + // Close server + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + + // Even though we have enqueued a message, don't send it, because the server + // is closed. + XCTAssertThrowsError(ofType: RPCError.self, + try stateMachine.nextOutboundMessage()) { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "Can't send response if server is closed.") + } + } + + // - MARK: Next inbound message + + func testNextInboundMessageWhenClientIdleAndServerIdle() { + var stateMachine = makeServerStateMachine() + XCTAssertNil(stateMachine.nextInboundMessage()) + } + + func testNextInboundMessageWhenClientOpenAndServerIdle() { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + XCTAssertNil(stateMachine.nextInboundMessage()) + } + + func testNextInboundMessageWhenClientOpenAndServerOpen() throws { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + // Open server + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + + let receivedBytes = ByteBuffer(bytes: [ + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42 // original message + ]) + try stateMachine.receive(message: receivedBytes, endStream: false) + + let receivedMessage = try XCTUnwrap(stateMachine.nextInboundMessage()) + XCTAssertEqual(receivedMessage, [42, 42]) + + XCTAssertNil(stateMachine.nextInboundMessage()) + } + + func testNextInboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { + var stateMachine = makeServerStateMachineWithCompression() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadataWithDeflateCompression, endStream: false)) + + // Open server + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + + let originalMessage = [UInt8]([42, 42, 43, 43]) + var framer = GRPCMessageFramer() + let compressor = Zlib.Compressor(method: .deflate) + defer { compressor.end() } + framer.append(originalMessage) + let receivedBytes = try framer.next(compressor: compressor)! + + try stateMachine.receive(message: receivedBytes, endStream: false) + + let receivedMessage = try XCTUnwrap(stateMachine.nextInboundMessage()) + XCTAssertEqual(receivedMessage, originalMessage) + + XCTAssertNil(stateMachine.nextInboundMessage()) + } + + func testNextInboundMessageWhenClientOpenAndServerClosed() throws { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + // Open server + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + + let receivedBytes = ByteBuffer(bytes: [ + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42 // original message + ]) + try stateMachine.receive(message: receivedBytes, endStream: false) + + // Close server + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + + let receivedMessage = try XCTUnwrap(stateMachine.nextInboundMessage()) + XCTAssertEqual(receivedMessage, [42, 42]) + + XCTAssertNil(stateMachine.nextInboundMessage()) + } + + func testNextInboundMessageWhenClientClosedAndServerIdle() { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + // Close client + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + + XCTAssertNil(stateMachine.nextInboundMessage()) + } + + func testNextInboundMessageWhenClientClosedAndServerOpen() throws { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + // Open server + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + + let receivedBytes = ByteBuffer(bytes: [ + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42 // original message + ]) + try stateMachine.receive(message: receivedBytes, endStream: false) + + // Close client + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + + // Even though the client is closed, because the server received a message + // while it was still open, we must get the message now. + let receivedMessage = try XCTUnwrap(stateMachine.nextInboundMessage()) + XCTAssertEqual(receivedMessage, [42, 42]) + + XCTAssertNil(stateMachine.nextInboundMessage()) + } + + func testNextInboundMessageWhenClientClosedAndServerClosed() throws { + var stateMachine = makeServerStateMachine() + + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + + // Open server + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + + let receivedBytes = ByteBuffer(bytes: [ + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42 // original message + ]) + try stateMachine.receive(message: receivedBytes, endStream: false) + + // Close server + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + + // Close client + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + + // Even though the client and server are closed, because the server received + // a message while the client was still open, we must get the message now. + let receivedMessage = try XCTUnwrap(stateMachine.nextInboundMessage()) + XCTAssertEqual(receivedMessage, [42, 42]) + + XCTAssertNil(stateMachine.nextInboundMessage()) + } + } From 89f3ea48b211d63b448e9db48fb24d657df3d56a Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Tue, 13 Feb 2024 16:12:10 +0000 Subject: [PATCH 11/51] Multiple PR changes --- Sources/GRPCCore/Encoding.swift | 21 - Sources/GRPCCore/Internal/Metadata+GRPC.swift | 17 +- .../Compression/CompressionAlgorithm.swift | 52 +++ .../GRPCStreamStateMachine.swift | 434 ++++++++++++------ .../GRPCHTTP2Core/Internal/ContentType.swift | 19 +- Sources/GRPCHTTP2Core/MessageEncoding.swift | 8 + .../GRPCStreamStateMachineTests.swift | 183 ++++---- 7 files changed, 453 insertions(+), 281 deletions(-) delete mode 100644 Sources/GRPCCore/Encoding.swift create mode 100644 Sources/GRPCHTTP2Core/Compression/CompressionAlgorithm.swift create mode 100644 Sources/GRPCHTTP2Core/MessageEncoding.swift diff --git a/Sources/GRPCCore/Encoding.swift b/Sources/GRPCCore/Encoding.swift deleted file mode 100644 index eef54eb3a..000000000 --- a/Sources/GRPCCore/Encoding.swift +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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. - */ - -public enum Encoding: String, Equatable { - case identity - case deflate - case gzip -} diff --git a/Sources/GRPCCore/Internal/Metadata+GRPC.swift b/Sources/GRPCCore/Internal/Metadata+GRPC.swift index 13e9db122..9bff423e3 100644 --- a/Sources/GRPCCore/Internal/Metadata+GRPC.swift +++ b/Sources/GRPCCore/Internal/Metadata+GRPC.swift @@ -38,7 +38,7 @@ extension Metadata { } @inlinable - public var timeout: Duration? { + var timeout: Duration? { get { self.firstString(forKey: .timeout).flatMap { Timeout(decoding: $0)?.duration } } @@ -50,27 +50,12 @@ extension Metadata { } } } - - @inlinable - public var encoding: Encoding? { - get { - self.firstString(forKey: .encoding).flatMap { Encoding(rawValue: $0) } - } - set { - if let newValue { - self.replaceOrAddString(newValue.rawValue, forKey: .encoding) - } else { - self.removeAllValues(forKey: .encoding) - } - } - } } extension Metadata { @usableFromInline enum GRPCKey: String, Sendable, Hashable { case timeout = "grpc-timeout" - case encoding = "grpc-encoding" case retryPushbackMs = "grpc-retry-pushback-ms" case previousRPCAttempts = "grpc-previous-rpc-attempts" } diff --git a/Sources/GRPCHTTP2Core/Compression/CompressionAlgorithm.swift b/Sources/GRPCHTTP2Core/Compression/CompressionAlgorithm.swift new file mode 100644 index 000000000..e080ba2e1 --- /dev/null +++ b/Sources/GRPCHTTP2Core/Compression/CompressionAlgorithm.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. + */ + +/// Supported message compression algorithms. +/// +/// These algorithms are indicated in the "grpc-encoding" header. As such, a lack of "grpc-encoding" +/// header indicates that there is no message compression. +struct CompressionAlgorithm: Equatable, Sendable { + /// Identity compression; "no" compression but indicated via the "grpc-encoding" header. + public static let identity = CompressionAlgorithm(.identity) + public static let deflate = CompressionAlgorithm(.deflate) + public static let gzip = CompressionAlgorithm(.gzip) + + // The order here is important: most compression to least. + public static let all: [CompressionAlgorithm] = [.gzip, .deflate, .identity] + + public var name: String { + return self.algorithm.rawValue + } + + internal enum Algorithm: String { + case identity + case deflate + case gzip + } + + internal let algorithm: Algorithm + + private init(_ algorithm: Algorithm) { + self.algorithm = algorithm + } + + internal init?(rawValue: String) { + guard let algorithm = Algorithm(rawValue: rawValue) else { + return nil + } + self.algorithm = algorithm + } +} diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 2a4557b44..43db83c75 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -37,10 +37,13 @@ fileprivate protocol GRPCStreamStateMachineProtocol { } enum GRPCStreamStateMachineConfiguration { - case client(maximumPayloadSize: Int) + case client( + maximumPayloadSize: Int, + supportedCompressionAlgorithms: [CompressionAlgorithm] + ) case server( maximumPayloadSize: Int, - supportedCompressionAlgorithms: [Encoding] + supportedCompressionAlgorithms: [CompressionAlgorithm] ) } @@ -57,30 +60,51 @@ enum GRPCStreamStateMachineState { let maximumPayloadSize: Int } + enum DecompressionConfiguration { + case decompressionNotYetKnown + case decompression(CompressionAlgorithm?) + } + struct ClientOpenServerIdleState { + let maximumPayloadSize: Int var framer: GRPCMessageFramer var compressor: Zlib.Compressor? - let deframer: NIOSingleStepByteToMessageProcessor + // The deframer must be optional because the client will not have one configured + // until the server opens and sends a grpc-encoding header. + // It will be present for the server though, because even though it's idle, + // it can still receive compressed messages from the client. + let deframer: NIOSingleStepByteToMessageProcessor? var decompressor: Zlib.Decompressor? var inboundMessageBuffer: OneOrManyQueue<[UInt8]> init( previousState: ClientIdleServerIdleState, - compressionAlgorithm: Encoding? + compressionAlgorithm: CompressionAlgorithm?, + decompressionConfiguration: DecompressionConfiguration ) { + self.maximumPayloadSize = previousState.maximumPayloadSize + if let zlibMethod = Zlib.Method(encoding: compressionAlgorithm) { self.compressor = Zlib.Compressor(method: zlibMethod) - self.decompressor = Zlib.Decompressor(method: zlibMethod) } - self.framer = GRPCMessageFramer() - let decoder = GRPCMessageDeframer( - maximumPayloadSize: previousState.maximumPayloadSize, - decompressor: self.decompressor - ) - self.deframer = NIOSingleStepByteToMessageProcessor(decoder) + + // TODO: we should check here or in a `MessageEncoder` (instead of the state machine) + // that the server supports the given encoding - otherwise return the corresponding response. + if case .decompression(let decompressionAlgorithm) = decompressionConfiguration { + if let zlibMethod = Zlib.Method(encoding: decompressionAlgorithm) { + self.decompressor = Zlib.Decompressor(method: zlibMethod) + } + let decoder = GRPCMessageDeframer( + maximumPayloadSize: previousState.maximumPayloadSize, + decompressor: self.decompressor + ) + self.deframer = NIOSingleStepByteToMessageProcessor(decoder) + } else { + self.deframer = nil + } self.inboundMessageBuffer = .init() } @@ -95,11 +119,29 @@ enum GRPCStreamStateMachineState { var inboundMessageBuffer: OneOrManyQueue<[UInt8]> - init(previousState: ClientOpenServerIdleState) { + init( + previousState: ClientOpenServerIdleState, + decompressionAlgorithm: CompressionAlgorithm? + ) { self.framer = previousState.framer self.compressor = previousState.compressor - self.deframer = previousState.deframer - self.decompressor = previousState.decompressor + + if let previousDeframer = previousState.deframer { + self.deframer = previousDeframer + self.decompressor = previousState.decompressor + } else { + // TODO: we should check here or in a `MessageEncoder` (instead of the state machine) + // that the client supports the given encoding - otherwise return the corresponding response. + if let zlibMethod = Zlib.Method(encoding: decompressionAlgorithm) { + self.decompressor = Zlib.Decompressor(method: zlibMethod) + } + let decoder = GRPCMessageDeframer( + maximumPayloadSize: previousState.maximumPayloadSize, + decompressor: self.decompressor + ) + self.deframer = NIOSingleStepByteToMessageProcessor(decoder) + } + self.inboundMessageBuffer = previousState.inboundMessageBuffer } } @@ -108,7 +150,7 @@ enum GRPCStreamStateMachineState { var framer: GRPCMessageFramer var compressor: Zlib.Compressor? - let deframer: NIOSingleStepByteToMessageProcessor + let deframer: NIOSingleStepByteToMessageProcessor? var decompressor: Zlib.Decompressor? var inboundMessageBuffer: OneOrManyQueue<[UInt8]> @@ -120,18 +162,34 @@ enum GRPCStreamStateMachineState { self.decompressor = previousState.decompressor self.inboundMessageBuffer = previousState.inboundMessageBuffer } + + init(previousState: ClientOpenServerIdleState) { + self.framer = previousState.framer + self.compressor = previousState.compressor + self.inboundMessageBuffer = previousState.inboundMessageBuffer + // The server went directly from idle to closed - this means it sent a + // trailers-only response: + // - if we're the client, the previous state was a nil deframer, but that + // is okay because we don't need a deframer as the server won't be sending + // any messages; + // - if we're the server, we'll keep whatever deframer we had. + self.deframer = previousState.deframer + self.decompressor = previousState.decompressor + } } struct ClientClosedServerIdleState { + let maximumPayloadSize: Int var framer: GRPCMessageFramer var compressor: Zlib.Compressor? - let deframer: NIOSingleStepByteToMessageProcessor + let deframer: NIOSingleStepByteToMessageProcessor? var decompressor: Zlib.Decompressor? var inboundMessageBuffer: OneOrManyQueue<[UInt8]> init(previousState: ClientOpenServerIdleState) { + self.maximumPayloadSize = previousState.maximumPayloadSize self.framer = previousState.framer self.compressor = previousState.compressor self.deframer = previousState.deframer @@ -157,22 +215,42 @@ enum GRPCStreamStateMachineState { self.inboundMessageBuffer = previousState.inboundMessageBuffer } - init(previousState: ClientClosedServerIdleState) { + init( + previousState: ClientClosedServerIdleState, + decompressionAlgorithm: CompressionAlgorithm? + ) { self.framer = previousState.framer self.compressor = previousState.compressor - self.deframer = previousState.deframer - self.decompressor = previousState.decompressor self.inboundMessageBuffer = previousState.inboundMessageBuffer + + if let previousDeframer = previousState.deframer { + self.deframer = previousDeframer + self.decompressor = previousState.decompressor + } else { + if let zlibMethod = Zlib.Method(encoding: decompressionAlgorithm) { + self.decompressor = Zlib.Decompressor(method: zlibMethod) + } + let decoder = GRPCMessageDeframer( + maximumPayloadSize: previousState.maximumPayloadSize, + decompressor: self.decompressor + ) + self.deframer = NIOSingleStepByteToMessageProcessor(decoder) + } } } struct ClientClosedServerClosedState { + // These are already deframed, so we don't need the deframer anymore. var inboundMessageBuffer: OneOrManyQueue<[UInt8]> init(previousState: ClientClosedServerOpenState) { self.inboundMessageBuffer = previousState.inboundMessageBuffer } + init(previousState: ClientClosedServerIdleState) { + self.inboundMessageBuffer = previousState.inboundMessageBuffer + } + init(previousState: ClientOpenServerClosedState) { self.inboundMessageBuffer = previousState.inboundMessageBuffer } @@ -188,8 +266,12 @@ struct GRPCStreamStateMachine { skipAssertions: Bool = false ) { switch configuration { - case .client(let maximumPayloadSize): - self._stateMachine = Client(maximumPayloadSize: maximumPayloadSize, skipAssertions: skipAssertions) + case .client(let maximumPayloadSize, let supportedCompressionAlgorithms): + self._stateMachine = Client( + maximumPayloadSize: maximumPayloadSize, + supportedCompressionAlgorithms: supportedCompressionAlgorithms, + skipAssertions: skipAssertions + ) case .server(let maximumPayloadSize, let supportedCompressionAlgorithms): self._stateMachine = Server( maximumPayloadSize: maximumPayloadSize, @@ -232,10 +314,16 @@ struct GRPCStreamStateMachine { extension GRPCStreamStateMachine { struct Client: GRPCStreamStateMachineProtocol { fileprivate var state: GRPCStreamStateMachineState + private let supportedCompressionAlgorithms: [CompressionAlgorithm] private let skipAssertions: Bool - init(maximumPayloadSize: Int, skipAssertions: Bool) { + init( + maximumPayloadSize: Int, + supportedCompressionAlgorithms: [CompressionAlgorithm], + skipAssertions: Bool + ) { self.state = .clientIdleServerIdle(.init(maximumPayloadSize: maximumPayloadSize)) + self.supportedCompressionAlgorithms = supportedCompressionAlgorithms self.skipAssertions = skipAssertions } @@ -243,7 +331,7 @@ extension GRPCStreamStateMachine { // Client sends metadata only when opening the stream. switch self.state { case .clientIdleServerIdle(let state): - guard metadata.endpoint != nil else { + guard metadata.path != nil else { throw RPCError( code: .invalidArgument, message: "Endpoint is missing: client cannot send initial metadata without it." @@ -252,12 +340,13 @@ extension GRPCStreamStateMachine { self.state = .clientOpenServerIdle(.init( previousState: state, - compressionAlgorithm: metadata.encoding + compressionAlgorithm: metadata.encoding, + decompressionConfiguration: .decompressionNotYetKnown )) case .clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed: - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Client is already open: shouldn't be sending metadata.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client is already open: shouldn't be sending metadata.") case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Client is closed: can't send metadata.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client is closed: can't send metadata.") } } @@ -265,7 +354,7 @@ extension GRPCStreamStateMachine { // Client sends message. switch self.state { case .clientIdleServerIdle: - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Client not yet open.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client not yet open.") case .clientOpenServerIdle(var state): state.framer.append(message) if endStream { @@ -287,12 +376,12 @@ extension GRPCStreamStateMachine { self.state = .clientClosedServerClosed(.init(previousState: state)) } case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Client is closed, cannot send a message.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client is closed, cannot send a message.") } } mutating func send(status: String, trailingMetadata: Metadata) throws { - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Client cannot send status and trailer.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client cannot send status and trailer.") } /// Returns the client's next request to the server. @@ -300,7 +389,7 @@ extension GRPCStreamStateMachine { mutating func nextOutboundMessage() throws -> ByteBuffer? { switch self.state { case .clientIdleServerIdle: - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Client is not open yet.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client is not open yet.") case .clientOpenServerIdle(var state): let request = try state.framer.next(compressor: state.compressor) self.state = .clientOpenServerIdle(state) @@ -309,9 +398,12 @@ extension GRPCStreamStateMachine { let request = try state.framer.next(compressor: state.compressor) self.state = .clientOpenServerOpen(state) return request - case .clientOpenServerClosed, .clientClosedServerClosed: - // Nothing to do: no point in sending request if server is closed. - return nil + case .clientOpenServerClosed(var state): + // Server may have closed but still be waiting for client messages, + // for example if it's a client-streaming RPC. + let request = try state.framer.next(compressor: state.compressor) + self.state = .clientOpenServerClosed(state) + return request case .clientClosedServerIdle(var state): let request = try state.framer.next(compressor: state.compressor) self.state = .clientClosedServerIdle(state) @@ -320,70 +412,76 @@ extension GRPCStreamStateMachine { let request = try state.framer.next(compressor: state.compressor) self.state = .clientClosedServerOpen(state) return request + case .clientClosedServerClosed: + // Nothing to do if both are closed. + return nil } } mutating func receive(metadata: Metadata, endStream: Bool) throws -> OnMetadataReceived { - // This is metadata received by the client from the server. - // It can be initial, which confirms that the server is now open; - // or an END_STREAM trailer, meaning the response is over. - if endStream { - try self.clientReceivedEndHeader() - } else { - try self.clientReceivedMetadata() - } - return .doNothing - } - - mutating func clientReceivedEndHeader() throws { switch self.state { case .clientIdleServerIdle: - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Client can't have received a stream end trailer if both client and server are idle.") - case .clientOpenServerIdle: - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server cannot have sent an end stream header if it is still idle.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server cannot have sent metadata if the client is idle.") + case .clientOpenServerIdle(let state): + if endStream { + // This is a trailers-only response: close server. + self.state = .clientOpenServerClosed(.init(previousState: state)) + } else { + self.state = .clientOpenServerOpen(.init( + previousState: state, + decompressionAlgorithm: metadata.encoding + )) + } case .clientOpenServerOpen(let state): - self.state = .clientOpenServerClosed(.init(previousState: state)) - case .clientOpenServerClosed: - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server is already closed, can't have received the end stream trailer twice.") - case .clientClosedServerIdle: - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server cannot have sent end stream trailer if it is idle.") + if endStream { + self.state = .clientOpenServerClosed(.init(previousState: state)) + } else { + // This state is valid: server can send trailing metadata without END_STREAM + // set, and follow it with an empty message frame where the flag *is* set. + () + // TODO: I believe we should set some flag in the state to signal that + // we're expecting an empty data frame with END_STREAM set; otherwise, + // we could get an infinite number of metadata frames from the server - + // not sure this should be allowed. + } + case .clientClosedServerIdle(let state): + if endStream { + // This is a trailers-only response. + self.state = .clientClosedServerClosed(.init(previousState: state)) + } else { + self.state = .clientClosedServerOpen(.init( + previousState: state, + decompressionAlgorithm: metadata.encoding + )) + } case .clientClosedServerOpen(let state): - state.compressor?.end() - state.decompressor?.end() - self.state = .clientClosedServerClosed(.init(previousState: state)) - case .clientClosedServerClosed: - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server cannot have sent end stream trailer if it is already closed.") - } - } - - mutating func clientReceivedMetadata() throws { - switch self.state { - case .clientIdleServerIdle: - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server cannot have sent metadata if the client is idle.") - case .clientOpenServerIdle(let state): - self.state = .clientOpenServerOpen(.init(previousState: state)) - case .clientOpenServerOpen: - // This state is valid: server can send trailing metadata without END_STREAM - // set, and follow it with an empty message frame where the flag *is* set. - () - // TODO: I believe we should set some flag in the state to signal that - // we're expecting an empty data frame with END_STREAM set; otherwise, - // we could get an infinite number of metadata frames from the server - - // not sure this should be allowed. - case .clientOpenServerClosed: - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server is closed, nothing could have been sent.") - case .clientClosedServerClosed, .clientClosedServerIdle, .clientClosedServerOpen: - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Client is closed, shouldn't have received anything.") + if endStream { + state.compressor?.end() + state.decompressor?.end() + self.state = .clientClosedServerClosed(.init(previousState: state)) + } else { + // This state is valid: server can send trailing metadata without END_STREAM + // set, and follow it with an empty message frame where the flag *is* set. + () + // TODO: I believe we should set some flag in the state to signal that + // we're expecting an empty data frame with END_STREAM set; otherwise, + // we could get an infinite number of metadata frames from the server - + // not sure this should be allowed. + } + case .clientOpenServerClosed, .clientClosedServerClosed: + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server is closed, nothing could have been sent.") } + + return .doNothing } mutating func receive(message: ByteBuffer, endStream: Bool) throws { // This is a message received by the client, from the server. switch self.state { case .clientIdleServerIdle: - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Cannot have received anything from server if client is not yet open.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Cannot have received anything from server if client is not yet open.") case .clientOpenServerIdle, .clientClosedServerIdle: - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server cannot have sent a message before sending the initial metadata.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server cannot have sent a message before sending the initial metadata.") case .clientOpenServerOpen(var state): try state.deframer.process(buffer: message) { deframedMessage in state.inboundMessageBuffer.append(deframedMessage) @@ -405,9 +503,9 @@ extension GRPCStreamStateMachine { self.state = .clientClosedServerOpen(state) } case .clientOpenServerClosed: - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Cannot have received anything from a closed server.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Cannot have received anything from a closed server.") case .clientClosedServerClosed: - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Shouldn't have received anything if both client and server are closed.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Shouldn't have received anything if both client and server are closed.") } } @@ -429,18 +527,18 @@ extension GRPCStreamStateMachine { let message = state.inboundMessageBuffer.pop() self.state = .clientClosedServerClosed(state) return message - case .clientOpenServerIdle, - .clientIdleServerIdle, + case .clientIdleServerIdle, + .clientOpenServerIdle, .clientClosedServerIdle: return nil } } - private func assertionFailureAndCreateRPCErrorOnFailedPrecondition(_ message: String) -> RPCError { + private func assertionFailureAndCreateRPCErrorOnInternalError(_ message: String, line: UInt = #line) -> RPCError { if !self.skipAssertions { - assertionFailure(message) + assertionFailure(message, line: line) } - return RPCError(code: .failedPrecondition, message: message) + return RPCError(code: .internalError, message: message) } } } @@ -449,12 +547,12 @@ extension GRPCStreamStateMachine { extension GRPCStreamStateMachine { struct Server: GRPCStreamStateMachineProtocol { fileprivate var state: GRPCStreamStateMachineState - let supportedCompressionAlgorithms: [Encoding] + let supportedCompressionAlgorithms: [CompressionAlgorithm] private let skipAssertions: Bool init( maximumPayloadSize: Int, - supportedCompressionAlgorithms: [Encoding], + supportedCompressionAlgorithms: [CompressionAlgorithm], skipAssertions: Bool ) { self.state = .clientIdleServerIdle(.init(maximumPayloadSize: maximumPayloadSize)) @@ -466,15 +564,21 @@ extension GRPCStreamStateMachine { // Server sends initial metadata. This transitions server to open. switch self.state { case .clientIdleServerIdle: - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Client cannot be idle if server is sending initial metadata: it must have opened.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client cannot be idle if server is sending initial metadata: it must have opened.") case .clientOpenServerIdle(let state): - self.state = .clientOpenServerOpen(.init(previousState: state)) + self.state = .clientOpenServerOpen(.init( + previousState: state, + decompressionAlgorithm: metadata.encoding + )) case .clientOpenServerClosed, .clientClosedServerClosed: - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server cannot send metadata if closed.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server cannot send metadata if closed.") case .clientClosedServerIdle(let state): - self.state = .clientClosedServerOpen(.init(previousState: state)) + self.state = .clientClosedServerOpen(.init( + previousState: state, + decompressionAlgorithm: metadata.encoding + )) case .clientOpenServerOpen, .clientClosedServerOpen: - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server has already sent initial metadata.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server has already sent initial metadata.") } } @@ -482,7 +586,7 @@ extension GRPCStreamStateMachine { mutating func send(message: [UInt8], endStream: Bool) throws { switch self.state { case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server must have sent initial metadata before sending a message.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server must have sent initial metadata before sending a message.") case .clientOpenServerOpen(var state): state.framer.append(message) if endStream { @@ -498,7 +602,7 @@ extension GRPCStreamStateMachine { self.state = .clientClosedServerOpen(state) } case .clientOpenServerClosed, .clientClosedServerClosed: - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server can't send a message if it's closed.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server can't send a message if it's closed.") } } @@ -506,7 +610,7 @@ extension GRPCStreamStateMachine { // Close the server. switch self.state { case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server can't send status if idle.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server can't send status if idle.") case .clientOpenServerOpen(let state): self.state = .clientOpenServerClosed(.init(previousState: state)) case .clientClosedServerOpen(let state): @@ -514,15 +618,15 @@ extension GRPCStreamStateMachine { state.decompressor?.end() self.state = .clientClosedServerClosed(.init(previousState: state)) case .clientOpenServerClosed, .clientClosedServerClosed: - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server can't send anything if closed.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server can't send anything if closed.") } } mutating func receive(metadata: Metadata, endStream: Bool) throws -> OnMetadataReceived { if endStream, case .clientIdleServerIdle = self.state { - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition( + throw self.assertionFailureAndCreateRPCErrorOnInternalError( """ - Client should have opened before ending the stream: + Client should have opened before ending the stream: \ stream shouldn't have been closed when sending initial metadata. """ ) @@ -530,16 +634,26 @@ extension GRPCStreamStateMachine { switch self.state { case .clientIdleServerIdle(let state): + var preferredCompressionEncoding: CompressionAlgorithm? = nil + if let acceptedEncodings = metadata.acceptedEncodings { + for acceptedEncoding in acceptedEncodings where self.supportedCompressionAlgorithms.contains(where: { $0 == acceptedEncoding }) { + // Found the preferred encoding: use it to compress responses. + preferredCompressionEncoding = acceptedEncoding + break + } + } + self.state = .clientOpenServerIdle(.init( previousState: state, - compressionAlgorithm: metadata.encoding + compressionAlgorithm: preferredCompressionEncoding, + decompressionConfiguration: .decompression(metadata.encoding) )) guard let contentType = metadata.contentType else { throw RPCError(code: .invalidArgument, message: "Invalid or empty content-type.") } - guard let endpoint = metadata.endpoint else { + guard let endpoint = metadata.path else { throw RPCError(code: .unimplemented, message: "No :path header has been set.") } @@ -555,7 +669,7 @@ extension GRPCStreamStateMachine { message: "grpc-encoding must contain no more than one value" ) } - guard let encoding = Encoding(rawValue: rawEncoding) else { + guard let encoding = CompressionAlgorithm(rawValue: rawEncoding) else { let status = Status( code: .unimplemented, message: "\(rawEncoding) compression is not supported; supported algorithms are listed in grpc-accept-encoding" @@ -563,7 +677,7 @@ extension GRPCStreamStateMachine { let trailers = Metadata(dictionaryLiteral: ( "grpc-accept-encoding", .string(self.supportedCompressionAlgorithms - .map({ $0.rawValue }) + .map({ $0.name }) .joined(separator: ",") ) )) @@ -584,7 +698,7 @@ extension GRPCStreamStateMachine { let trailers = Metadata(dictionaryLiteral: ( "grpc-accept-encoding", .string(self.supportedCompressionAlgorithms - .map({ $0.rawValue }) + .map({ $0.name }) .joined(separator: ",") ) )) @@ -594,18 +708,21 @@ extension GRPCStreamStateMachine { } return .doNothing case .clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed: - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Client shouldn't have sent metadata twice.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client shouldn't have sent metadata twice.") case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Client can't have sent metadata if closed.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client can't have sent metadata if closed.") } } mutating func receive(message: ByteBuffer, endStream: Bool) throws { switch self.state { case .clientIdleServerIdle: - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Can't have received a message if client is idle.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Can't have received a message if client is idle.") case .clientOpenServerIdle(var state): - try state.deframer.process(buffer: message) { deframedMessage in + // Deframer must be present on the server side, as we know the decompression + // algorithm from the moment the client opens. + assert(state.deframer != nil) + try state.deframer!.process(buffer: message) { deframedMessage in state.inboundMessageBuffer.append(deframedMessage) } @@ -629,14 +746,14 @@ extension GRPCStreamStateMachine { // Ignore the rest of the request: do nothing. () case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Client can't send a message if closed.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client can't send a message if closed.") } } mutating func nextOutboundMessage() throws -> ByteBuffer? { switch self.state { case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Server is not open yet.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server is not open yet.") case .clientOpenServerOpen(var state): let response = try state.framer.next(compressor: state.compressor) self.state = .clientOpenServerOpen(state) @@ -646,7 +763,7 @@ extension GRPCStreamStateMachine { self.state = .clientClosedServerOpen(state) return response case .clientOpenServerClosed, .clientClosedServerClosed: - throw assertionFailureAndCreateRPCErrorOnFailedPrecondition("Can't send response if server is closed.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Can't send response if server is closed.") } } @@ -668,18 +785,18 @@ extension GRPCStreamStateMachine { let request = state.inboundMessageBuffer.pop() self.state = .clientClosedServerOpen(state) return request - case .clientClosedServerIdle, - .clientIdleServerIdle, - .clientClosedServerClosed: + case .clientClosedServerClosed(var state): + let request = state.inboundMessageBuffer.pop() + self.state = .clientClosedServerClosed(state) + return request + case .clientClosedServerIdle, .clientIdleServerIdle: return nil } } - private func assertionFailureAndCreateRPCErrorOnFailedPrecondition(_ message: String) -> RPCError { - if !self.skipAssertions { - assertionFailure(message) - } - return RPCError(code: .failedPrecondition, message: message) + private func assertionFailureAndCreateRPCErrorOnInternalError(_ message: String, line: UInt = #line) -> RPCError { + assert(self.skipAssertions, message, line: line) + return RPCError(code: .internalError, message: message) } } } @@ -695,46 +812,97 @@ extension MethodDescriptor { } extension Metadata { - public var endpoint: MethodDescriptor? { + var path: MethodDescriptor? { get { - self[stringValues: ":path"] - .first(where: { _ in true }) + self.firstString(forKey: .endpoint) .flatMap { MethodDescriptor(fullyQualifiedMethod: $0) } } set { if let newValue { - self.replaceOrAddString(newValue.fullyQualifiedMethod, forKey: ":path") + self.replaceOrAddString(newValue.fullyQualifiedMethod, forKey: .endpoint) } else { - self.removeAllValues(forKey: ":path") + self.removeAllValues(forKey: .endpoint) } } } - public var contentType: ContentType? { + var contentType: ContentType? { get { - self[stringValues: "content-type"] - .first(where: { _ in true }) + self.firstString(forKey: .contentType) .flatMap { ContentType(value: $0) } } set { if let newValue { - self.replaceOrAddString(newValue.canonicalValue, forKey: "content-type") + self.replaceOrAddString(newValue.canonicalValue, forKey: .contentType) } else { - self.removeAllValues(forKey: "content-type") + self.removeAllValues(forKey: .contentType) } } } + + var encoding: CompressionAlgorithm? { + get { + self.firstString(forKey: .encoding).flatMap { CompressionAlgorithm(rawValue: $0) } + } + set { + if let newValue { + self.replaceOrAddString(newValue.name, forKey: .encoding) + } else { + self.removeAllValues(forKey: .encoding) + } + } + } + + var acceptedEncodings: [CompressionAlgorithm]? { + get { + self.firstString(forKey: .acceptEncoding)? + .split(separator: ",") + .compactMap { CompressionAlgorithm(rawValue: String($0)) } + } + set { + if let newValue { + self.replaceOrAddString(newValue.map({ $0.name }).joined(separator: ","), forKey: .acceptEncoding) + } else { + self.removeAllValues(forKey: .acceptEncoding) + } + } + } + + private enum GRPCHTTP2Keys: String { + case endpoint = ":path" + case contentType = "content-type" + case encoding = "grpc-encoding" + case acceptEncoding = "grpc-accept-encoding" + } + + private func firstString(forKey key: GRPCHTTP2Keys) -> String? { + self[stringValues: key.rawValue].first(where: { _ in true }) + } + + private mutating func replaceOrAddString(_ value: String, forKey key: GRPCHTTP2Keys) { + self.replaceOrAddString(value, forKey: key.rawValue) + } + + private mutating func removeAllValues(forKey key: GRPCHTTP2Keys) { + self.removeAllValues(forKey: key.rawValue) + } } extension Zlib.Method { - init?(encoding: Encoding?) { + init?(encoding: CompressionAlgorithm?) { + guard let encoding else { + return nil + } + switch encoding { - case .none, .identity: + case .identity: return nil case .deflate: self = .deflate case .gzip: self = .gzip + default: + return nil } } } diff --git a/Sources/GRPCHTTP2Core/Internal/ContentType.swift b/Sources/GRPCHTTP2Core/Internal/ContentType.swift index f46eb90e0..f3377c133 100644 --- a/Sources/GRPCHTTP2Core/Internal/ContentType.swift +++ b/Sources/GRPCHTTP2Core/Internal/ContentType.swift @@ -16,11 +16,8 @@ // See: // - https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md -// - https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md -public enum ContentType { +enum ContentType { case protobuf - case webProtobuf - case webTextProtobuf init?(value: String) { switch value { @@ -28,14 +25,6 @@ public enum ContentType { "application/grpc+proto": self = .protobuf - case "application/grpc-web", - "application/grpc-web+proto": - self = .webProtobuf - - case "application/grpc-web-text", - "application/grpc-web-text+proto": - self = .webTextProtobuf - default: return nil } @@ -46,12 +35,6 @@ public enum ContentType { case .protobuf: // This is more widely supported than "application/grpc+proto" return "application/grpc" - - case .webProtobuf: - return "application/grpc-web+proto" - - case .webTextProtobuf: - return "application/grpc-web-text+proto" } } } diff --git a/Sources/GRPCHTTP2Core/MessageEncoding.swift b/Sources/GRPCHTTP2Core/MessageEncoding.swift new file mode 100644 index 000000000..69fe2d91c --- /dev/null +++ b/Sources/GRPCHTTP2Core/MessageEncoding.swift @@ -0,0 +1,8 @@ +// +// File.swift +// +// +// Created by Gus Cairo on 14/02/2024. +// + +import Foundation diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index a9b63a895..cf29f2520 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -29,7 +29,10 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { private func makeClientStateMachine() -> GRPCStreamStateMachine { GRPCStreamStateMachine( - configuration: .client(maximumPayloadSize: 100), + configuration: .client( + maximumPayloadSize: 100, + supportedCompressionAlgorithms: [.deflate] + ), skipAssertions: true ) } @@ -49,7 +52,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Try sending metadata again: should throw XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client is already open: shouldn't be sending metadata.") } } @@ -65,7 +68,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Try sending metadata again: should throw XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client is already open: shouldn't be sending metadata.") } } @@ -84,7 +87,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Try sending metadata again: should throw XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client is already open: shouldn't be sending metadata.") } } @@ -100,7 +103,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Try sending metadata again: should throw XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client is closed: can't send metadata.") } } @@ -119,7 +122,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Try sending metadata again: should throw XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client is closed: can't send metadata.") } } @@ -141,7 +144,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Try sending metadata again: should throw XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client is closed: can't send metadata.") } } @@ -154,7 +157,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Try to send a message without opening (i.e. without sending initial metadata) XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(message: [], endStream: false)) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client not yet open.") } } @@ -210,7 +213,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Try sending another message: it should fail XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(message: [], endStream: false)) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client is closed, cannot send a message.") } } @@ -230,7 +233,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Try sending another message: it should fail XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(message: [], endStream: false)) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client is closed, cannot send a message.") } } @@ -253,7 +256,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Try sending another message: it should fail XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(message: [], endStream: false)) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client is closed, cannot send a message.") } } @@ -266,7 +269,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // This operation is never allowed on the client. XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(status: "test", trailingMetadata: .init())) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client cannot send status and trailer.") } } @@ -280,7 +283,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // This operation is never allowed on the client. XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(status: "test", trailingMetadata: .init())) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client cannot send status and trailer.") } } @@ -297,7 +300,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // This operation is never allowed on the client. XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(status: "test", trailingMetadata: .init())) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client cannot send status and trailer.") } } @@ -317,7 +320,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // This operation is never allowed on the client. XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(status: "test", trailingMetadata: .init())) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client cannot send status and trailer.") } } @@ -334,7 +337,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // This operation is never allowed on the client. XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(status: "test", trailingMetadata: .init())) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client cannot send status and trailer.") } } @@ -354,7 +357,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // This operation is never allowed on the client. XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(status: "test", trailingMetadata: .init())) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client cannot send status and trailer.") } } @@ -377,7 +380,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // This operation is never allowed on the client. XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(status: "test", trailingMetadata: .init())) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client cannot send status and trailer.") } } @@ -389,7 +392,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.receive(metadata: .init(), endStream: false)) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server cannot have sent metadata if the client is idle.") } } @@ -400,7 +403,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Open client XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) - // Open server + // Receive metadata = open server let action = try stateMachine.receive(metadata: .init(), endStream: false) guard case .doNothing = action else { XCTFail("Expected action to be doNothing") @@ -439,12 +442,12 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.receive(metadata: .init(), endStream: false)) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server is closed, nothing could have been sent.") } } - func testReceiveInitialMetadataWhenClientClosedAndServerIdle() { + func testReceiveInitialMetadataWhenClientClosedAndServerIdle() throws { var stateMachine = makeClientStateMachine() // Open client @@ -453,14 +456,15 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: .init(), endStream: false)) { error in - XCTAssertEqual(error.code, .failedPrecondition) - XCTAssertEqual(error.message, "Client is closed, shouldn't have received anything.") + // Receive metadata = open server + let action = try stateMachine.receive(metadata: .init(), endStream: false) + guard case .doNothing = action else { + XCTFail("Expected action to be doNothing") + return } } - func testReceiveInitialMetadataWhenClientClosedAndServerOpen() { + func testReceiveInitialMetadataWhenClientClosedAndServerOpen() throws { var stateMachine = makeClientStateMachine() // Open client @@ -472,10 +476,11 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: .init(), endStream: false)) { error in - XCTAssertEqual(error.code, .failedPrecondition) - XCTAssertEqual(error.message, "Client is closed, shouldn't have received anything.") + // Receive metadata = open server + let action = try stateMachine.receive(metadata: .init(), endStream: false) + guard case .doNothing = action else { + XCTFail("Expected action to be doNothing") + return } } @@ -496,8 +501,8 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.receive(metadata: .init(), endStream: false)) { error in - XCTAssertEqual(error.code, .failedPrecondition) - XCTAssertEqual(error.message, "Client is closed, shouldn't have received anything.") + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Server is closed, nothing could have been sent.") } } @@ -509,8 +514,8 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Receive an end trailer XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.receive(metadata: .init(), endStream: true)) { error in - XCTAssertEqual(error.code, .failedPrecondition) - XCTAssertEqual(error.message, "Client can't have received a stream end trailer if both client and server are idle.") + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Server cannot have sent metadata if the client is idle.") } } @@ -520,12 +525,8 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Open client XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) - // Receive an end trailer - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: .init(), endStream: true)) { error in - XCTAssertEqual(error.code, .failedPrecondition) - XCTAssertEqual(error.message, "Server cannot have sent an end stream header if it is still idle.") - } + // Receive a trailer-only response + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) } func testReceiveEndTrailerWhenClientOpenAndServerOpen() throws { @@ -560,8 +561,8 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Receive another end trailer XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.receive(metadata: .init(), endStream: true)) { error in - XCTAssertEqual(error.code, .failedPrecondition) - XCTAssertEqual(error.message, "Server is already closed, can't have received the end stream trailer twice.") + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Server is closed, nothing could have been sent.") } } @@ -574,12 +575,8 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - // Closing the server now should throw - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: .init(), endStream: true)) { error in - XCTAssertEqual(error.code, .failedPrecondition) - XCTAssertEqual(error.message, "Server cannot have sent end stream trailer if it is idle.") - } + // Server sends a trailers-only response + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) } func testReceiveEndTrailerWhenClientClosedAndServerOpen() throws { @@ -620,8 +617,8 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Closing the server again should throw XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.receive(metadata: .init(), endStream: true)) { error in - XCTAssertEqual(error.code, .failedPrecondition) - XCTAssertEqual(error.message, "Server cannot have sent end stream trailer if it is already closed.") + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Server is closed, nothing could have been sent.") } } @@ -632,7 +629,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.receive(message: .init(), endStream: false)) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Cannot have received anything from server if client is not yet open.") } } @@ -645,7 +642,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.receive(message: .init(), endStream: false)) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server cannot have sent a message before sending the initial metadata.") } } @@ -677,7 +674,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.receive(message: .init(), endStream: false)) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Cannot have received anything from a closed server.") } } @@ -693,7 +690,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.receive(message: .init(), endStream: false)) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server cannot have sent a message before sending the initial metadata.") } } @@ -731,7 +728,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.receive(message: .init(), endStream: false)) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Shouldn't have received anything if both client and server are closed.") } } @@ -743,7 +740,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.nextOutboundMessage()) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client is not open yet.") } } @@ -1133,7 +1130,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client cannot be idle if server is sending initial metadata: it must have opened.") } } @@ -1159,7 +1156,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Try sending metadata again: should throw XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server has already sent initial metadata.") } } @@ -1178,7 +1175,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Try sending metadata again: should throw XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server cannot send metadata if closed.") } } @@ -1211,7 +1208,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Try sending metadata again: should throw XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server has already sent initial metadata.") } } @@ -1233,7 +1230,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Try sending metadata again: should throw XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server cannot send metadata if closed.") } } @@ -1245,7 +1242,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(message: [], endStream: false)) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server must have sent initial metadata before sending a message.") } } @@ -1259,7 +1256,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Now send a message XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(message: [], endStream: false)) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server must have sent initial metadata before sending a message.") } } @@ -1292,7 +1289,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Try sending another message: it should fail XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(message: [], endStream: false)) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send a message if it's closed.") } } @@ -1308,7 +1305,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(message: [], endStream: false)) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server must have sent initial metadata before sending a message.") } } @@ -1348,7 +1345,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Try sending another message: it should fail XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(message: [], endStream: false)) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send a message if it's closed.") } } @@ -1360,7 +1357,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(status: "test", trailingMetadata: .init())) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send status if idle.") } } @@ -1373,7 +1370,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(status: "test", trailingMetadata: .init())) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send status if idle.") } } @@ -1392,7 +1389,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Try sending another message: it should fail because server is now closed. XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(message: [], endStream: false)) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send a message if it's closed.") } } @@ -1411,7 +1408,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(status: "test", trailingMetadata: .init())) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send anything if closed.") } } @@ -1427,7 +1424,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(status: "test", trailingMetadata: .init())) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send status if idle.") } } @@ -1465,7 +1462,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(status: "test", trailingMetadata: .init())) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send anything if closed.") } } @@ -1490,11 +1487,11 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // to open the stream (initial metadata). XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.receive(metadata: self.testMetadata, endStream: true)) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual( error.message, """ - Client should have opened before ending the stream: + Client should have opened before ending the stream: \ stream shouldn't have been closed when sending initial metadata. """ ) @@ -1555,7 +1552,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Try receiving initial metadata again - should fail XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.receive(metadata: self.testMetadata, endStream: false)) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client shouldn't have sent metadata twice.") } } @@ -1571,7 +1568,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.receive(metadata: self.testMetadata, endStream: false)) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client shouldn't have sent metadata twice.") } } @@ -1590,7 +1587,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.receive(metadata: self.testMetadata, endStream: false)) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client shouldn't have sent metadata twice.") } } @@ -1606,7 +1603,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.receive(metadata: self.testMetadata, endStream: false)) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't have sent metadata if closed.") } } @@ -1626,7 +1623,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.receive(metadata: self.testMetadata, endStream: false)) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't have sent metadata if closed.") } } @@ -1648,7 +1645,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.receive(metadata: self.testMetadata, endStream: false)) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't have sent metadata if closed.") } } @@ -1660,7 +1657,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.receive(message: .init(), endStream: false)) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Can't have received a message if client is idle.") } } @@ -1678,7 +1675,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Verify client is now closed XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.receive(message: .init(), endStream: false)) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't send a message if closed.") } } @@ -1699,7 +1696,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Verify client is now closed XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.receive(message: .init(), endStream: false)) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't send a message if closed.") } } @@ -1731,7 +1728,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.receive(message: .init(), endStream: false)) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't send a message if closed.") } } @@ -1750,7 +1747,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.receive(message: .init(), endStream: false)) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't send a message if closed.") } } @@ -1772,7 +1769,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.receive(message: .init(), endStream: false)) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't send a message if closed.") } } @@ -1784,7 +1781,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.nextOutboundMessage()) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server is not open yet.") } } @@ -1797,7 +1794,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.nextOutboundMessage()) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server is not open yet.") } } @@ -1810,7 +1807,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.nextOutboundMessage()) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server is not open yet.") } } @@ -1878,7 +1875,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.nextOutboundMessage()) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Can't send response if server is closed.") } } @@ -1894,7 +1891,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.nextOutboundMessage()) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server is not open yet.") } } @@ -1957,7 +1954,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // is closed. XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.nextOutboundMessage()) { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Can't send response if server is closed.") } } From 21ac2d82830d6f082bbcb4f1438253acceb1885d Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Fri, 16 Feb 2024 10:49:29 +0000 Subject: [PATCH 12/51] Fix encoding negotiation and return headers --- Sources/GRPCCore/Internal/Base64.swift | 223 ++++++--- Sources/GRPCCore/Metadata.swift | 11 + .../GRPCStreamStateMachine.swift | 446 +++++++++++++----- Sources/GRPCHTTP2Core/MessageEncoding.swift | 8 - .../GRPCStreamStateMachineTests.swift | 197 ++++---- 5 files changed, 623 insertions(+), 262 deletions(-) delete mode 100644 Sources/GRPCHTTP2Core/MessageEncoding.swift diff --git a/Sources/GRPCCore/Internal/Base64.swift b/Sources/GRPCCore/Internal/Base64.swift index 6265b996b..8ccf2b324 100644 --- a/Sources/GRPCCore/Internal/Base64.swift +++ b/Sources/GRPCCore/Internal/Base64.swift @@ -20,18 +20,18 @@ Copyright (c) 2015-2016, Wojciech Muła, Alfred Klomp, Daniel Lemire (Unless otherwise stated in the source code) All rights reserved. - + Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - + 1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A @@ -48,19 +48,19 @@ // https://github.com/client9/stringencoders/blob/master/src/modp_b64.c /* The MIT License (MIT) - + Copyright (c) 2016 Nick Galbreath - + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -70,25 +70,50 @@ SOFTWARE. */ -internal enum Base64 {} - -extension Base64 { - internal struct DecodingOptions: OptionSet { +enum Base64 { + struct DecodingOptions: OptionSet { internal let rawValue: UInt internal init(rawValue: UInt) { self.rawValue = rawValue } - + internal static let base64UrlAlphabet = DecodingOptions(rawValue: UInt(1 << 0)) internal static let omitPaddingCharacter = DecodingOptions(rawValue: UInt(1 << 1)) } - - internal enum DecodingError: Error, Equatable { + + enum DecodingError: Error, Equatable { case invalidLength case invalidCharacter(UInt8) case unexpectedPaddingCharacter case unexpectedEnd } - - internal static func decode( + + static func encode(bytes: Buffer) -> String where Buffer.Element == UInt8 { + guard !bytes.isEmpty else { + return "" + } + + // In Base64, 3 bytes become 4 output characters, and we pad to the + // nearest multiple of four. + let base64StringLength = ((bytes.count + 2) / 3) * 4 + let alphabet = Base64.encodeBase64 + + return String(customUnsafeUninitializedCapacity: base64StringLength) { backingStorage in + var input = bytes.makeIterator() + var offset = 0 + while let firstByte = input.next() { + let secondByte = input.next() + let thirdByte = input.next() + + backingStorage[offset] = Base64.encode(alphabet: alphabet, firstByte: firstByte) + backingStorage[offset + 1] = Base64.encode(alphabet: alphabet, firstByte: firstByte, secondByte: secondByte) + backingStorage[offset + 2] = Base64.encode(alphabet: alphabet, secondByte: secondByte, thirdByte: thirdByte) + backingStorage[offset + 3] = Base64.encode(alphabet: alphabet, thirdByte: thirdByte) + offset += 4 + } + return offset + } + } + + static func decode( string encoded: String, options: DecodingOptions = [] ) throws -> [UInt8] { @@ -97,25 +122,25 @@ extension Base64 { guard characterPointer.count > 0 else { return [] } - + let outputLength = ((characterPointer.count + 3) / 4) * 3 - + return try characterPointer.withMemoryRebound(to: UInt8.self) { (input) -> [UInt8] in try [UInt8](unsafeUninitializedCapacity: outputLength) { output, length in try Self._decodeChromium(from: input, into: output, length: &length, options: options) } } } - + if decoded != nil { return decoded! } - + var encoded = encoded encoded.makeContiguousUTF8() return try Self.decode(string: encoded, options: options) } - + private static func _decodeChromium( from inBuffer: UnsafeBufferPointer, into outBuffer: UnsafeMutableBufferPointer, @@ -132,13 +157,13 @@ extension Base64 { // everythin alright so far break } - + let outputLength = ((inBuffer.count + 3) / 4) * 3 let fullchunks = remaining == 0 ? inBuffer.count / 4 - 1 : inBuffer.count / 4 guard outBuffer.count >= outputLength else { preconditionFailure("Expected the out buffer to be at least as long as outputLength") } - + try Self.withUnsafeDecodingTablesAsBufferPointers(options: options) { d0, d1, d2, d3 in var outIndex = 0 if fullchunks > 0 { @@ -149,12 +174,12 @@ extension Base64 { let a2 = inBuffer[inIndex + 2] let a3 = inBuffer[inIndex + 3] var x: UInt32 = d0[Int(a0)] | d1[Int(a1)] | d2[Int(a2)] | d3[Int(a3)] - + if x >= Self.badCharacter { // TODO: Inspect characters here better throw DecodingError.invalidCharacter(inBuffer[inIndex]) } - + withUnsafePointer(to: &x) { ptr in ptr.withMemoryRebound(to: UInt8.self, capacity: 4) { newPtr in outBuffer[outIndex] = newPtr[0] @@ -165,7 +190,7 @@ extension Base64 { } } } - + // inIndex is the first index in the last chunk let inIndex = fullchunks * 4 let a0 = inBuffer[inIndex] @@ -178,13 +203,13 @@ extension Base64 { if inIndex + 3 < inBuffer.count, inBuffer[inIndex + 3] != Self.encodePaddingCharacter { a3 = inBuffer[inIndex + 3] } - + var x: UInt32 = d0[Int(a0)] | d1[Int(a1)] | d2[Int(a2 ?? 65)] | d3[Int(a3 ?? 65)] if x >= Self.badCharacter { // TODO: Inspect characters here better throw DecodingError.invalidCharacter(inBuffer[inIndex]) } - + withUnsafePointer(to: &x) { ptr in ptr.withMemoryRebound(to: UInt8.self, capacity: 4) { newPtr in outBuffer[outIndex] = newPtr[0] @@ -199,12 +224,12 @@ extension Base64 { } } } - + length = outIndex } } - - static func withUnsafeDecodingTablesAsBufferPointers( + + private static func withUnsafeDecodingTablesAsBufferPointers( options: Base64.DecodingOptions, _ body: ( UnsafeBufferPointer, UnsafeBufferPointer, UnsafeBufferPointer, @@ -215,12 +240,12 @@ extension Base64 { let decoding1 = options.contains(.base64UrlAlphabet) ? Self.decoding1url : Self.decoding1 let decoding2 = options.contains(.base64UrlAlphabet) ? Self.decoding2url : Self.decoding2 let decoding3 = options.contains(.base64UrlAlphabet) ? Self.decoding3url : Self.decoding3 - + assert(decoding0.count == 256) assert(decoding1.count == 256) assert(decoding2.count == 256) assert(decoding3.count == 256) - + return try decoding0.withUnsafeBufferPointer { (d0) -> R in try decoding1.withUnsafeBufferPointer { (d1) -> R in try decoding2.withUnsafeBufferPointer { (d2) -> R in @@ -231,11 +256,65 @@ extension Base64 { } } } - - internal static let encodePaddingCharacter: UInt8 = 61 - static let badCharacter: UInt32 = 0x01FF_FFFF - - static let decoding0: [UInt32] = [ + + private static let encodePaddingCharacter: UInt8 = 61 + + private static let encodeBase64: [UInt8] = [ + UInt8(ascii: "A"), UInt8(ascii: "B"), UInt8(ascii: "C"), UInt8(ascii: "D"), + UInt8(ascii: "E"), UInt8(ascii: "F"), UInt8(ascii: "G"), UInt8(ascii: "H"), + UInt8(ascii: "I"), UInt8(ascii: "J"), UInt8(ascii: "K"), UInt8(ascii: "L"), + UInt8(ascii: "M"), UInt8(ascii: "N"), UInt8(ascii: "O"), UInt8(ascii: "P"), + UInt8(ascii: "Q"), UInt8(ascii: "R"), UInt8(ascii: "S"), UInt8(ascii: "T"), + UInt8(ascii: "U"), UInt8(ascii: "V"), UInt8(ascii: "W"), UInt8(ascii: "X"), + UInt8(ascii: "Y"), UInt8(ascii: "Z"), UInt8(ascii: "a"), UInt8(ascii: "b"), + UInt8(ascii: "c"), UInt8(ascii: "d"), UInt8(ascii: "e"), UInt8(ascii: "f"), + UInt8(ascii: "g"), UInt8(ascii: "h"), UInt8(ascii: "i"), UInt8(ascii: "j"), + UInt8(ascii: "k"), UInt8(ascii: "l"), UInt8(ascii: "m"), UInt8(ascii: "n"), + UInt8(ascii: "o"), UInt8(ascii: "p"), UInt8(ascii: "q"), UInt8(ascii: "r"), + UInt8(ascii: "s"), UInt8(ascii: "t"), UInt8(ascii: "u"), UInt8(ascii: "v"), + UInt8(ascii: "w"), UInt8(ascii: "x"), UInt8(ascii: "y"), UInt8(ascii: "z"), + UInt8(ascii: "0"), UInt8(ascii: "1"), UInt8(ascii: "2"), UInt8(ascii: "3"), + UInt8(ascii: "4"), UInt8(ascii: "5"), UInt8(ascii: "6"), UInt8(ascii: "7"), + UInt8(ascii: "8"), UInt8(ascii: "9"), UInt8(ascii: "+"), UInt8(ascii: "/"), + ] + + private static func encode(alphabet: [UInt8], firstByte: UInt8) -> UInt8 { + let index = firstByte >> 2 + return alphabet[Int(index)] + } + + private static func encode(alphabet: [UInt8], firstByte: UInt8, secondByte: UInt8?) -> UInt8 { + var index = (firstByte & 0b00000011) << 4 + if let secondByte = secondByte { + index += (secondByte & 0b11110000) >> 4 + } + return alphabet[Int(index)] + } + + private static func encode(alphabet: [UInt8], secondByte: UInt8?, thirdByte: UInt8?) -> UInt8 { + guard let secondByte = secondByte else { + // No second byte means we are just emitting padding. + return Base64.encodePaddingCharacter + } + var index = (secondByte & 0b00001111) << 2 + if let thirdByte = thirdByte { + index += (thirdByte & 0b11000000) >> 6 + } + return alphabet[Int(index)] + } + + private static func encode(alphabet: [UInt8], thirdByte: UInt8?) -> UInt8 { + guard let thirdByte = thirdByte else { + // No third byte means just padding. + return Base64.encodePaddingCharacter + } + let index = thirdByte & 0b00111111 + return alphabet[Int(index)] + } + + private static let badCharacter: UInt32 = 0x01FF_FFFF + + private static let decoding0: [UInt32] = [ 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, @@ -280,8 +359,8 @@ extension Base64 { 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, ] - - static let decoding1: [UInt32] = [ + + private static let decoding1: [UInt32] = [ 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, @@ -326,8 +405,8 @@ extension Base64 { 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, ] - - static let decoding2: [UInt32] = [ + + private static let decoding2: [UInt32] = [ 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, @@ -372,8 +451,8 @@ extension Base64 { 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, ] - - static let decoding3: [UInt32] = [ + + private static let decoding3: [UInt32] = [ 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, @@ -418,8 +497,8 @@ extension Base64 { 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, ] - - static let decoding0url: [UInt32] = [ + + private static let decoding0url: [UInt32] = [ 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 0 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 6 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 12 @@ -464,8 +543,8 @@ extension Base64 { 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, ] - - static let decoding1url: [UInt32] = [ + + private static let decoding1url: [UInt32] = [ 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 0 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 6 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 12 @@ -510,8 +589,8 @@ extension Base64 { 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, ] - - static let decoding2url: [UInt32] = [ + + private static let decoding2url: [UInt32] = [ 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 0 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 6 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 12 @@ -556,8 +635,8 @@ extension Base64 { 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, ] - - static let decoding3url: [UInt32] = [ + + private static let decoding3url: [UInt32] = [ 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 0 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 6 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 12 @@ -603,3 +682,35 @@ extension Base64 { 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, ] } + +extension String { + /// This is a backport of a proposed String initializer that will allow writing directly into an uninitialized String's backing memory. + /// + /// As this API does not exist prior to 5.3 on Linux, or on older Apple platforms, we fake it out with a pointer and accept the extra copy. + init(backportUnsafeUninitializedCapacity capacity: Int, + initializingUTF8With initializer: (_ buffer: UnsafeMutableBufferPointer) throws -> Int) rethrows { + + // The buffer will store zero terminated C string + let buffer = UnsafeMutableBufferPointer.allocate(capacity: capacity + 1) + defer { + buffer.deallocate() + } + + let initializedCount = try initializer(buffer) + precondition(initializedCount <= capacity, "Overran buffer in initializer!") + + // add zero termination + buffer[initializedCount] = 0 + + self = String(cString: buffer.baseAddress!) + } + + init(customUnsafeUninitializedCapacity capacity: Int, + initializingUTF8With initializer: (_ buffer: UnsafeMutableBufferPointer) throws -> Int) rethrows { + if #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) { + try self.init(unsafeUninitializedCapacity: capacity, initializingUTF8With: initializer) + } else { + try self.init(backportUnsafeUninitializedCapacity: capacity, initializingUTF8With: initializer) + } + } +} diff --git a/Sources/GRPCCore/Metadata.swift b/Sources/GRPCCore/Metadata.swift index 0b2e560f8..c9272acda 100644 --- a/Sources/GRPCCore/Metadata.swift +++ b/Sources/GRPCCore/Metadata.swift @@ -85,6 +85,17 @@ public struct Metadata: Sendable, Hashable { public enum Value: Sendable, Hashable { case string(String) case binary([UInt8]) + + /// The value as a String. If it was originally stored as a binary, the base64-encoded String version + /// of the binary data will be returned instead. + public var stringValue: String { + switch self { + case .string(let string): + return string + case .binary(let bytes): + return Base64.encode(bytes: bytes) + } + } } /// A metadata key-value pair. diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 43db83c75..2cbf47ee5 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -16,20 +16,28 @@ import GRPCCore import NIOCore +import NIOHPACK +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) enum OnMetadataReceived { - case reject(status: Status, trailers: Metadata) + case reject(status: Status, trailers: HPACKHeaders) case doNothing } +enum Scheme: String { + case http + case https +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) fileprivate protocol GRPCStreamStateMachineProtocol { var state: GRPCStreamStateMachineState { get set } - mutating func send(metadata: Metadata) throws + mutating func send(metadata: Metadata) throws -> HPACKHeaders mutating func send(message: [UInt8], endStream: Bool) throws - mutating func send(status: String, trailingMetadata: Metadata) throws + mutating func send(status: Status, metadata: Metadata, trailersOnly: Bool) throws -> HPACKHeaders - mutating func receive(metadata: Metadata, endStream: Bool) throws -> OnMetadataReceived + mutating func receive(metadata: HPACKHeaders, endStream: Bool) throws -> OnMetadataReceived mutating func receive(message: ByteBuffer, endStream: Bool) throws mutating func nextOutboundMessage() throws -> ByteBuffer? @@ -38,12 +46,16 @@ fileprivate protocol GRPCStreamStateMachineProtocol { enum GRPCStreamStateMachineConfiguration { case client( + methodDescriptor: MethodDescriptor, + scheme: Scheme, maximumPayloadSize: Int, - supportedCompressionAlgorithms: [CompressionAlgorithm] + outboundEncoding: CompressionAlgorithm?, + acceptedEncodings: [CompressionAlgorithm] ) case server( + scheme: Scheme, maximumPayloadSize: Int, - supportedCompressionAlgorithms: [CompressionAlgorithm] + acceptedEncodings: [CompressionAlgorithm] ) } @@ -69,6 +81,7 @@ enum GRPCStreamStateMachineState { let maximumPayloadSize: Int var framer: GRPCMessageFramer var compressor: Zlib.Compressor? + var outboundCompression: CompressionAlgorithm? // The deframer must be optional because the client will not have one configured // until the server opens and sends a grpc-encoding header. @@ -90,9 +103,13 @@ enum GRPCStreamStateMachineState { self.compressor = Zlib.Compressor(method: zlibMethod) } self.framer = GRPCMessageFramer() + self.outboundCompression = compressionAlgorithm - // TODO: we should check here or in a `MessageEncoder` (instead of the state machine) - // that the server supports the given encoding - otherwise return the corresponding response. + // In the case of the server, we will know what the decompression algorithm + // will be, since we know what the inbound encoding is, as the client has + // sent it when starting the request. + // In the case of the client, it will need to wait until the server responds + // with its initial metadata. if case .decompression(let decompressionAlgorithm) = decompressionConfiguration { if let zlibMethod = Zlib.Method(encoding: decompressionAlgorithm) { self.decompressor = Zlib.Decompressor(method: zlibMethod) @@ -121,17 +138,19 @@ enum GRPCStreamStateMachineState { init( previousState: ClientOpenServerIdleState, - decompressionAlgorithm: CompressionAlgorithm? + decompressionAlgorithm: CompressionAlgorithm? = nil ) { self.framer = previousState.framer self.compressor = previousState.compressor + // In the case of the server, it will already have a deframer set up, + // because it already knows what encoding the client is using. + // In the case of the client, it will only be able to set it up + // after it receives the chosen encoding from the server. if let previousDeframer = previousState.deframer { self.deframer = previousDeframer self.decompressor = previousState.decompressor } else { - // TODO: we should check here or in a `MessageEncoder` (instead of the state machine) - // that the client supports the given encoding - otherwise return the corresponding response. if let zlibMethod = Zlib.Method(encoding: decompressionAlgorithm) { self.decompressor = Zlib.Decompressor(method: zlibMethod) } @@ -182,6 +201,7 @@ enum GRPCStreamStateMachineState { let maximumPayloadSize: Int var framer: GRPCMessageFramer var compressor: Zlib.Compressor? + var outboundCompression: CompressionAlgorithm? let deframer: NIOSingleStepByteToMessageProcessor? var decompressor: Zlib.Decompressor? @@ -192,6 +212,7 @@ enum GRPCStreamStateMachineState { self.maximumPayloadSize = previousState.maximumPayloadSize self.framer = previousState.framer self.compressor = previousState.compressor + self.outboundCompression = previousState.outboundCompression self.deframer = previousState.deframer self.decompressor = previousState.decompressor self.inboundMessageBuffer = previousState.inboundMessageBuffer @@ -217,12 +238,16 @@ enum GRPCStreamStateMachineState { init( previousState: ClientClosedServerIdleState, - decompressionAlgorithm: CompressionAlgorithm? + decompressionAlgorithm: CompressionAlgorithm? = nil ) { self.framer = previousState.framer self.compressor = previousState.compressor self.inboundMessageBuffer = previousState.inboundMessageBuffer + // In the case of the server, it will already have a deframer set up, + // because it already knows what encoding the client is using. + // In the case of the client, it will only be able to set it up + // after it receives the chosen encoding from the server. if let previousDeframer = previousState.deframer { self.deframer = previousDeframer self.decompressor = previousState.decompressor @@ -266,22 +291,35 @@ struct GRPCStreamStateMachine { skipAssertions: Bool = false ) { switch configuration { - case .client(let maximumPayloadSize, let supportedCompressionAlgorithms): + case .client( + let methodDescriptor, + let scheme, + let maximumPayloadSize, + let outboundEncoding, + let acceptedEncodings + ): self._stateMachine = Client( + methodDescriptor: methodDescriptor, + scheme: scheme, maximumPayloadSize: maximumPayloadSize, - supportedCompressionAlgorithms: supportedCompressionAlgorithms, + outboundEncoding: outboundEncoding, + acceptedEncodings: acceptedEncodings, skipAssertions: skipAssertions ) - case .server(let maximumPayloadSize, let supportedCompressionAlgorithms): + case .server( + let scheme, + let maximumPayloadSize, + let acceptedEncodings + ): self._stateMachine = Server( maximumPayloadSize: maximumPayloadSize, - supportedCompressionAlgorithms: supportedCompressionAlgorithms, + acceptedEncodings: acceptedEncodings, skipAssertions: skipAssertions ) } } - mutating func send(metadata: Metadata) throws { + mutating func send(metadata: Metadata) throws -> HPACKHeaders { try self._stateMachine.send(metadata: metadata) } @@ -289,11 +327,11 @@ struct GRPCStreamStateMachine { try self._stateMachine.send(message: message, endStream: endStream) } - mutating func send(status: String, trailingMetadata: Metadata) throws { - try self._stateMachine.send(status: status, trailingMetadata: trailingMetadata) + mutating func send(status: Status, metadata: Metadata, trailersOnly: Bool) throws -> HPACKHeaders { + try self._stateMachine.send(status: status, metadata: metadata, trailersOnly: trailersOnly) } - mutating func receive(metadata: Metadata, endStream: Bool) throws -> OnMetadataReceived { + mutating func receive(metadata: HPACKHeaders, endStream: Bool) throws -> OnMetadataReceived { try self._stateMachine.receive(metadata: metadata, endStream: endStream) } @@ -314,35 +352,66 @@ struct GRPCStreamStateMachine { extension GRPCStreamStateMachine { struct Client: GRPCStreamStateMachineProtocol { fileprivate var state: GRPCStreamStateMachineState - private let supportedCompressionAlgorithms: [CompressionAlgorithm] + + private let methodDescriptor: MethodDescriptor + private let scheme: Scheme + private let outboundEncoding: CompressionAlgorithm? + private let acceptedEncodings: [CompressionAlgorithm] private let skipAssertions: Bool init( + methodDescriptor: MethodDescriptor, + scheme: Scheme, maximumPayloadSize: Int, - supportedCompressionAlgorithms: [CompressionAlgorithm], + outboundEncoding: CompressionAlgorithm?, + acceptedEncodings: [CompressionAlgorithm], skipAssertions: Bool ) { self.state = .clientIdleServerIdle(.init(maximumPayloadSize: maximumPayloadSize)) - self.supportedCompressionAlgorithms = supportedCompressionAlgorithms + self.methodDescriptor = methodDescriptor + self.scheme = scheme + self.outboundEncoding = outboundEncoding + self.acceptedEncodings = acceptedEncodings self.skipAssertions = skipAssertions } + + private func makeClientHeaders(from metadata: Metadata) -> HPACKHeaders { + var headers = HPACKHeaders() + headers.reserveCapacity(7 + metadata.count) + + // Add required headers + // See https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests + headers.path = self.methodDescriptor + headers.scheme = self.scheme + headers.method = "POST" + headers.contentType = .protobuf + headers.te = "trailers" // Used to detect incompatible proxies + + if let encoding = self.outboundEncoding { + headers.encoding = encoding + } + + if !self.acceptedEncodings.isEmpty { + headers.acceptedEncodings = self.acceptedEncodings + } + + for metadataPair in metadata { + headers.add(name: metadataPair.key, value: metadataPair.value.stringValue) + } + + return headers + } - mutating func send(metadata: Metadata) throws { + mutating func send(metadata: Metadata) throws -> HPACKHeaders { // Client sends metadata only when opening the stream. switch self.state { case .clientIdleServerIdle(let state): - guard metadata.path != nil else { - throw RPCError( - code: .invalidArgument, - message: "Endpoint is missing: client cannot send initial metadata without it." - ) - } - self.state = .clientOpenServerIdle(.init( previousState: state, - compressionAlgorithm: metadata.encoding, + compressionAlgorithm: self.outboundEncoding, decompressionConfiguration: .decompressionNotYetKnown )) + return self.makeClientHeaders(from: metadata) case .clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed: throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client is already open: shouldn't be sending metadata.") case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: @@ -380,7 +449,7 @@ extension GRPCStreamStateMachine { } } - mutating func send(status: String, trailingMetadata: Metadata) throws { + mutating func send(status: Status, metadata: Metadata, trailersOnly: Bool) throws -> HPACKHeaders { throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client cannot send status and trailer.") } @@ -418,7 +487,7 @@ extension GRPCStreamStateMachine { } } - mutating func receive(metadata: Metadata, endStream: Bool) throws -> OnMetadataReceived { + mutating func receive(metadata: HPACKHeaders, endStream: Bool) throws -> OnMetadataReceived { switch self.state { case .clientIdleServerIdle: throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server cannot have sent metadata if the client is idle.") @@ -547,39 +616,54 @@ extension GRPCStreamStateMachine { extension GRPCStreamStateMachine { struct Server: GRPCStreamStateMachineProtocol { fileprivate var state: GRPCStreamStateMachineState - let supportedCompressionAlgorithms: [CompressionAlgorithm] + let acceptedEncodings: [CompressionAlgorithm] private let skipAssertions: Bool init( maximumPayloadSize: Int, - supportedCompressionAlgorithms: [CompressionAlgorithm], + acceptedEncodings: [CompressionAlgorithm], skipAssertions: Bool ) { self.state = .clientIdleServerIdle(.init(maximumPayloadSize: maximumPayloadSize)) - self.supportedCompressionAlgorithms = supportedCompressionAlgorithms + self.acceptedEncodings = acceptedEncodings self.skipAssertions = skipAssertions } - mutating func send(metadata: Metadata) throws { - // Server sends initial metadata. This transitions server to open. + private func makeResponseHeaders(outboundEncoding: CompressionAlgorithm?, customMetadata: Metadata) -> HPACKHeaders { + // Response headers always contain :status (HTTP Status 200) and content-type. + // They may also contain grpc-encoding, grpc-accept-encoding, and custom metadata. + var headers = HPACKHeaders() + headers.reserveCapacity(4 + customMetadata.count) + + headers.status = "200" + headers.contentType = .protobuf + + if let outboundEncoding { + headers.encoding = outboundEncoding + } + + for metadataPair in customMetadata { + headers.add(name: metadataPair.key, value: metadataPair.value.stringValue) + } + + return headers + } + + mutating func send(metadata: Metadata) throws -> HPACKHeaders { + // Server sends initial metadata switch self.state { + case .clientOpenServerIdle(let state): + self.state = .clientOpenServerOpen(.init(previousState: state)) + return self.makeResponseHeaders(outboundEncoding: state.outboundCompression, customMetadata: metadata) + case .clientClosedServerIdle(let state): + self.state = .clientClosedServerOpen(.init(previousState: state)) + return self.makeResponseHeaders(outboundEncoding: state.outboundCompression, customMetadata: metadata) case .clientIdleServerIdle: throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client cannot be idle if server is sending initial metadata: it must have opened.") - case .clientOpenServerIdle(let state): - self.state = .clientOpenServerOpen(.init( - previousState: state, - decompressionAlgorithm: metadata.encoding - )) case .clientOpenServerClosed, .clientClosedServerClosed: throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server cannot send metadata if closed.") - case .clientClosedServerIdle(let state): - self.state = .clientClosedServerOpen(.init( - previousState: state, - decompressionAlgorithm: metadata.encoding - )) case .clientOpenServerOpen, .clientClosedServerOpen: throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server has already sent initial metadata.") - } } @@ -606,23 +690,62 @@ extension GRPCStreamStateMachine { } } - mutating func send(status: String, trailingMetadata: Metadata) throws { + private func makeTrailers( + status: Status, + customMetadata: Metadata, + trailersOnly: Bool + ) -> HPACKHeaders { + // Trailers always contain the grpc-status header, and optionally, + // grpc-status-message, and custom metadata. + // If it's a trailers-only response, they will also contain :status and + // content-type. + var headers = HPACKHeaders() + + if trailersOnly { + // Reserve 5 for capacity: 3 for the required headers, and 1 for the + // optional status message. + headers.reserveCapacity(4 + customMetadata.count) + headers.status = "200" + headers.contentType = .protobuf + } else { + // Reserve 2 for capacity: one for the required grpc-status, and + // one for the optional message. + headers.reserveCapacity(2 + customMetadata.count) + } + + headers.grpcStatus = status.code + + if !status.message.isEmpty { + // TODO: this message has to be percent-encoded + headers.grpcStatusMessage = status.message + } + + for metadataPair in customMetadata { + headers.add(name: metadataPair.key, value: metadataPair.value.stringValue) + } + + return headers + } + + mutating func send(status: Status, metadata: Metadata, trailersOnly: Bool) throws -> HPACKHeaders { // Close the server. switch self.state { - case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server can't send status if idle.") case .clientOpenServerOpen(let state): self.state = .clientOpenServerClosed(.init(previousState: state)) + return self.makeTrailers(status: status, customMetadata: metadata, trailersOnly: trailersOnly) case .clientClosedServerOpen(let state): state.compressor?.end() state.decompressor?.end() self.state = .clientClosedServerClosed(.init(previousState: state)) + return self.makeTrailers(status: status, customMetadata: metadata, trailersOnly: trailersOnly) + case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server can't send status if idle.") case .clientOpenServerClosed, .clientClosedServerClosed: throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server can't send anything if closed.") } } - mutating func receive(metadata: Metadata, endStream: Bool) throws -> OnMetadataReceived { + mutating func receive(metadata: HPACKHeaders, endStream: Bool) throws -> OnMetadataReceived { if endStream, case .clientIdleServerIdle = self.state { throw self.assertionFailureAndCreateRPCErrorOnInternalError( """ @@ -634,58 +757,38 @@ extension GRPCStreamStateMachine { switch self.state { case .clientIdleServerIdle(let state): - var preferredCompressionEncoding: CompressionAlgorithm? = nil - if let acceptedEncodings = metadata.acceptedEncodings { - for acceptedEncoding in acceptedEncodings where self.supportedCompressionAlgorithms.contains(where: { $0 == acceptedEncoding }) { - // Found the preferred encoding: use it to compress responses. - preferredCompressionEncoding = acceptedEncoding - break - } - } - - self.state = .clientOpenServerIdle(.init( - previousState: state, - compressionAlgorithm: preferredCompressionEncoding, - decompressionConfiguration: .decompression(metadata.encoding) - )) - guard let contentType = metadata.contentType else { - throw RPCError(code: .invalidArgument, message: "Invalid or empty content-type.") + throw RPCError(code: .invalidArgument, message: "Invalid or empty \(GRPCHTTP2Keys.contentType.rawValue).") } guard let endpoint = metadata.path else { - throw RPCError(code: .unimplemented, message: "No :path header has been set.") + throw RPCError(code: .unimplemented, message: "No \(GRPCHTTP2Keys.path.rawValue) header has been set.") } - // TODO: Should we verify the RPCRouter can handle this endpoint here, // or should we verify that in the handler? - let encodingValues = metadata[stringValues: "grpc-encoding"] + var outboundEncoding: CompressionAlgorithm? = nil + if let clientAdvertisedEncodings = metadata.acceptedEncodings { + for clientAcceptedEncoding in clientAdvertisedEncodings where self.acceptedEncodings.contains(where: { $0 == clientAcceptedEncoding }) { + // Found the preferred encoding: use it to compress responses. + outboundEncoding = clientAcceptedEncoding + break + } + } + + var inboundEncoding: CompressionAlgorithm? = nil + let encodingValues = metadata[GRPCHTTP2Keys.encoding.rawValue] var encodingValuesIterator = encodingValues.makeIterator() if let rawEncoding = encodingValuesIterator.next() { guard encodingValuesIterator.next() == nil else { throw RPCError( code: .invalidArgument, - message: "grpc-encoding must contain no more than one value" - ) - } - guard let encoding = CompressionAlgorithm(rawValue: rawEncoding) else { - let status = Status( - code: .unimplemented, - message: "\(rawEncoding) compression is not supported; supported algorithms are listed in grpc-accept-encoding" + message: "\(GRPCHTTP2Keys.encoding) must contain no more than one value." ) - let trailers = Metadata(dictionaryLiteral: ( - "grpc-accept-encoding", - .string(self.supportedCompressionAlgorithms - .map({ $0.name }) - .joined(separator: ",") - ) - )) - return .reject(status: status, trailers: trailers) } - guard self.supportedCompressionAlgorithms.contains(where: { $0 == encoding }) else { - if self.supportedCompressionAlgorithms.isEmpty { + guard let clientEncoding = CompressionAlgorithm(rawValue: rawEncoding), self.acceptedEncodings.contains(where: { $0 == clientEncoding }) else { + if self.acceptedEncodings.isEmpty { throw RPCError( code: .unimplemented, message: "Compression is not supported" @@ -693,19 +796,27 @@ extension GRPCStreamStateMachine { } else { let status = Status( code: .unimplemented, - message: "\(encoding) compression is not supported; supported algorithms are listed in grpc-accept-encoding" + message: "\(rawEncoding) compression is not supported; supported algorithms are listed in grpc-accept-encoding" ) - let trailers = Metadata(dictionaryLiteral: ( - "grpc-accept-encoding", - .string(self.supportedCompressionAlgorithms + let trailers = HPACKHeaders(httpHeaders: [ + "grpc-accept-encoding": + self.acceptedEncodings .map({ $0.name }) .joined(separator: ",") - ) - )) + ]) return .reject(status: status, trailers: trailers) } } + + inboundEncoding = clientEncoding } + + self.state = .clientOpenServerIdle(.init( + previousState: state, + compressionAlgorithm: outboundEncoding, + decompressionConfiguration: .decompression(inboundEncoding) + )) + return .doNothing case .clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed: throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client shouldn't have sent metadata twice.") @@ -811,17 +922,65 @@ extension MethodDescriptor { } } +private enum GRPCHTTP2Keys: String { + case path = ":path" + case contentType = "content-type" + case encoding = "grpc-encoding" + case acceptEncoding = "grpc-accept-encoding" + case scheme = ":scheme" + case method = ":method" + case te = "te" + case status = ":status" + case grpcStatus = "grpc-status" + case grpcStatusMessage = "grpc-status-message" +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension Metadata { var path: MethodDescriptor? { get { - self.firstString(forKey: .endpoint) + self.firstString(forKey: .path) + .flatMap { MethodDescriptor(fullyQualifiedMethod: $0) } + } + } + + var contentType: ContentType? { + get { + self.firstString(forKey: .contentType) + .flatMap { ContentType(value: $0) } + } + } + + var encoding: CompressionAlgorithm? { + get { + self.firstString(forKey: .encoding).flatMap { CompressionAlgorithm(rawValue: $0) } + } + } + + var acceptedEncodings: [CompressionAlgorithm]? { + get { + self.firstString(forKey: .acceptEncoding)? + .split(separator: ",") + .compactMap { CompressionAlgorithm(rawValue: String($0)) } + } + } + + private func firstString(forKey key: GRPCHTTP2Keys) -> String? { + self[stringValues: key.rawValue].first(where: { _ in true }) + } +} + +extension HPACKHeaders { + var path: MethodDescriptor? { + get { + self.firstString(forKey: .path) .flatMap { MethodDescriptor(fullyQualifiedMethod: $0) } } set { if let newValue { - self.replaceOrAddString(newValue.fullyQualifiedMethod, forKey: .endpoint) + self.replaceOrAddString(newValue.fullyQualifiedMethod, forKey: .path) } else { - self.removeAllValues(forKey: .endpoint) + self.removeAllValues(forKey: .path) } } } @@ -868,23 +1027,96 @@ extension Metadata { } } - private enum GRPCHTTP2Keys: String { - case endpoint = ":path" - case contentType = "content-type" - case encoding = "grpc-encoding" - case acceptEncoding = "grpc-accept-encoding" + var scheme: Scheme? { + get { + self.firstString(forKey: .scheme).flatMap { Scheme(rawValue: $0) } + } + set { + if let newValue { + self.replaceOrAddString(newValue.rawValue, forKey: .scheme) + } else { + self.removeAllValues(forKey: .scheme) + } + } + } + + var method: String? { + get { + self.firstString(forKey: .method) + } + set { + if let newValue { + self.replaceOrAddString(newValue, forKey: .method) + } else { + self.removeAllValues(forKey: .method) + } + } + } + + var te: String? { + get { + self.firstString(forKey: .te) + } + set { + if let newValue { + self.replaceOrAddString(newValue, forKey: .te) + } else { + self.removeAllValues(forKey: .te) + } + } + } + + var status: String? { + get { + self.firstString(forKey: .status) + } + set { + if let newValue { + self.replaceOrAddString(newValue, forKey: .status) + } else { + self.removeAllValues(forKey: .status) + } + } + } + + var grpcStatus: Status.Code? { + get { + self.firstString(forKey: .grpcStatus) + .flatMap { Int($0) } + .flatMap { Status.Code(rawValue: $0) } + } + set { + if let newValue { + self.replaceOrAddString(String(newValue.rawValue), forKey: .grpcStatus) + } else { + self.removeAllValues(forKey: .grpcStatus) + } + } + } + + var grpcStatusMessage: String? { + get { + self.firstString(forKey: .grpcStatusMessage) + } + set { + if let newValue { + self.replaceOrAddString(newValue, forKey: .grpcStatusMessage) + } else { + self.removeAllValues(forKey: .grpcStatusMessage) + } + } } private func firstString(forKey key: GRPCHTTP2Keys) -> String? { - self[stringValues: key.rawValue].first(where: { _ in true }) + self[key.rawValue].first(where: { _ in true }) } private mutating func replaceOrAddString(_ value: String, forKey key: GRPCHTTP2Keys) { - self.replaceOrAddString(value, forKey: key.rawValue) + self.replaceOrAdd(name: value, value: key.rawValue) } private mutating func removeAllValues(forKey key: GRPCHTTP2Keys) { - self.removeAllValues(forKey: key.rawValue) + self.remove(name: key.rawValue) } } diff --git a/Sources/GRPCHTTP2Core/MessageEncoding.swift b/Sources/GRPCHTTP2Core/MessageEncoding.swift deleted file mode 100644 index 69fe2d91c..000000000 --- a/Sources/GRPCHTTP2Core/MessageEncoding.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// File.swift -// -// -// Created by Gus Cairo on 14/02/2024. -// - -import Foundation diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index cf29f2520..b2e58ee01 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -17,21 +17,34 @@ import GRPCCore import XCTest import NIOCore +import NIOHPACK @testable import GRPCHTTP2Core final class GRPCStreamClientStateMachineTests: XCTestCase { - private let testMetadata: Metadata = [":path": "test/test"] - private let testMetadataWithDeflateCompression: Metadata = [ - ":path": "test/test", - "grpc-encoding": "deflate" - ] + private let testMetadata: Metadata = [] private func makeClientStateMachine() -> GRPCStreamStateMachine { GRPCStreamStateMachine( configuration: .client( + methodDescriptor: .init(service: "test", method: "test"), + scheme: .http, maximumPayloadSize: 100, - supportedCompressionAlgorithms: [.deflate] + outboundEncoding: nil, + acceptedEncodings: [.deflate] + ), + skipAssertions: true + ) + } + + private func makeClientStateMachineWithCompression() -> GRPCStreamStateMachine { + GRPCStreamStateMachine( + configuration: .client( + methodDescriptor: .init(service: "test", method: "test"), + scheme: .http, + maximumPayloadSize: 100, + outboundEncoding: .deflate, + acceptedEncodings: [.deflate] ), skipAssertions: true ) @@ -268,7 +281,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // This operation is never allowed on the client. XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(status: "test", trailingMetadata: .init())) { error in + try stateMachine.send(status: Status(code: .ok, message: ""), metadata: .init(), trailersOnly: false)) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client cannot send status and trailer.") } @@ -282,7 +295,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // This operation is never allowed on the client. XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(status: "test", trailingMetadata: .init())) { error in + try stateMachine.send(status: Status(code: .ok, message: ""), metadata: .init(), trailersOnly: false)) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client cannot send status and trailer.") } @@ -299,7 +312,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // This operation is never allowed on the client. XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(status: "test", trailingMetadata: .init())) { error in + try stateMachine.send(status: Status(code: .ok, message: ""), metadata: .init(), trailersOnly: false)) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client cannot send status and trailer.") } @@ -319,7 +332,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // This operation is never allowed on the client. XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(status: "test", trailingMetadata: .init())) { error in + try stateMachine.send(status: Status(code: .ok, message: ""), metadata: .init(), trailersOnly: false)) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client cannot send status and trailer.") } @@ -336,7 +349,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // This operation is never allowed on the client. XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(status: "test", trailingMetadata: .init())) { error in + try stateMachine.send(status: Status(code: .ok, message: ""), metadata: .init(), trailersOnly: false)) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client cannot send status and trailer.") } @@ -356,7 +369,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // This operation is never allowed on the client. XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(status: "test", trailingMetadata: .init())) { error in + try stateMachine.send(status: Status(code: .ok, message: ""), metadata: .init(), trailersOnly: false)) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client cannot send status and trailer.") } @@ -379,7 +392,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // This operation is never allowed on the client. XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(status: "test", trailingMetadata: .init())) { error in + try stateMachine.send(status: Status(code: .ok, message: ""), metadata: .init(), trailersOnly: false)) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client cannot send status and trailer.") } @@ -766,10 +779,10 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { } func testNextOutboundMessageWhenClientOpenAndServerIdle_WithCompression() throws { - var stateMachine = makeClientStateMachine() + var stateMachine = makeClientStateMachineWithCompression() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadataWithDeflateCompression)) + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) var request = try stateMachine.nextOutboundMessage() XCTAssertNil(request) @@ -812,10 +825,10 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { } func testNextOutboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { - var stateMachine = makeClientStateMachine() + var stateMachine = makeClientStateMachineWithCompression() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadataWithDeflateCompression)) + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) @@ -969,10 +982,10 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { } func testNextInboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { - var stateMachine = makeClientStateMachine() + var stateMachine = makeClientStateMachineWithCompression() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadataWithDeflateCompression)) + XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) @@ -1090,24 +1103,25 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { } final class GRPCStreamServerStateMachineTests: XCTestCase { - private let testMetadata: Metadata = [ + private let receivedHeaders: HPACKHeaders = [ ":path": "test/test", "content-type": "application/grpc" ] - private let testMetadataWithDeflateCompression: Metadata = [ + private let receivedHeadersWithDeflateCompression: HPACKHeaders = [ ":path": "test/test", "content-type": "application/grpc", "grpc-encoding": "deflate" ] - private let testMetadataWithoutContentType: Metadata = [":path": "test/test"] - private let testMetadataWithInvalidContentType: Metadata = ["content-type": "invalid/invalid"] - private let testMetadataWithoutEndpoint: Metadata = ["content-type": "application/grpc"] + private let receivedHeadersWithoutContentType: HPACKHeaders = [":path": "test/test"] + private let receivedHeadersWithInvalidContentType: HPACKHeaders = ["content-type": "invalid/invalid"] + private let receivedHeadersWithoutEndpoint: HPACKHeaders = ["content-type": "application/grpc"] private func makeServerStateMachine() -> GRPCStreamStateMachine { GRPCStreamStateMachine( configuration: .server( + scheme: .http, maximumPayloadSize: 100, - supportedCompressionAlgorithms: [] + acceptedEncodings: [] ), skipAssertions: true ) @@ -1116,8 +1130,9 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { private func makeServerStateMachineWithCompression() -> GRPCStreamStateMachine { GRPCStreamStateMachine( configuration: .server( + scheme: .http, maximumPayloadSize: 100, - supportedCompressionAlgorithms: [.deflate] + acceptedEncodings: [.deflate] ), skipAssertions: true ) @@ -1139,16 +1154,16 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) } func testSendMetadataWhenClientOpenAndServerOpen() throws { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1165,7 +1180,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1184,7 +1199,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) // Close client XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) @@ -1198,7 +1213,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1217,7 +1232,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1251,7 +1266,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) // Now send a message XCTAssertThrowsError(ofType: RPCError.self, @@ -1265,7 +1280,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1278,7 +1293,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1298,7 +1313,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) // Close client XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) @@ -1314,7 +1329,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1331,7 +1346,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1356,7 +1371,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(status: "test", trailingMetadata: .init())) { error in + try stateMachine.send(status: .init(code: .ok, message: ""), metadata: .init(), trailersOnly: false)) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send status if idle.") } @@ -1366,10 +1381,10 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(status: "test", trailingMetadata: .init())) { error in + try stateMachine.send(status: .init(code: .ok, message: ""), metadata: .init(), trailersOnly: false)) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send status if idle.") } @@ -1379,12 +1394,12 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - XCTAssertNoThrow(try stateMachine.send(status: "test", trailingMetadata: .init())) + XCTAssertNoThrow(try stateMachine.send(status: .init(code: .ok, message: ""), metadata: .init(), trailersOnly: false)) // Try sending another message: it should fail because server is now closed. XCTAssertThrowsError(ofType: RPCError.self, @@ -1398,7 +1413,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1407,7 +1422,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(status: "test", trailingMetadata: .init())) { error in + try stateMachine.send(status: .init(code: .ok, message: ""), metadata: .init(), trailersOnly: false)) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send anything if closed.") } @@ -1417,13 +1432,13 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) // Close client XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(status: "test", trailingMetadata: .init())) { error in + try stateMachine.send(status: .init(code: .ok, message: ""), metadata: .init(), trailersOnly: false)) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send status if idle.") } @@ -1433,7 +1448,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1442,14 +1457,14 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) // Client is closed but may still be awaiting response, so we should be able to send it. - XCTAssertNoThrow(try stateMachine.send(status: "test", trailingMetadata: .init())) + XCTAssertNoThrow(try stateMachine.send(status: .init(code: .ok, message: ""), metadata: .init(), trailersOnly: false)) } func testSendStatusAndTrailersWhenClientClosedAndServerClosed() { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1461,7 +1476,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(status: "test", trailingMetadata: .init())) { error in + try stateMachine.send(status: .init(code: .ok, message: ""), metadata: .init(), trailersOnly: false)) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send anything if closed.") } @@ -1472,7 +1487,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { func testReceiveMetadataWhenClientIdleAndServerIdle() throws { var stateMachine = makeServerStateMachine() - let action = try stateMachine.receive(metadata: self.testMetadata, endStream: false) + let action = try stateMachine.receive(metadata: self.receivedHeaders, endStream: false) guard case .doNothing = action else { XCTFail("Expected action to be doNothing") return @@ -1486,7 +1501,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // sending a message with endStream set. If they send metadata it has to be // to open the stream (initial metadata). XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: self.testMetadata, endStream: true)) { error in + try stateMachine.receive(metadata: self.receivedHeaders, endStream: true)) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual( error.message, @@ -1502,7 +1517,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: self.testMetadataWithoutContentType, endStream: false)) { error in + try stateMachine.receive(metadata: self.receivedHeadersWithoutContentType, endStream: false)) { error in XCTAssertEqual(error.code, .invalidArgument) XCTAssertEqual(error.message, "Invalid or empty content-type.") } @@ -1512,7 +1527,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: self.testMetadataWithInvalidContentType, endStream: false)) { error in + try stateMachine.receive(metadata: self.receivedHeadersWithInvalidContentType, endStream: false)) { error in XCTAssertEqual(error.code, .invalidArgument) XCTAssertEqual(error.message, "Invalid or empty content-type.") } @@ -1522,7 +1537,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: self.testMetadataWithoutEndpoint, endStream: false)) { error in + try stateMachine.receive(metadata: self.receivedHeadersWithoutEndpoint, endStream: false)) { error in XCTAssertEqual(error.code, .unimplemented) XCTAssertEqual(error.message, "No :path header has been set.") } @@ -1534,7 +1549,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Try opening client if no compression has been configured in the server: // should fail. XCTAssertThrowsError(ofType: RPCError.self, - try noCompressionStateMachine.receive(metadata: self.testMetadataWithDeflateCompression, endStream: false)) { error in + try noCompressionStateMachine.receive(metadata: self.receivedHeadersWithDeflateCompression, endStream: false)) { error in XCTAssertEqual(error.code, .unimplemented) XCTAssertEqual(error.message, "Compression is not supported") } @@ -1547,11 +1562,11 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) // Try receiving initial metadata again - should fail XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: self.testMetadata, endStream: false)) { error in + try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client shouldn't have sent metadata twice.") } @@ -1561,13 +1576,13 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: self.testMetadata, endStream: false)) { error in + try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client shouldn't have sent metadata twice.") } @@ -1577,7 +1592,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1586,7 +1601,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: self.testMetadata, endStream: false)) { error in + try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client shouldn't have sent metadata twice.") } @@ -1596,13 +1611,13 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) // Close client XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: self.testMetadata, endStream: false)) { error in + try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't have sent metadata if closed.") } @@ -1612,7 +1627,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1622,7 +1637,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: self.testMetadata, endStream: false)) { error in + try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't have sent metadata if closed.") } @@ -1632,7 +1647,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1644,7 +1659,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: self.testMetadata, endStream: false)) { error in + try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't have sent metadata if closed.") } @@ -1666,7 +1681,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) // Receive messages successfully: the second one should close client. XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: false)) @@ -1684,7 +1699,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1705,7 +1720,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1721,7 +1736,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) // Close client XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) @@ -1737,7 +1752,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1756,7 +1771,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1790,7 +1805,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.nextOutboundMessage()) { error in @@ -1803,7 +1818,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.nextOutboundMessage()) { error in @@ -1816,7 +1831,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1839,7 +1854,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachineWithCompression() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadataWithDeflateCompression, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeadersWithDeflateCompression, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1865,7 +1880,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1884,7 +1899,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) // Close client XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) @@ -1900,7 +1915,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1936,7 +1951,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1970,7 +1985,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) XCTAssertNil(stateMachine.nextInboundMessage()) } @@ -1979,7 +1994,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -2001,7 +2016,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachineWithCompression() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadataWithDeflateCompression, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeadersWithDeflateCompression, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -2025,7 +2040,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -2050,7 +2065,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) // Close client XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) @@ -2062,7 +2077,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -2089,7 +2104,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.testMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) From 35eef1a4c2e254982e49ec870dcd4eba362eb871 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Fri, 16 Feb 2024 11:18:14 +0000 Subject: [PATCH 13/51] Merge both state machines --- .../GRPCStreamStateMachine.swift | 1093 ++++++++--------- .../GRPCStreamStateMachineTests.swift | 24 +- 2 files changed, 543 insertions(+), 574 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 2cbf47ee5..39067ec0d 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -29,34 +29,21 @@ enum Scheme: String { case https } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -fileprivate protocol GRPCStreamStateMachineProtocol { - var state: GRPCStreamStateMachineState { get set } - - mutating func send(metadata: Metadata) throws -> HPACKHeaders - mutating func send(message: [UInt8], endStream: Bool) throws - mutating func send(status: Status, metadata: Metadata, trailersOnly: Bool) throws -> HPACKHeaders +enum GRPCStreamStateMachineConfiguration { + case client(ClientConfiguration) + case server(ServerConfiguration) - mutating func receive(metadata: HPACKHeaders, endStream: Bool) throws -> OnMetadataReceived - mutating func receive(message: ByteBuffer, endStream: Bool) throws + struct ClientConfiguration { + var methodDescriptor: MethodDescriptor + var scheme: Scheme + var outboundEncoding: CompressionAlgorithm? + var acceptedEncodings: [CompressionAlgorithm] + } - mutating func nextOutboundMessage() throws -> ByteBuffer? - mutating func nextInboundMessage() -> [UInt8]? -} - -enum GRPCStreamStateMachineConfiguration { - case client( - methodDescriptor: MethodDescriptor, - scheme: Scheme, - maximumPayloadSize: Int, - outboundEncoding: CompressionAlgorithm?, - acceptedEncodings: [CompressionAlgorithm] - ) - case server( - scheme: Scheme, - maximumPayloadSize: Int, - acceptedEncodings: [CompressionAlgorithm] - ) + struct ServerConfiguration { + var scheme: Scheme + var acceptedEncodings: [CompressionAlgorithm] + } } enum GRPCStreamStateMachineState { @@ -67,7 +54,7 @@ enum GRPCStreamStateMachineState { case clientClosedServerIdle(ClientClosedServerIdleState) case clientClosedServerOpen(ClientClosedServerOpenState) case clientClosedServerClosed(ClientClosedServerClosedState) - + struct ClientIdleServerIdleState { let maximumPayloadSize: Int } @@ -160,7 +147,7 @@ enum GRPCStreamStateMachineState { ) self.deframer = NIOSingleStepByteToMessageProcessor(decoder) } - + self.inboundMessageBuffer = previousState.inboundMessageBuffer } } @@ -284,630 +271,612 @@ enum GRPCStreamStateMachineState { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) struct GRPCStreamStateMachine { - private var _stateMachine: GRPCStreamStateMachineProtocol + private var state: GRPCStreamStateMachineState + private var configuration: GRPCStreamStateMachineConfiguration + private var skipAssertions: Bool init( configuration: GRPCStreamStateMachineConfiguration, + maximumPayloadSize: Int, skipAssertions: Bool = false ) { - switch configuration { - case .client( - let methodDescriptor, - let scheme, - let maximumPayloadSize, - let outboundEncoding, - let acceptedEncodings - ): - self._stateMachine = Client( - methodDescriptor: methodDescriptor, - scheme: scheme, - maximumPayloadSize: maximumPayloadSize, - outboundEncoding: outboundEncoding, - acceptedEncodings: acceptedEncodings, - skipAssertions: skipAssertions - ) - case .server( - let scheme, - let maximumPayloadSize, - let acceptedEncodings - ): - self._stateMachine = Server( - maximumPayloadSize: maximumPayloadSize, - acceptedEncodings: acceptedEncodings, - skipAssertions: skipAssertions - ) - } + self.state = .clientIdleServerIdle(.init(maximumPayloadSize: maximumPayloadSize)) + self.configuration = configuration + self.skipAssertions = skipAssertions } mutating func send(metadata: Metadata) throws -> HPACKHeaders { - try self._stateMachine.send(metadata: metadata) + switch self.configuration { + case .client(let clientConfiguration): + return try clientSend(metadata: metadata, configuration: clientConfiguration) + case .server: + return try serverSend(metadata: metadata) + } } mutating func send(message: [UInt8], endStream: Bool) throws { - try self._stateMachine.send(message: message, endStream: endStream) + switch self.configuration { + case .client: + try clientSend(message: message, endStream: endStream) + case .server: + try serverSend(message: message, endStream: endStream) + } } mutating func send(status: Status, metadata: Metadata, trailersOnly: Bool) throws -> HPACKHeaders { - try self._stateMachine.send(status: status, metadata: metadata, trailersOnly: trailersOnly) + switch self.configuration { + case .client: + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client cannot send status and trailer.") + case .server: + return try serverSend(status: status, metadata: metadata, trailersOnly: trailersOnly) + } } mutating func receive(metadata: HPACKHeaders, endStream: Bool) throws -> OnMetadataReceived { - try self._stateMachine.receive(metadata: metadata, endStream: endStream) + switch self.configuration { + case .client: + return try clientReceive(metadata: metadata, endStream: endStream) + case .server(let serverConfiguration): + return try serverReceive(metadata: metadata, endStream: endStream, configuration: serverConfiguration) + } } mutating func receive(message: ByteBuffer, endStream: Bool) throws { - try self._stateMachine.receive(message: message, endStream: endStream) + switch self.configuration { + case .client: + try clientReceive(message: message, endStream: endStream) + case .server: + try serverReceive(message: message, endStream: endStream) + } } mutating func nextOutboundMessage() throws -> ByteBuffer? { - try self._stateMachine.nextOutboundMessage() + switch self.configuration { + case .client: + return try clientNextOutboundMessage() + case .server: + return try serverNextOutboundMessage() + } } mutating func nextInboundMessage() -> [UInt8]? { - self._stateMachine.nextInboundMessage() + switch self.configuration { + case .client: + return clientNextInboundMessage() + case .server: + return serverNextInboundMessage() + } } } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension GRPCStreamStateMachine { - struct Client: GRPCStreamStateMachineProtocol { - fileprivate var state: GRPCStreamStateMachineState + private func makeClientHeaders( + methodDescriptor: MethodDescriptor, + scheme: Scheme, + outboundEncoding: CompressionAlgorithm?, + acceptedEncodings: [CompressionAlgorithm], + customMetadata: Metadata + ) -> HPACKHeaders { + var headers = HPACKHeaders() + headers.reserveCapacity(7 + customMetadata.count) - private let methodDescriptor: MethodDescriptor - private let scheme: Scheme - private let outboundEncoding: CompressionAlgorithm? - private let acceptedEncodings: [CompressionAlgorithm] - private let skipAssertions: Bool - - init( - methodDescriptor: MethodDescriptor, - scheme: Scheme, - maximumPayloadSize: Int, - outboundEncoding: CompressionAlgorithm?, - acceptedEncodings: [CompressionAlgorithm], - skipAssertions: Bool - ) { - self.state = .clientIdleServerIdle(.init(maximumPayloadSize: maximumPayloadSize)) - self.methodDescriptor = methodDescriptor - self.scheme = scheme - self.outboundEncoding = outboundEncoding - self.acceptedEncodings = acceptedEncodings - self.skipAssertions = skipAssertions + // Add required headers + // See https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests + headers.path = methodDescriptor + headers.scheme = scheme + headers.method = "POST" + headers.contentType = .protobuf + headers.te = "trailers" // Used to detect incompatible proxies + + if let encoding = outboundEncoding { + headers.encoding = encoding } - private func makeClientHeaders(from metadata: Metadata) -> HPACKHeaders { - var headers = HPACKHeaders() - headers.reserveCapacity(7 + metadata.count) - - // Add required headers - // See https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests - headers.path = self.methodDescriptor - headers.scheme = self.scheme - headers.method = "POST" - headers.contentType = .protobuf - headers.te = "trailers" // Used to detect incompatible proxies - - if let encoding = self.outboundEncoding { - headers.encoding = encoding + if !acceptedEncodings.isEmpty { + headers.acceptedEncodings = acceptedEncodings + } + + for metadataPair in customMetadata { + headers.add(name: metadataPair.key, value: metadataPair.value.stringValue) + } + + return headers + } + + private mutating func clientSend( + metadata: Metadata, + configuration: GRPCStreamStateMachineConfiguration.ClientConfiguration + ) throws -> HPACKHeaders { + // Client sends metadata only when opening the stream. + switch self.state { + case .clientIdleServerIdle(let state): + self.state = .clientOpenServerIdle(.init( + previousState: state, + compressionAlgorithm: configuration.outboundEncoding, + decompressionConfiguration: .decompressionNotYetKnown + )) + return self.makeClientHeaders( + methodDescriptor: configuration.methodDescriptor, + scheme: configuration.scheme, + outboundEncoding: configuration.outboundEncoding, + acceptedEncodings: configuration.acceptedEncodings, + customMetadata: metadata + ) + case .clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed: + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client is already open: shouldn't be sending metadata.") + case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client is closed: can't send metadata.") + } + } + + private mutating func clientSend(message: [UInt8], endStream: Bool) throws { + // Client sends message. + switch self.state { + case .clientIdleServerIdle: + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client not yet open.") + case .clientOpenServerIdle(var state): + state.framer.append(message) + if endStream { + self.state = .clientClosedServerIdle(.init(previousState: state)) + } else { + self.state = .clientOpenServerIdle(state) } - - if !self.acceptedEncodings.isEmpty { - headers.acceptedEncodings = self.acceptedEncodings + case .clientOpenServerOpen(var state): + state.framer.append(message) + if endStream { + self.state = .clientClosedServerOpen(.init(previousState: state)) + } else { + self.state = .clientOpenServerOpen(state) } - - for metadataPair in metadata { - headers.add(name: metadataPair.key, value: metadataPair.value.stringValue) + case .clientOpenServerClosed(let state): + // The server has closed, so it makes no sense to send the rest of the request. + // However, do close if endStream is set. + if endStream { + self.state = .clientClosedServerClosed(.init(previousState: state)) } - - return headers + case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client is closed, cannot send a message.") } - - mutating func send(metadata: Metadata) throws -> HPACKHeaders { - // Client sends metadata only when opening the stream. - switch self.state { - case .clientIdleServerIdle(let state): - self.state = .clientOpenServerIdle(.init( + } + + /// Returns the client's next request to the server. + /// - Returns: The request to be made to the server. + private mutating func clientNextOutboundMessage() throws -> ByteBuffer? { + switch self.state { + case .clientIdleServerIdle: + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client is not open yet.") + case .clientOpenServerIdle(var state): + let request = try state.framer.next(compressor: state.compressor) + self.state = .clientOpenServerIdle(state) + return request + case .clientOpenServerOpen(var state): + let request = try state.framer.next(compressor: state.compressor) + self.state = .clientOpenServerOpen(state) + return request + case .clientOpenServerClosed(var state): + // Server may have closed but still be waiting for client messages, + // for example if it's a client-streaming RPC. + let request = try state.framer.next(compressor: state.compressor) + self.state = .clientOpenServerClosed(state) + return request + case .clientClosedServerIdle(var state): + let request = try state.framer.next(compressor: state.compressor) + self.state = .clientClosedServerIdle(state) + return request + case .clientClosedServerOpen(var state): + let request = try state.framer.next(compressor: state.compressor) + self.state = .clientClosedServerOpen(state) + return request + case .clientClosedServerClosed: + // Nothing to do if both are closed. + return nil + } + } + + private mutating func clientReceive(metadata: HPACKHeaders, endStream: Bool) throws -> OnMetadataReceived { + switch self.state { + case .clientIdleServerIdle: + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server cannot have sent metadata if the client is idle.") + case .clientOpenServerIdle(let state): + if endStream { + // This is a trailers-only response: close server. + self.state = .clientOpenServerClosed(.init(previousState: state)) + } else { + self.state = .clientOpenServerOpen(.init( previousState: state, - compressionAlgorithm: self.outboundEncoding, - decompressionConfiguration: .decompressionNotYetKnown + decompressionAlgorithm: metadata.encoding )) - return self.makeClientHeaders(from: metadata) - case .clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client is already open: shouldn't be sending metadata.") - case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client is closed: can't send metadata.") } - } - - mutating func send(message: [UInt8], endStream: Bool) throws { - // Client sends message. - switch self.state { - case .clientIdleServerIdle: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client not yet open.") - case .clientOpenServerIdle(var state): - state.framer.append(message) - if endStream { - self.state = .clientClosedServerIdle(.init(previousState: state)) - } else { - self.state = .clientOpenServerIdle(state) - } - case .clientOpenServerOpen(var state): - state.framer.append(message) - if endStream { - self.state = .clientClosedServerOpen(.init(previousState: state)) - } else { - self.state = .clientOpenServerOpen(state) - } - case .clientOpenServerClosed(let state): - // The server has closed, so it makes no sense to send the rest of the request. - // However, do close if endStream is set. - if endStream { - self.state = .clientClosedServerClosed(.init(previousState: state)) - } - case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client is closed, cannot send a message.") + case .clientOpenServerOpen(let state): + if endStream { + self.state = .clientOpenServerClosed(.init(previousState: state)) + } else { + // This state is valid: server can send trailing metadata without END_STREAM + // set, and follow it with an empty message frame where the flag *is* set. + () + // TODO: I believe we should set some flag in the state to signal that + // we're expecting an empty data frame with END_STREAM set; otherwise, + // we could get an infinite number of metadata frames from the server - + // not sure this should be allowed. } - } - - mutating func send(status: Status, metadata: Metadata, trailersOnly: Bool) throws -> HPACKHeaders { - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client cannot send status and trailer.") - } - - /// Returns the client's next request to the server. - /// - Returns: The request to be made to the server. - mutating func nextOutboundMessage() throws -> ByteBuffer? { - switch self.state { - case .clientIdleServerIdle: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client is not open yet.") - case .clientOpenServerIdle(var state): - let request = try state.framer.next(compressor: state.compressor) - self.state = .clientOpenServerIdle(state) - return request - case .clientOpenServerOpen(var state): - let request = try state.framer.next(compressor: state.compressor) - self.state = .clientOpenServerOpen(state) - return request - case .clientOpenServerClosed(var state): - // Server may have closed but still be waiting for client messages, - // for example if it's a client-streaming RPC. - let request = try state.framer.next(compressor: state.compressor) - self.state = .clientOpenServerClosed(state) - return request - case .clientClosedServerIdle(var state): - let request = try state.framer.next(compressor: state.compressor) - self.state = .clientClosedServerIdle(state) - return request - case .clientClosedServerOpen(var state): - let request = try state.framer.next(compressor: state.compressor) - self.state = .clientClosedServerOpen(state) - return request - case .clientClosedServerClosed: - // Nothing to do if both are closed. - return nil + case .clientClosedServerIdle(let state): + if endStream { + // This is a trailers-only response. + self.state = .clientClosedServerClosed(.init(previousState: state)) + } else { + self.state = .clientClosedServerOpen(.init( + previousState: state, + decompressionAlgorithm: metadata.encoding + )) } - } - - mutating func receive(metadata: HPACKHeaders, endStream: Bool) throws -> OnMetadataReceived { - switch self.state { - case .clientIdleServerIdle: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server cannot have sent metadata if the client is idle.") - case .clientOpenServerIdle(let state): - if endStream { - // This is a trailers-only response: close server. - self.state = .clientOpenServerClosed(.init(previousState: state)) - } else { - self.state = .clientOpenServerOpen(.init( - previousState: state, - decompressionAlgorithm: metadata.encoding - )) - } - case .clientOpenServerOpen(let state): - if endStream { - self.state = .clientOpenServerClosed(.init(previousState: state)) - } else { - // This state is valid: server can send trailing metadata without END_STREAM - // set, and follow it with an empty message frame where the flag *is* set. - () - // TODO: I believe we should set some flag in the state to signal that - // we're expecting an empty data frame with END_STREAM set; otherwise, - // we could get an infinite number of metadata frames from the server - - // not sure this should be allowed. - } - case .clientClosedServerIdle(let state): - if endStream { - // This is a trailers-only response. - self.state = .clientClosedServerClosed(.init(previousState: state)) - } else { - self.state = .clientClosedServerOpen(.init( - previousState: state, - decompressionAlgorithm: metadata.encoding - )) - } - case .clientClosedServerOpen(let state): - if endStream { - state.compressor?.end() - state.decompressor?.end() - self.state = .clientClosedServerClosed(.init(previousState: state)) - } else { - // This state is valid: server can send trailing metadata without END_STREAM - // set, and follow it with an empty message frame where the flag *is* set. - () - // TODO: I believe we should set some flag in the state to signal that - // we're expecting an empty data frame with END_STREAM set; otherwise, - // we could get an infinite number of metadata frames from the server - - // not sure this should be allowed. - } - case .clientOpenServerClosed, .clientClosedServerClosed: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server is closed, nothing could have been sent.") + case .clientClosedServerOpen(let state): + if endStream { + state.compressor?.end() + state.decompressor?.end() + self.state = .clientClosedServerClosed(.init(previousState: state)) + } else { + // This state is valid: server can send trailing metadata without END_STREAM + // set, and follow it with an empty message frame where the flag *is* set. + () + // TODO: I believe we should set some flag in the state to signal that + // we're expecting an empty data frame with END_STREAM set; otherwise, + // we could get an infinite number of metadata frames from the server - + // not sure this should be allowed. } - - return .doNothing + case .clientOpenServerClosed, .clientClosedServerClosed: + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server is closed, nothing could have been sent.") } - mutating func receive(message: ByteBuffer, endStream: Bool) throws { - // This is a message received by the client, from the server. - switch self.state { - case .clientIdleServerIdle: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Cannot have received anything from server if client is not yet open.") - case .clientOpenServerIdle, .clientClosedServerIdle: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server cannot have sent a message before sending the initial metadata.") - case .clientOpenServerOpen(var state): - try state.deframer.process(buffer: message) { deframedMessage in - state.inboundMessageBuffer.append(deframedMessage) - } - if endStream { - self.state = .clientOpenServerClosed(.init(previousState: state)) - } else { - self.state = .clientOpenServerOpen(state) - } - case .clientClosedServerOpen(var state): - // The client may have sent the end stream and thus it's closed, - // but the server may still be responding. - try state.deframer.process(buffer: message) { deframedMessage in - state.inboundMessageBuffer.append(deframedMessage) - } - if endStream { - self.state = .clientClosedServerClosed(.init(previousState: state)) - } else { - self.state = .clientClosedServerOpen(state) - } - case .clientOpenServerClosed: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Cannot have received anything from a closed server.") - case .clientClosedServerClosed: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Shouldn't have received anything if both client and server are closed.") + return .doNothing + } + + private mutating func clientReceive(message: ByteBuffer, endStream: Bool) throws { + // This is a message received by the client, from the server. + switch self.state { + case .clientIdleServerIdle: + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Cannot have received anything from server if client is not yet open.") + case .clientOpenServerIdle, .clientClosedServerIdle: + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server cannot have sent a message before sending the initial metadata.") + case .clientOpenServerOpen(var state): + try state.deframer.process(buffer: message) { deframedMessage in + state.inboundMessageBuffer.append(deframedMessage) } - } - - mutating func nextInboundMessage() -> [UInt8]? { - switch self.state { - case .clientOpenServerOpen(var state): - let message = state.inboundMessageBuffer.pop() + if endStream { + self.state = .clientOpenServerClosed(.init(previousState: state)) + } else { self.state = .clientOpenServerOpen(state) - return message - case .clientOpenServerClosed(var state): - let message = state.inboundMessageBuffer.pop() - self.state = .clientOpenServerClosed(state) - return message - case .clientClosedServerOpen(var state): - let message = state.inboundMessageBuffer.pop() + } + case .clientClosedServerOpen(var state): + // The client may have sent the end stream and thus it's closed, + // but the server may still be responding. + try state.deframer.process(buffer: message) { deframedMessage in + state.inboundMessageBuffer.append(deframedMessage) + } + if endStream { + self.state = .clientClosedServerClosed(.init(previousState: state)) + } else { self.state = .clientClosedServerOpen(state) - return message - case .clientClosedServerClosed(var state): - let message = state.inboundMessageBuffer.pop() - self.state = .clientClosedServerClosed(state) - return message - case .clientIdleServerIdle, - .clientOpenServerIdle, - .clientClosedServerIdle: - return nil } + case .clientOpenServerClosed: + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Cannot have received anything from a closed server.") + case .clientClosedServerClosed: + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Shouldn't have received anything if both client and server are closed.") } - - private func assertionFailureAndCreateRPCErrorOnInternalError(_ message: String, line: UInt = #line) -> RPCError { - if !self.skipAssertions { - assertionFailure(message, line: line) - } - return RPCError(code: .internalError, message: message) + } + + private mutating func clientNextInboundMessage() -> [UInt8]? { + switch self.state { + case .clientOpenServerOpen(var state): + let message = state.inboundMessageBuffer.pop() + self.state = .clientOpenServerOpen(state) + return message + case .clientOpenServerClosed(var state): + let message = state.inboundMessageBuffer.pop() + self.state = .clientOpenServerClosed(state) + return message + case .clientClosedServerOpen(var state): + let message = state.inboundMessageBuffer.pop() + self.state = .clientClosedServerOpen(state) + return message + case .clientClosedServerClosed(var state): + let message = state.inboundMessageBuffer.pop() + self.state = .clientClosedServerClosed(state) + return message + case .clientIdleServerIdle, + .clientOpenServerIdle, + .clientClosedServerIdle: + return nil + } + } + + private func assertionFailureAndCreateRPCErrorOnInternalError(_ message: String, line: UInt = #line) -> RPCError { + if !self.skipAssertions { + assertionFailure(message, line: line) } + return RPCError(code: .internalError, message: message) } } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension GRPCStreamStateMachine { - struct Server: GRPCStreamStateMachineProtocol { - fileprivate var state: GRPCStreamStateMachineState - let acceptedEncodings: [CompressionAlgorithm] - private let skipAssertions: Bool + private func makeResponseHeaders(outboundEncoding: CompressionAlgorithm?, customMetadata: Metadata) -> HPACKHeaders { + // Response headers always contain :status (HTTP Status 200) and content-type. + // They may also contain grpc-encoding, grpc-accept-encoding, and custom metadata. + var headers = HPACKHeaders() + headers.reserveCapacity(4 + customMetadata.count) - init( - maximumPayloadSize: Int, - acceptedEncodings: [CompressionAlgorithm], - skipAssertions: Bool - ) { - self.state = .clientIdleServerIdle(.init(maximumPayloadSize: maximumPayloadSize)) - self.acceptedEncodings = acceptedEncodings - self.skipAssertions = skipAssertions + headers.status = "200" + headers.contentType = .protobuf + + if let outboundEncoding { + headers.encoding = outboundEncoding } - private func makeResponseHeaders(outboundEncoding: CompressionAlgorithm?, customMetadata: Metadata) -> HPACKHeaders { - // Response headers always contain :status (HTTP Status 200) and content-type. - // They may also contain grpc-encoding, grpc-accept-encoding, and custom metadata. - var headers = HPACKHeaders() - headers.reserveCapacity(4 + customMetadata.count) - - headers.status = "200" - headers.contentType = .protobuf - - if let outboundEncoding { - headers.encoding = outboundEncoding + for metadataPair in customMetadata { + headers.add(name: metadataPair.key, value: metadataPair.value.stringValue) + } + + return headers + } + + private mutating func serverSend(metadata: Metadata) throws -> HPACKHeaders { + // Server sends initial metadata + switch self.state { + case .clientOpenServerIdle(let state): + self.state = .clientOpenServerOpen(.init(previousState: state)) + return self.makeResponseHeaders(outboundEncoding: state.outboundCompression, customMetadata: metadata) + case .clientClosedServerIdle(let state): + self.state = .clientClosedServerOpen(.init(previousState: state)) + return self.makeResponseHeaders(outboundEncoding: state.outboundCompression, customMetadata: metadata) + case .clientIdleServerIdle: + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client cannot be idle if server is sending initial metadata: it must have opened.") + case .clientOpenServerClosed, .clientClosedServerClosed: + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server cannot send metadata if closed.") + case .clientOpenServerOpen, .clientClosedServerOpen: + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server has already sent initial metadata.") + } + } + + private mutating func serverSend(message: [UInt8], endStream: Bool) throws { + switch self.state { + case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server must have sent initial metadata before sending a message.") + case .clientOpenServerOpen(var state): + state.framer.append(message) + if endStream { + self.state = .clientOpenServerClosed(.init(previousState: state)) + } else { + self.state = .clientOpenServerOpen(state) } - - for metadataPair in customMetadata { - headers.add(name: metadataPair.key, value: metadataPair.value.stringValue) + case .clientClosedServerOpen(var state): + state.framer.append(message) + if endStream { + self.state = .clientClosedServerClosed(.init(previousState: state)) + } else { + self.state = .clientClosedServerOpen(state) } - - return headers + case .clientOpenServerClosed, .clientClosedServerClosed: + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server can't send a message if it's closed.") } + } + + private func makeTrailers( + status: Status, + customMetadata: Metadata, + trailersOnly: Bool + ) -> HPACKHeaders { + // Trailers always contain the grpc-status header, and optionally, + // grpc-status-message, and custom metadata. + // If it's a trailers-only response, they will also contain :status and + // content-type. + var headers = HPACKHeaders() - mutating func send(metadata: Metadata) throws -> HPACKHeaders { - // Server sends initial metadata - switch self.state { - case .clientOpenServerIdle(let state): - self.state = .clientOpenServerOpen(.init(previousState: state)) - return self.makeResponseHeaders(outboundEncoding: state.outboundCompression, customMetadata: metadata) - case .clientClosedServerIdle(let state): - self.state = .clientClosedServerOpen(.init(previousState: state)) - return self.makeResponseHeaders(outboundEncoding: state.outboundCompression, customMetadata: metadata) - case .clientIdleServerIdle: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client cannot be idle if server is sending initial metadata: it must have opened.") - case .clientOpenServerClosed, .clientClosedServerClosed: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server cannot send metadata if closed.") - case .clientOpenServerOpen, .clientClosedServerOpen: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server has already sent initial metadata.") - } + if trailersOnly { + // Reserve 5 for capacity: 3 for the required headers, and 1 for the + // optional status message. + headers.reserveCapacity(4 + customMetadata.count) + headers.status = "200" + headers.contentType = .protobuf + } else { + // Reserve 2 for capacity: one for the required grpc-status, and + // one for the optional message. + headers.reserveCapacity(2 + customMetadata.count) } - mutating func send(message: [UInt8], endStream: Bool) throws { - switch self.state { - case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server must have sent initial metadata before sending a message.") - case .clientOpenServerOpen(var state): - state.framer.append(message) - if endStream { - self.state = .clientOpenServerClosed(.init(previousState: state)) - } else { - self.state = .clientOpenServerOpen(state) - } - case .clientClosedServerOpen(var state): - state.framer.append(message) - if endStream { - self.state = .clientClosedServerClosed(.init(previousState: state)) - } else { - self.state = .clientClosedServerOpen(state) - } - case .clientOpenServerClosed, .clientClosedServerClosed: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server can't send a message if it's closed.") - } + headers.grpcStatus = status.code + + if !status.message.isEmpty { + // TODO: this message has to be percent-encoded + headers.grpcStatusMessage = status.message } - private func makeTrailers( - status: Status, - customMetadata: Metadata, - trailersOnly: Bool - ) -> HPACKHeaders { - // Trailers always contain the grpc-status header, and optionally, - // grpc-status-message, and custom metadata. - // If it's a trailers-only response, they will also contain :status and - // content-type. - var headers = HPACKHeaders() - - if trailersOnly { - // Reserve 5 for capacity: 3 for the required headers, and 1 for the - // optional status message. - headers.reserveCapacity(4 + customMetadata.count) - headers.status = "200" - headers.contentType = .protobuf - } else { - // Reserve 2 for capacity: one for the required grpc-status, and - // one for the optional message. - headers.reserveCapacity(2 + customMetadata.count) - } - - headers.grpcStatus = status.code - - if !status.message.isEmpty { - // TODO: this message has to be percent-encoded - headers.grpcStatusMessage = status.message - } - - for metadataPair in customMetadata { - headers.add(name: metadataPair.key, value: metadataPair.value.stringValue) - } - - return headers + for metadataPair in customMetadata { + headers.add(name: metadataPair.key, value: metadataPair.value.stringValue) } - - mutating func send(status: Status, metadata: Metadata, trailersOnly: Bool) throws -> HPACKHeaders { - // Close the server. - switch self.state { - case .clientOpenServerOpen(let state): - self.state = .clientOpenServerClosed(.init(previousState: state)) - return self.makeTrailers(status: status, customMetadata: metadata, trailersOnly: trailersOnly) - case .clientClosedServerOpen(let state): - state.compressor?.end() - state.decompressor?.end() - self.state = .clientClosedServerClosed(.init(previousState: state)) - return self.makeTrailers(status: status, customMetadata: metadata, trailersOnly: trailersOnly) - case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server can't send status if idle.") - case .clientOpenServerClosed, .clientClosedServerClosed: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server can't send anything if closed.") - } + + return headers + } + + private mutating func serverSend(status: Status, metadata: Metadata, trailersOnly: Bool) throws -> HPACKHeaders { + // Close the server. + switch self.state { + case .clientOpenServerOpen(let state): + self.state = .clientOpenServerClosed(.init(previousState: state)) + return self.makeTrailers(status: status, customMetadata: metadata, trailersOnly: trailersOnly) + case .clientClosedServerOpen(let state): + state.compressor?.end() + state.decompressor?.end() + self.state = .clientClosedServerClosed(.init(previousState: state)) + return self.makeTrailers(status: status, customMetadata: metadata, trailersOnly: trailersOnly) + case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server can't send status if idle.") + case .clientOpenServerClosed, .clientClosedServerClosed: + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server can't send anything if closed.") } - - mutating func receive(metadata: HPACKHeaders, endStream: Bool) throws -> OnMetadataReceived { - if endStream, case .clientIdleServerIdle = self.state { - throw self.assertionFailureAndCreateRPCErrorOnInternalError( + } + + private mutating func serverReceive( + metadata: HPACKHeaders, + endStream: Bool, + configuration: GRPCStreamStateMachineConfiguration.ServerConfiguration + ) throws -> OnMetadataReceived { + if endStream, case .clientIdleServerIdle = self.state { + throw self.assertionFailureAndCreateRPCErrorOnInternalError( """ Client should have opened before ending the stream: \ stream shouldn't have been closed when sending initial metadata. """ - ) + ) + } + + switch self.state { + case .clientIdleServerIdle(let state): + guard metadata.contentType != nil else { + throw RPCError(code: .invalidArgument, message: "Invalid or empty \(GRPCHTTP2Keys.contentType.rawValue).") } - switch self.state { - case .clientIdleServerIdle(let state): - guard let contentType = metadata.contentType else { - throw RPCError(code: .invalidArgument, message: "Invalid or empty \(GRPCHTTP2Keys.contentType.rawValue).") + guard metadata.path != nil else { + throw RPCError(code: .unimplemented, message: "No \(GRPCHTTP2Keys.path.rawValue) header has been set.") + } + // TODO: Should we verify the RPCRouter can handle this endpoint here, + // or should we verify that in the handler? + + var outboundEncoding: CompressionAlgorithm? = nil + if let clientAdvertisedEncodings = metadata.acceptedEncodings { + for clientAcceptedEncoding in clientAdvertisedEncodings where configuration.acceptedEncodings.contains(where: { $0 == clientAcceptedEncoding }) { + // Found the preferred encoding: use it to compress responses. + outboundEncoding = clientAcceptedEncoding + break } - - guard let endpoint = metadata.path else { - throw RPCError(code: .unimplemented, message: "No \(GRPCHTTP2Keys.path.rawValue) header has been set.") + } + + var inboundEncoding: CompressionAlgorithm? = nil + let encodingValues = metadata[GRPCHTTP2Keys.encoding.rawValue] + var encodingValuesIterator = encodingValues.makeIterator() + if let rawEncoding = encodingValuesIterator.next() { + guard encodingValuesIterator.next() == nil else { + throw RPCError( + code: .invalidArgument, + message: "\(GRPCHTTP2Keys.encoding) must contain no more than one value." + ) } - // TODO: Should we verify the RPCRouter can handle this endpoint here, - // or should we verify that in the handler? - var outboundEncoding: CompressionAlgorithm? = nil - if let clientAdvertisedEncodings = metadata.acceptedEncodings { - for clientAcceptedEncoding in clientAdvertisedEncodings where self.acceptedEncodings.contains(where: { $0 == clientAcceptedEncoding }) { - // Found the preferred encoding: use it to compress responses. - outboundEncoding = clientAcceptedEncoding - break - } - } - - var inboundEncoding: CompressionAlgorithm? = nil - let encodingValues = metadata[GRPCHTTP2Keys.encoding.rawValue] - var encodingValuesIterator = encodingValues.makeIterator() - if let rawEncoding = encodingValuesIterator.next() { - guard encodingValuesIterator.next() == nil else { + guard let clientEncoding = CompressionAlgorithm(rawValue: rawEncoding), configuration.acceptedEncodings.contains(where: { $0 == clientEncoding }) else { + if configuration.acceptedEncodings.isEmpty { throw RPCError( - code: .invalidArgument, - message: "\(GRPCHTTP2Keys.encoding) must contain no more than one value." + code: .unimplemented, + message: "Compression is not supported" ) + } else { + let status = Status( + code: .unimplemented, + message: "\(rawEncoding) compression is not supported; supported algorithms are listed in grpc-accept-encoding" + ) + let trailers = HPACKHeaders(httpHeaders: [ + "grpc-accept-encoding": + configuration.acceptedEncodings + .map({ $0.name }) + .joined(separator: ",") + ]) + return .reject(status: status, trailers: trailers) } - - guard let clientEncoding = CompressionAlgorithm(rawValue: rawEncoding), self.acceptedEncodings.contains(where: { $0 == clientEncoding }) else { - if self.acceptedEncodings.isEmpty { - throw RPCError( - code: .unimplemented, - message: "Compression is not supported" - ) - } else { - let status = Status( - code: .unimplemented, - message: "\(rawEncoding) compression is not supported; supported algorithms are listed in grpc-accept-encoding" - ) - let trailers = HPACKHeaders(httpHeaders: [ - "grpc-accept-encoding": - self.acceptedEncodings - .map({ $0.name }) - .joined(separator: ",") - ]) - return .reject(status: status, trailers: trailers) - } - } - - inboundEncoding = clientEncoding - } - - self.state = .clientOpenServerIdle(.init( - previousState: state, - compressionAlgorithm: outboundEncoding, - decompressionConfiguration: .decompression(inboundEncoding) - )) - - return .doNothing - case .clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client shouldn't have sent metadata twice.") - case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client can't have sent metadata if closed.") - } - } - - mutating func receive(message: ByteBuffer, endStream: Bool) throws { - switch self.state { - case .clientIdleServerIdle: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Can't have received a message if client is idle.") - case .clientOpenServerIdle(var state): - // Deframer must be present on the server side, as we know the decompression - // algorithm from the moment the client opens. - assert(state.deframer != nil) - try state.deframer!.process(buffer: message) { deframedMessage in - state.inboundMessageBuffer.append(deframedMessage) - } - - if endStream { - self.state = .clientClosedServerIdle(.init(previousState: state)) - } else { - self.state = .clientOpenServerIdle(state) - } - case .clientOpenServerOpen(var state): - try state.deframer.process(buffer: message) { deframedMessage in - state.inboundMessageBuffer.append(deframedMessage) } - if endStream { - self.state = .clientClosedServerOpen(.init(previousState: state)) - } else { - self.state = .clientOpenServerOpen(state) - } - case .clientOpenServerClosed: - // Client is not done sending request, but server has already closed. - // Ignore the rest of the request: do nothing. - () - case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client can't send a message if closed.") + inboundEncoding = clientEncoding } + + self.state = .clientOpenServerIdle(.init( + previousState: state, + compressionAlgorithm: outboundEncoding, + decompressionConfiguration: .decompression(inboundEncoding) + )) + + return .doNothing + case .clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed: + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client shouldn't have sent metadata twice.") + case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client can't have sent metadata if closed.") } - - mutating func nextOutboundMessage() throws -> ByteBuffer? { - switch self.state { - case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server is not open yet.") - case .clientOpenServerOpen(var state): - let response = try state.framer.next(compressor: state.compressor) - self.state = .clientOpenServerOpen(state) - return response - case .clientClosedServerOpen(var state): - let response = try state.framer.next(compressor: state.compressor) - self.state = .clientClosedServerOpen(state) - return response - case .clientOpenServerClosed, .clientClosedServerClosed: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Can't send response if server is closed.") + } + + private mutating func serverReceive(message: ByteBuffer, endStream: Bool) throws { + switch self.state { + case .clientIdleServerIdle: + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Can't have received a message if client is idle.") + case .clientOpenServerIdle(var state): + // Deframer must be present on the server side, as we know the decompression + // algorithm from the moment the client opens. + assert(state.deframer != nil) + try state.deframer!.process(buffer: message) { deframedMessage in + state.inboundMessageBuffer.append(deframedMessage) } - } - - mutating func nextInboundMessage() -> [UInt8]? { - switch self.state { - case .clientOpenServerIdle(var state): - let request = state.inboundMessageBuffer.pop() + + if endStream { + self.state = .clientClosedServerIdle(.init(previousState: state)) + } else { self.state = .clientOpenServerIdle(state) - return request - case .clientOpenServerOpen(var state): - let request = state.inboundMessageBuffer.pop() + } + case .clientOpenServerOpen(var state): + try state.deframer.process(buffer: message) { deframedMessage in + state.inboundMessageBuffer.append(deframedMessage) + } + + if endStream { + self.state = .clientClosedServerOpen(.init(previousState: state)) + } else { self.state = .clientOpenServerOpen(state) - return request - case .clientOpenServerClosed(var state): - let request = state.inboundMessageBuffer.pop() - self.state = .clientOpenServerClosed(state) - return request - case .clientClosedServerOpen(var state): - let request = state.inboundMessageBuffer.pop() - self.state = .clientClosedServerOpen(state) - return request - case .clientClosedServerClosed(var state): - let request = state.inboundMessageBuffer.pop() - self.state = .clientClosedServerClosed(state) - return request - case .clientClosedServerIdle, .clientIdleServerIdle: - return nil } + case .clientOpenServerClosed: + // Client is not done sending request, but server has already closed. + // Ignore the rest of the request: do nothing. + () + case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client can't send a message if closed.") } - - private func assertionFailureAndCreateRPCErrorOnInternalError(_ message: String, line: UInt = #line) -> RPCError { - assert(self.skipAssertions, message, line: line) - return RPCError(code: .internalError, message: message) + } + + private mutating func serverNextOutboundMessage() throws -> ByteBuffer? { + switch self.state { + case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server is not open yet.") + case .clientOpenServerOpen(var state): + let response = try state.framer.next(compressor: state.compressor) + self.state = .clientOpenServerOpen(state) + return response + case .clientClosedServerOpen(var state): + let response = try state.framer.next(compressor: state.compressor) + self.state = .clientClosedServerOpen(state) + return response + case .clientOpenServerClosed, .clientClosedServerClosed: + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Can't send response if server is closed.") + } + } + + private mutating func serverNextInboundMessage() -> [UInt8]? { + switch self.state { + case .clientOpenServerIdle(var state): + let request = state.inboundMessageBuffer.pop() + self.state = .clientOpenServerIdle(state) + return request + case .clientOpenServerOpen(var state): + let request = state.inboundMessageBuffer.pop() + self.state = .clientOpenServerOpen(state) + return request + case .clientOpenServerClosed(var state): + let request = state.inboundMessageBuffer.pop() + self.state = .clientOpenServerClosed(state) + return request + case .clientClosedServerOpen(var state): + let request = state.inboundMessageBuffer.pop() + self.state = .clientClosedServerOpen(state) + return request + case .clientClosedServerClosed(var state): + let request = state.inboundMessageBuffer.pop() + self.state = .clientClosedServerClosed(state) + return request + case .clientClosedServerIdle, .clientIdleServerIdle: + return nil } } } @@ -1114,7 +1083,7 @@ extension HPACKHeaders { private mutating func replaceOrAddString(_ value: String, forKey key: GRPCHTTP2Keys) { self.replaceOrAdd(name: value, value: key.rawValue) } - + private mutating func removeAllValues(forKey key: GRPCHTTP2Keys) { self.remove(name: key.rawValue) } diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index b2e58ee01..a059e9097 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -26,26 +26,26 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { private func makeClientStateMachine() -> GRPCStreamStateMachine { GRPCStreamStateMachine( - configuration: .client( + configuration: .client( .init( methodDescriptor: .init(service: "test", method: "test"), scheme: .http, - maximumPayloadSize: 100, outboundEncoding: nil, acceptedEncodings: [.deflate] - ), + )), + maximumPayloadSize: 100, skipAssertions: true ) } private func makeClientStateMachineWithCompression() -> GRPCStreamStateMachine { GRPCStreamStateMachine( - configuration: .client( + configuration: .client(.init( methodDescriptor: .init(service: "test", method: "test"), scheme: .http, - maximumPayloadSize: 100, outboundEncoding: .deflate, acceptedEncodings: [.deflate] - ), + )), + maximumPayloadSize: 100, skipAssertions: true ) } @@ -1118,22 +1118,22 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { private func makeServerStateMachine() -> GRPCStreamStateMachine { GRPCStreamStateMachine( - configuration: .server( + configuration: .server(.init( scheme: .http, - maximumPayloadSize: 100, acceptedEncodings: [] - ), + )), + maximumPayloadSize: 100, skipAssertions: true ) } private func makeServerStateMachineWithCompression() -> GRPCStreamStateMachine { GRPCStreamStateMachine( - configuration: .server( + configuration: .server(.init( scheme: .http, - maximumPayloadSize: 100, acceptedEncodings: [.deflate] - ), + )), + maximumPayloadSize: 100, skipAssertions: true ) } From 1938ae238e85229d0ba24a031cd568111b64bc7d Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Fri, 16 Feb 2024 15:10:33 +0000 Subject: [PATCH 14/51] More PR changes --- .../Compression/CompressionAlgorithm.swift | 2 +- .../GRPCStreamStateMachine.swift | 198 ++++---- .../GRPCStreamStateMachineTests.swift | 421 ++++++++++-------- 3 files changed, 347 insertions(+), 274 deletions(-) diff --git a/Sources/GRPCHTTP2Core/Compression/CompressionAlgorithm.swift b/Sources/GRPCHTTP2Core/Compression/CompressionAlgorithm.swift index e080ba2e1..82de83eed 100644 --- a/Sources/GRPCHTTP2Core/Compression/CompressionAlgorithm.swift +++ b/Sources/GRPCHTTP2Core/Compression/CompressionAlgorithm.swift @@ -18,7 +18,7 @@ /// /// These algorithms are indicated in the "grpc-encoding" header. As such, a lack of "grpc-encoding" /// header indicates that there is no message compression. -struct CompressionAlgorithm: Equatable, Sendable { +public struct CompressionAlgorithm: Hashable, Sendable { /// Identity compression; "no" compression but indicated via the "grpc-encoding" header. public static let identity = CompressionAlgorithm(.identity) public static let deflate = CompressionAlgorithm(.deflate) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 39067ec0d..8c5e190f5 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -20,8 +20,9 @@ import NIOHPACK @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) enum OnMetadataReceived { - case reject(status: Status, trailers: HPACKHeaders) - case doNothing + case receivedMetadata(Metadata) + // Server-specific actions + case rejectRPC(trailers: HPACKHeaders) } enum Scheme: String { @@ -324,9 +325,9 @@ struct GRPCStreamStateMachine { mutating func receive(message: ByteBuffer, endStream: Bool) throws { switch self.configuration { case .client: - try clientReceive(message: message, endStream: endStream) + try clientReceive(bytes: message, endStream: endStream) case .server: - try serverReceive(message: message, endStream: endStream) + try serverReceive(bytes: message, endStream: endStream) } } @@ -476,8 +477,6 @@ extension GRPCStreamStateMachine { private mutating func clientReceive(metadata: HPACKHeaders, endStream: Bool) throws -> OnMetadataReceived { switch self.state { - case .clientIdleServerIdle: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server cannot have sent metadata if the client is idle.") case .clientOpenServerIdle(let state): if endStream { // This is a trailers-only response: close server. @@ -488,6 +487,7 @@ extension GRPCStreamStateMachine { decompressionAlgorithm: metadata.encoding )) } + return .receivedMetadata(Metadata(headers: metadata)) case .clientOpenServerOpen(let state): if endStream { self.state = .clientOpenServerClosed(.init(previousState: state)) @@ -500,6 +500,7 @@ extension GRPCStreamStateMachine { // we could get an infinite number of metadata frames from the server - // not sure this should be allowed. } + return .receivedMetadata(Metadata(headers: metadata)) case .clientClosedServerIdle(let state): if endStream { // This is a trailers-only response. @@ -510,6 +511,7 @@ extension GRPCStreamStateMachine { decompressionAlgorithm: metadata.encoding )) } + return .receivedMetadata(Metadata(headers: metadata)) case .clientClosedServerOpen(let state): if endStream { state.compressor?.end() @@ -524,14 +526,26 @@ extension GRPCStreamStateMachine { // we could get an infinite number of metadata frames from the server - // not sure this should be allowed. } - case .clientOpenServerClosed, .clientClosedServerClosed: + return .receivedMetadata(Metadata(headers: metadata)) + case .clientClosedServerClosed: + // We could end up here if we received a grpc-status header in a previous + // frame (which would have already close the server) and then we receive + // an empty frame with EOS set. + // We wouldn't want to throw in that scenario, so we just ignore it. + // Note that we don't want to ignore it if EOS is not set here though, as + // then it would be an invalid payload. + if !endStream || metadata.count > 0 { + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server is closed, nothing could have been sent.") + } + return .receivedMetadata([]) + case .clientIdleServerIdle: + throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server cannot have sent metadata if the client is idle.") + case .clientOpenServerClosed: throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server is closed, nothing could have been sent.") } - - return .doNothing } - private mutating func clientReceive(message: ByteBuffer, endStream: Bool) throws { + private mutating func clientReceive(bytes: ByteBuffer, endStream: Bool) throws { // This is a message received by the client, from the server. switch self.state { case .clientIdleServerIdle: @@ -539,7 +553,7 @@ extension GRPCStreamStateMachine { case .clientOpenServerIdle, .clientClosedServerIdle: throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server cannot have sent a message before sending the initial metadata.") case .clientOpenServerOpen(var state): - try state.deframer.process(buffer: message) { deframedMessage in + try state.deframer.process(buffer: bytes) { deframedMessage in state.inboundMessageBuffer.append(deframedMessage) } if endStream { @@ -550,7 +564,7 @@ extension GRPCStreamStateMachine { case .clientClosedServerOpen(var state): // The client may have sent the end stream and thus it's closed, // but the server may still be responding. - try state.deframer.process(buffer: message) { deframedMessage in + try state.deframer.process(buffer: bytes) { deframedMessage in state.inboundMessageBuffer.append(deframedMessage) } if endStream { @@ -733,57 +747,79 @@ extension GRPCStreamStateMachine { switch self.state { case .clientIdleServerIdle(let state): guard metadata.contentType != nil else { - throw RPCError(code: .invalidArgument, message: "Invalid or empty \(GRPCHTTP2Keys.contentType.rawValue).") + // Respond with HTTP-level Unsupported Media Type status code. + var trailers = HPACKHeaders() + trailers.status = "415" + return .rejectRPC(trailers: trailers) } guard metadata.path != nil else { - throw RPCError(code: .unimplemented, message: "No \(GRPCHTTP2Keys.path.rawValue) header has been set.") + var trailers = HPACKHeaders() + trailers.reserveCapacity(2) + trailers.grpcStatus = .unimplemented + trailers.grpcStatusMessage = "No \(GRPCHTTP2Keys.path.rawValue) header has been set." + return .rejectRPC(trailers: trailers) } - // TODO: Should we verify the RPCRouter can handle this endpoint here, - // or should we verify that in the handler? - - var outboundEncoding: CompressionAlgorithm? = nil - if let clientAdvertisedEncodings = metadata.acceptedEncodings { - for clientAcceptedEncoding in clientAdvertisedEncodings where configuration.acceptedEncodings.contains(where: { $0 == clientAcceptedEncoding }) { - // Found the preferred encoding: use it to compress responses. - outboundEncoding = clientAcceptedEncoding - break - } + + func isIdentityOrCompatibleEncoding(_ clientEncoding: CompressionAlgorithm) -> Bool { + clientEncoding == .identity || + configuration.acceptedEncodings.contains(where: { $0 == clientEncoding }) } + // Firstly, find out if we support the client's chosen encoding, and reject + // the RPC if we don't. var inboundEncoding: CompressionAlgorithm? = nil - let encodingValues = metadata[GRPCHTTP2Keys.encoding.rawValue] + let encodingValues = metadata.values(forHeader: GRPCHTTP2Keys.encoding.rawValue, canonicalForm: true) var encodingValuesIterator = encodingValues.makeIterator() if let rawEncoding = encodingValuesIterator.next() { guard encodingValuesIterator.next() == nil else { - throw RPCError( - code: .invalidArgument, - message: "\(GRPCHTTP2Keys.encoding) must contain no more than one value." - ) + var trailers = HPACKHeaders() + trailers.reserveCapacity(2) + trailers.grpcStatus = .internalError + trailers.grpcStatusMessage = "\(GRPCHTTP2Keys.encoding) must contain no more than one value." + return .rejectRPC(trailers: trailers) } - guard let clientEncoding = CompressionAlgorithm(rawValue: rawEncoding), configuration.acceptedEncodings.contains(where: { $0 == clientEncoding }) else { + guard let clientEncoding = CompressionAlgorithm(rawValue: String(rawEncoding)), isIdentityOrCompatibleEncoding(clientEncoding) else { if configuration.acceptedEncodings.isEmpty { - throw RPCError( - code: .unimplemented, - message: "Compression is not supported" - ) + var trailers = HPACKHeaders() + trailers.reserveCapacity(2) + trailers.grpcStatus = .unimplemented + trailers.grpcStatusMessage = "Compression is not supported" + return .rejectRPC(trailers: trailers) } else { - let status = Status( - code: .unimplemented, - message: "\(rawEncoding) compression is not supported; supported algorithms are listed in grpc-accept-encoding" - ) - let trailers = HPACKHeaders(httpHeaders: [ - "grpc-accept-encoding": - configuration.acceptedEncodings - .map({ $0.name }) - .joined(separator: ",") - ]) - return .reject(status: status, trailers: trailers) + var trailers = HPACKHeaders() + trailers.reserveCapacity(3) + trailers.grpcStatus = .unimplemented + trailers.grpcStatusMessage = """ + \(rawEncoding) compression is not supported; \ + supported algorithms are listed in grpc-accept-encoding + """ + trailers.acceptedEncodings = configuration.acceptedEncodings + return .rejectRPC(trailers: trailers) } } - inboundEncoding = clientEncoding + // Server supports client's encoding. + // If it's identity, just skip it altogether. + if clientEncoding != .identity { + inboundEncoding = clientEncoding + } + } + + // Secondly, find a compatible encoding the server can use to compress outbound messages, + // based on the encodings the client has advertised. + var outboundEncoding: CompressionAlgorithm? = nil + if let clientAdvertisedEncodings = metadata.acceptedEncodings { + for clientAcceptedEncoding in clientAdvertisedEncodings where isIdentityOrCompatibleEncoding(clientAcceptedEncoding) { + // Found the preferred encoding: use it to compress responses. + // If it's identity, just skip it altogether, since we won't be + // compressing. + if clientAcceptedEncoding != .identity { + outboundEncoding = clientAcceptedEncoding + } + break + } } self.state = .clientOpenServerIdle(.init( @@ -792,7 +828,7 @@ extension GRPCStreamStateMachine { decompressionConfiguration: .decompression(inboundEncoding) )) - return .doNothing + return .receivedMetadata(Metadata(headers: metadata)) case .clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed: throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client shouldn't have sent metadata twice.") case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: @@ -800,7 +836,7 @@ extension GRPCStreamStateMachine { } } - private mutating func serverReceive(message: ByteBuffer, endStream: Bool) throws { + private mutating func serverReceive(bytes: ByteBuffer, endStream: Bool) throws { switch self.state { case .clientIdleServerIdle: throw self.assertionFailureAndCreateRPCErrorOnInternalError("Can't have received a message if client is idle.") @@ -808,7 +844,7 @@ extension GRPCStreamStateMachine { // Deframer must be present on the server side, as we know the decompression // algorithm from the moment the client opens. assert(state.deframer != nil) - try state.deframer!.process(buffer: message) { deframedMessage in + try state.deframer!.process(buffer: bytes) { deframedMessage in state.inboundMessageBuffer.append(deframedMessage) } @@ -818,7 +854,7 @@ extension GRPCStreamStateMachine { self.state = .clientOpenServerIdle(state) } case .clientOpenServerOpen(var state): - try state.deframer.process(buffer: message) { deframedMessage in + try state.deframer.process(buffer: bytes) { deframedMessage in state.inboundMessageBuffer.append(deframedMessage) } @@ -891,7 +927,7 @@ extension MethodDescriptor { } } -private enum GRPCHTTP2Keys: String { +internal enum GRPCHTTP2Keys: String, CaseIterable { case path = ":path" case contentType = "content-type" case encoding = "grpc-encoding" @@ -904,41 +940,6 @@ private enum GRPCHTTP2Keys: String { case grpcStatusMessage = "grpc-status-message" } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension Metadata { - var path: MethodDescriptor? { - get { - self.firstString(forKey: .path) - .flatMap { MethodDescriptor(fullyQualifiedMethod: $0) } - } - } - - var contentType: ContentType? { - get { - self.firstString(forKey: .contentType) - .flatMap { ContentType(value: $0) } - } - } - - var encoding: CompressionAlgorithm? { - get { - self.firstString(forKey: .encoding).flatMap { CompressionAlgorithm(rawValue: $0) } - } - } - - var acceptedEncodings: [CompressionAlgorithm]? { - get { - self.firstString(forKey: .acceptEncoding)? - .split(separator: ",") - .compactMap { CompressionAlgorithm(rawValue: String($0)) } - } - } - - private func firstString(forKey key: GRPCHTTP2Keys) -> String? { - self[stringValues: key.rawValue].first(where: { _ in true }) - } -} - extension HPACKHeaders { var path: MethodDescriptor? { get { @@ -1077,11 +1078,11 @@ extension HPACKHeaders { } private func firstString(forKey key: GRPCHTTP2Keys) -> String? { - self[key.rawValue].first(where: { _ in true }) + self.values(forHeader: key.rawValue, canonicalForm: true).first(where: { _ in true }).map { String($0) } } private mutating func replaceOrAddString(_ value: String, forKey key: GRPCHTTP2Keys) { - self.replaceOrAdd(name: value, value: key.rawValue) + self.replaceOrAdd(name: key.rawValue, value: value) } private mutating func removeAllValues(forKey key: GRPCHTTP2Keys) { @@ -1107,3 +1108,24 @@ extension Zlib.Method { } } } + +extension Metadata { + init(headers: HPACKHeaders) { + var metadata = Metadata() + // TODO: since this is what we'll pass on to the user, I was wondering if it would be useful + // to filter out the headers that relate to the protocol, and just leave the user-defined ones. + for header in headers where !GRPCHTTP2Keys.allCases.contains(where: { $0.rawValue == header.name}) { + if header.name.hasSuffix("-bin") { + do { + let decodedBinary = try header.value.base64Decoded() + metadata.addBinary(decodedBinary, forKey: header.name) + } catch { + metadata.addString(header.value, forKey: header.name) + } + } else { + metadata.addString(header.value, forKey: header.name) + } + } + self = metadata + } +} diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index a059e9097..1c02a0bd1 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -22,8 +22,6 @@ import NIOHPACK @testable import GRPCHTTP2Core final class GRPCStreamClientStateMachineTests: XCTestCase { - private let testMetadata: Metadata = [] - private func makeClientStateMachine() -> GRPCStreamStateMachine { GRPCStreamStateMachine( configuration: .client( .init( @@ -54,14 +52,14 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { func testSendMetadataWhenClientIdleAndServerIdle() throws { var stateMachine = makeClientStateMachine() - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) } func testSendMetadataWhenClientOpenAndServerIdle() throws { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Try sending metadata again: should throw XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in @@ -74,7 +72,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) @@ -90,7 +88,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) @@ -109,7 +107,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) @@ -125,7 +123,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) @@ -144,7 +142,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) @@ -179,7 +177,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Now send a message XCTAssertNoThrow(try stateMachine.send(message: [], endStream: false)) @@ -189,7 +187,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) @@ -202,7 +200,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) @@ -218,7 +216,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) @@ -235,7 +233,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) @@ -255,7 +253,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) @@ -291,7 +289,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // This operation is never allowed on the client. XCTAssertThrowsError(ofType: RPCError.self, @@ -305,7 +303,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) @@ -322,7 +320,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachine() // Open stream - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) @@ -342,7 +340,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) @@ -359,7 +357,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) @@ -379,7 +377,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) @@ -414,38 +412,55 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Receive metadata = open server - let action = try stateMachine.receive(metadata: .init(), endStream: false) - guard case .doNothing = action else { - XCTFail("Expected action to be doNothing") + let action = try stateMachine.receive(metadata: [GRPCHTTP2Keys.encoding.rawValue: "deflate", "custom": "123", "custom-bin": String(base64Encoding: [42,43,44])], endStream: false) + guard case .receivedMetadata(let customMetadata) = action else { + XCTFail("Expected action to be receivedMetadata") return } + + XCTAssertEqual(customMetadata.count, 2) + let customHeaderStringValues = try XCTUnwrap(customMetadata[stringValues: "custom"]) + var headerStringValuesIterator = customHeaderStringValues.makeIterator() + XCTAssertEqual(headerStringValuesIterator.next(), "123") + XCTAssertNil(headerStringValuesIterator.next()) + + let customHeaderBinaryValues = try XCTUnwrap(customMetadata[binaryValues: "custom-bin"]) + var headerBinaryValuesIterator = customHeaderBinaryValues.makeIterator() + XCTAssertEqual(headerBinaryValuesIterator.next()!, [42,43,44]) + XCTAssertNil(headerBinaryValuesIterator.next()) } func testReceiveInitialMetadataWhenClientOpenAndServerOpen() throws { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) // Try opening server again - let action = try stateMachine.receive(metadata: .init(), endStream: false) - guard case .doNothing = action else { - XCTFail("Expected action to be doNothing") + let action = try stateMachine.receive(metadata: [GRPCHTTP2Keys.encoding.rawValue: "deflate", "custom": "123"], endStream: false) + guard case .receivedMetadata(let customMetadata) = action else { + XCTFail("Expected action to be receivedMetadata") return } + + XCTAssertEqual(customMetadata.count, 1) + let customHeaderStringValues = try XCTUnwrap(customMetadata[stringValues: "custom"]) + var headerStringValuesIterator = customHeaderStringValues.makeIterator() + XCTAssertEqual(headerStringValuesIterator.next(), "123") + XCTAssertNil(headerStringValuesIterator.next()) } func testReceiveInitialMetadataWhenClientOpenAndServerClosed() { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) @@ -464,24 +479,30 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) // Receive metadata = open server - let action = try stateMachine.receive(metadata: .init(), endStream: false) - guard case .doNothing = action else { - XCTFail("Expected action to be doNothing") + let action = try stateMachine.receive(metadata: [GRPCHTTP2Keys.encoding.rawValue: "deflate", "custom": "123"], endStream: false) + guard case .receivedMetadata(let customMetadata) = action else { + XCTFail("Expected action to be receivedMetadata") return } + + XCTAssertEqual(customMetadata.count, 1) + let customHeaderStringValues = try XCTUnwrap(customMetadata[stringValues: "custom"]) + var headerStringValuesIterator = customHeaderStringValues.makeIterator() + XCTAssertEqual(headerStringValuesIterator.next(), "123") + XCTAssertNil(headerStringValuesIterator.next()) } func testReceiveInitialMetadataWhenClientClosedAndServerOpen() throws { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) @@ -490,18 +511,24 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) // Receive metadata = open server - let action = try stateMachine.receive(metadata: .init(), endStream: false) - guard case .doNothing = action else { - XCTFail("Expected action to be doNothing") + let action = try stateMachine.receive(metadata: [GRPCHTTP2Keys.encoding.rawValue: "deflate", "custom": "123"], endStream: false) + guard case .receivedMetadata(let customMetadata) = action else { + XCTFail("Expected action to be receivedMetadata") return } + + XCTAssertEqual(customMetadata.count, 1) + let customHeaderStringValues = try XCTUnwrap(customMetadata[stringValues: "custom"]) + var headerStringValuesIterator = customHeaderStringValues.makeIterator() + XCTAssertEqual(headerStringValuesIterator.next(), "123") + XCTAssertNil(headerStringValuesIterator.next()) } func testReceiveInitialMetadataWhenClientClosedAndServerClosed() { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) @@ -512,6 +539,8 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Close server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) + // This time receive metadata but with endStream = false. We should throw + // here, since this would be invalid. XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.receive(metadata: .init(), endStream: false)) { error in XCTAssertEqual(error.code, .internalError) @@ -536,7 +565,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Receive a trailer-only response XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) @@ -546,24 +575,30 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) // Receive an end trailer - let action = try stateMachine.receive(metadata: .init(), endStream: true) - guard case .doNothing = action else { - XCTFail("Expected action to be doNothing") + let action = try stateMachine.receive(metadata: [GRPCHTTP2Keys.encoding.rawValue: "deflate", "custom": "123"], endStream: true) + guard case .receivedMetadata(let customMetadata) = action else { + XCTFail("Expected action to be receivedMetadata") return } + + XCTAssertEqual(customMetadata.count, 1) + let customHeaderStringValues = try XCTUnwrap(customMetadata[stringValues: "custom"]) + var headerStringValuesIterator = customHeaderStringValues.makeIterator() + XCTAssertEqual(headerStringValuesIterator.next(), "123") + XCTAssertNil(headerStringValuesIterator.next()) } func testReceiveEndTrailerWhenClientOpenAndServerClosed() { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) @@ -583,7 +618,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) @@ -596,7 +631,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) @@ -605,18 +640,24 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) // Closing the server now should not throw - let action = try stateMachine.receive(metadata: .init(), endStream: true) - guard case .doNothing = action else { - XCTFail("Expected action to be doNothing") + let action = try stateMachine.receive(metadata: [GRPCHTTP2Keys.encoding.rawValue: "deflate", "custom": "123"], endStream: true) + guard case .receivedMetadata(let customMetadata) = action else { + XCTFail("Expected action to be receivedMetadata") return } + + XCTAssertEqual(customMetadata.count, 1) + let customHeaderStringValues = try XCTUnwrap(customMetadata[stringValues: "custom"]) + var headerStringValuesIterator = customHeaderStringValues.makeIterator() + XCTAssertEqual(headerStringValuesIterator.next(), "123") + XCTAssertNil(headerStringValuesIterator.next()) } func testReceiveEndTrailerWhenClientClosedAndServerClosed() { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) @@ -627,12 +668,10 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Close server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) - // Closing the server again should throw - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: .init(), endStream: true)) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Server is closed, nothing could have been sent.") - } + // Close server again (endStream = true) and assert we don't throw. + // This can happen if the previous close was caused by a grpc-status header + // and then the server sends an empty frame with EOS set. + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) } // - MARK: Receive message @@ -651,7 +690,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.receive(message: .init(), endStream: false)) { error in @@ -664,7 +703,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) @@ -677,7 +716,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) @@ -696,7 +735,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) @@ -712,7 +751,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) @@ -728,7 +767,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) @@ -762,34 +801,32 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) - var request = try stateMachine.nextOutboundMessage() - XCTAssertNil(request) + XCTAssertNil(try stateMachine.nextOutboundMessage()) XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) - request = try XCTUnwrap(stateMachine.nextOutboundMessage()) + let request = try XCTUnwrap(stateMachine.nextOutboundMessage()) let expectedBytes: [UInt8] = [ 0, // compression flag: unset 0, 0, 0, 2, // message length: 2 bytes 42, 42 // original message ] - XCTAssertEqual(Array(buffer: request!), expectedBytes) + XCTAssertEqual(Array(buffer: request), expectedBytes) } func testNextOutboundMessageWhenClientOpenAndServerIdle_WithCompression() throws { var stateMachine = makeClientStateMachineWithCompression() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) - var request = try stateMachine.nextOutboundMessage() - XCTAssertNil(request) + XCTAssertNil(try stateMachine.nextOutboundMessage()) let originalMessage = [UInt8]([42, 42, 43, 43]) XCTAssertNoThrow(try stateMachine.send(message: originalMessage, endStream: false)) - request = try XCTUnwrap(stateMachine.nextOutboundMessage()) + let request = try XCTUnwrap(stateMachine.nextOutboundMessage()) var framer = GRPCMessageFramer() let compressor = Zlib.Compressor(method: .deflate) @@ -798,47 +835,45 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { let framedMessage = try XCTUnwrap(framer.next(compressor: compressor)) let expectedBytes = Array(buffer: framedMessage) - XCTAssertEqual(Array(buffer: request!), expectedBytes) + XCTAssertEqual(Array(buffer: request), expectedBytes) } func testNextOutboundMessageWhenClientOpenAndServerOpen() throws { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - var request = try stateMachine.nextOutboundMessage() - XCTAssertNil(request) + XCTAssertNil(try stateMachine.nextOutboundMessage()) XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) - request = try XCTUnwrap(stateMachine.nextOutboundMessage()) + let request = try XCTUnwrap(stateMachine.nextOutboundMessage()) let expectedBytes: [UInt8] = [ 0, // compression flag: unset 0, 0, 0, 2, // message length: 2 bytes 42, 42 // original message ] - XCTAssertEqual(Array(buffer: request!), expectedBytes) + XCTAssertEqual(Array(buffer: request), expectedBytes) } func testNextOutboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { var stateMachine = makeClientStateMachineWithCompression() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - var request = try stateMachine.nextOutboundMessage() - XCTAssertNil(request) + XCTAssertNil(try stateMachine.nextOutboundMessage()) let originalMessage = [UInt8]([42, 42, 43, 43]) XCTAssertNoThrow(try stateMachine.send(message: originalMessage, endStream: false)) - request = try XCTUnwrap(stateMachine.nextOutboundMessage()) + let request = try XCTUnwrap(stateMachine.nextOutboundMessage()) var framer = GRPCMessageFramer() let compressor = Zlib.Compressor(method: .deflate) @@ -847,14 +882,14 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { let framedMessage = try XCTUnwrap(framer.next(compressor: compressor)) let expectedBytes = Array(buffer: framedMessage) - XCTAssertEqual(Array(buffer: request!), expectedBytes) + XCTAssertEqual(Array(buffer: request), expectedBytes) } func testNextOutboundMessageWhenClientOpenAndServerClosed() throws { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) @@ -875,7 +910,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Send a message and close client XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: true)) @@ -898,7 +933,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) @@ -924,7 +959,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) @@ -954,7 +989,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) XCTAssertNil(stateMachine.nextInboundMessage()) } @@ -963,7 +998,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) @@ -985,10 +1020,10 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachineWithCompression() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeadersWithDeflateCompression, endStream: false)) let originalMessage = [UInt8]([42, 42, 43, 43]) var framer = GRPCMessageFramer() @@ -1009,7 +1044,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) @@ -1034,7 +1069,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) @@ -1048,7 +1083,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) @@ -1075,7 +1110,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = makeClientStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: self.testMetadata)) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) @@ -1102,20 +1137,23 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { } } -final class GRPCStreamServerStateMachineTests: XCTestCase { - private let receivedHeaders: HPACKHeaders = [ - ":path": "test/test", - "content-type": "application/grpc" +extension HPACKHeaders { + static let receivedHeaders: Self = [ + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc" ] - private let receivedHeadersWithDeflateCompression: HPACKHeaders = [ - ":path": "test/test", - "content-type": "application/grpc", - "grpc-encoding": "deflate" + static let receivedHeadersWithDeflateCompression: Self = [ + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.encoding.rawValue: "deflate", + GRPCHTTP2Keys.acceptEncoding.rawValue: "deflate" ] - private let receivedHeadersWithoutContentType: HPACKHeaders = [":path": "test/test"] - private let receivedHeadersWithInvalidContentType: HPACKHeaders = ["content-type": "invalid/invalid"] - private let receivedHeadersWithoutEndpoint: HPACKHeaders = ["content-type": "application/grpc"] - + static let receivedHeadersWithoutContentType: Self = [GRPCHTTP2Keys.path.rawValue: "test/test"] + static let receivedHeadersWithInvalidContentType: Self = [GRPCHTTP2Keys.contentType.rawValue: "invalid/invalid"] + static let receivedHeadersWithoutEndpoint: Self = [GRPCHTTP2Keys.contentType.rawValue: "application/grpc"] +} + +final class GRPCStreamServerStateMachineTests: XCTestCase { private func makeServerStateMachine() -> GRPCStreamStateMachine { GRPCStreamStateMachine( configuration: .server(.init( @@ -1154,7 +1192,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) XCTAssertNoThrow(try stateMachine.send(metadata: .init())) } @@ -1163,7 +1201,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1180,7 +1218,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1199,7 +1237,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) // Close client XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) @@ -1213,7 +1251,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1232,7 +1270,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1266,7 +1304,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) // Now send a message XCTAssertThrowsError(ofType: RPCError.self, @@ -1280,7 +1318,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1293,7 +1331,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1313,7 +1351,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) // Close client XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) @@ -1329,7 +1367,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1346,7 +1384,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1381,7 +1419,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(status: .init(code: .ok, message: ""), metadata: .init(), trailersOnly: false)) { error in @@ -1394,7 +1432,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1413,7 +1451,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1432,7 +1470,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) // Close client XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) @@ -1448,7 +1486,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1464,7 +1502,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1487,11 +1525,13 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { func testReceiveMetadataWhenClientIdleAndServerIdle() throws { var stateMachine = makeServerStateMachine() - let action = try stateMachine.receive(metadata: self.receivedHeaders, endStream: false) - guard case .doNothing = action else { + let action = try stateMachine.receive(metadata: .receivedHeaders, endStream: false) + guard case .receivedMetadata(let metadata) = action else { XCTFail("Expected action to be doNothing") return } + + XCTAssertTrue(metadata.isEmpty) } func testReceiveMetadataWhenClientIdleAndServerIdle_WithEndStream() { @@ -1501,7 +1541,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // sending a message with endStream set. If they send metadata it has to be // to open the stream (initial metadata). XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: self.receivedHeaders, endStream: true)) { error in + try stateMachine.receive(metadata: .receivedHeaders, endStream: true)) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual( error.message, @@ -1513,34 +1553,47 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } } - func testReceiveMetadataWhenClientIdleAndServerIdle_MissingContentType() { + func testReceiveMetadataWhenClientIdleAndServerIdle_MissingContentType() throws { var stateMachine = makeServerStateMachine() - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: self.receivedHeadersWithoutContentType, endStream: false)) { error in - XCTAssertEqual(error.code, .invalidArgument) - XCTAssertEqual(error.message, "Invalid or empty content-type.") + let action = try stateMachine.receive(metadata: .receivedHeadersWithoutContentType, endStream: false) + + guard case .rejectRPC(let trailers) = action else { + XCTFail("RPC should have been rejected.") + return } + + XCTAssertEqual(trailers.count, 1) + XCTAssertEqual(trailers.status, "415") } - func testReceiveMetadataWhenClientIdleAndServerIdle_InvalidContentType() { + func testReceiveMetadataWhenClientIdleAndServerIdle_InvalidContentType() throws { var stateMachine = makeServerStateMachine() - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: self.receivedHeadersWithInvalidContentType, endStream: false)) { error in - XCTAssertEqual(error.code, .invalidArgument) - XCTAssertEqual(error.message, "Invalid or empty content-type.") + let action = try stateMachine.receive(metadata: .receivedHeadersWithInvalidContentType, endStream: false) + + guard case .rejectRPC(let trailers) = action else { + XCTFail("RPC should have been rejected.") + return } + + XCTAssertEqual(trailers.count, 1) + XCTAssertEqual(trailers.status, "415") } - func testReceiveMetadataWhenClientIdleAndServerIdle_MissingPath() { + func testReceiveMetadataWhenClientIdleAndServerIdle_MissingPath() throws { var stateMachine = makeServerStateMachine() - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: self.receivedHeadersWithoutEndpoint, endStream: false)) { error in - XCTAssertEqual(error.code, .unimplemented) - XCTAssertEqual(error.message, "No :path header has been set.") + let action = try stateMachine.receive(metadata: .receivedHeadersWithoutEndpoint, endStream: false) + + guard case .rejectRPC(let trailers) = action else { + XCTFail("RPC should have been rejected.") + return } + + XCTAssertEqual(trailers.count, 2) + XCTAssertEqual(trailers.grpcStatus, .unimplemented) + XCTAssertEqual(trailers.grpcStatusMessage, "No :path header has been set.") } func testReceiveMetadataWhenClientIdleAndServerIdle_Encoding() { @@ -1549,7 +1602,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Try opening client if no compression has been configured in the server: // should fail. XCTAssertThrowsError(ofType: RPCError.self, - try noCompressionStateMachine.receive(metadata: self.receivedHeadersWithDeflateCompression, endStream: false)) { error in + try noCompressionStateMachine.receive(metadata: .receivedHeadersWithDeflateCompression, endStream: false)) { error in XCTAssertEqual(error.code, .unimplemented) XCTAssertEqual(error.message, "Compression is not supported") } @@ -1562,11 +1615,11 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) // Try receiving initial metadata again - should fail XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) { error in + try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client shouldn't have sent metadata twice.") } @@ -1576,13 +1629,13 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) { error in + try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client shouldn't have sent metadata twice.") } @@ -1592,7 +1645,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1601,7 +1654,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) { error in + try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client shouldn't have sent metadata twice.") } @@ -1611,13 +1664,13 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) // Close client XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) { error in + try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't have sent metadata if closed.") } @@ -1627,7 +1680,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1637,7 +1690,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) { error in + try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't have sent metadata if closed.") } @@ -1647,7 +1700,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1659,7 +1712,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) { error in + try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't have sent metadata if closed.") } @@ -1681,7 +1734,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) // Receive messages successfully: the second one should close client. XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: false)) @@ -1699,7 +1752,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1720,7 +1773,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1736,7 +1789,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) // Close client XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) @@ -1752,7 +1805,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1771,7 +1824,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1805,7 +1858,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.nextOutboundMessage()) { error in @@ -1818,7 +1871,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.nextOutboundMessage()) { error in @@ -1831,40 +1884,38 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - var request = try stateMachine.nextOutboundMessage() - XCTAssertNil(request) + XCTAssertNil(try stateMachine.nextOutboundMessage()) XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) - request = try XCTUnwrap(stateMachine.nextOutboundMessage()) + let request = try XCTUnwrap(stateMachine.nextOutboundMessage()) let expectedBytes: [UInt8] = [ 0, // compression flag: unset 0, 0, 0, 2, // message length: 2 bytes 42, 42 // original message ] - XCTAssertEqual(Array(buffer: request!), expectedBytes) + XCTAssertEqual(Array(buffer: request), expectedBytes) } func testNextOutboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { var stateMachine = makeServerStateMachineWithCompression() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeadersWithDeflateCompression, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeadersWithDeflateCompression, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - var request = try stateMachine.nextOutboundMessage() - XCTAssertNil(request) + XCTAssertNil(try stateMachine.nextOutboundMessage()) let originalMessage = [UInt8]([42, 42, 43, 43]) XCTAssertNoThrow(try stateMachine.send(message: originalMessage, endStream: false)) - request = try XCTUnwrap(stateMachine.nextOutboundMessage()) + let request = try XCTUnwrap(stateMachine.nextOutboundMessage()) var framer = GRPCMessageFramer() let compressor = Zlib.Compressor(method: .deflate) @@ -1873,14 +1924,14 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { let framedMessage = try XCTUnwrap(framer.next(compressor: compressor)) let expectedBytes = Array(buffer: framedMessage) - XCTAssertEqual(Array(buffer: request!), expectedBytes) + XCTAssertEqual(Array(buffer: request), expectedBytes) } func testNextOutboundMessageWhenClientOpenAndServerClosed() throws { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1899,7 +1950,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) // Close client XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) @@ -1915,7 +1966,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1951,7 +2002,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -1985,7 +2036,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) XCTAssertNil(stateMachine.nextInboundMessage()) } @@ -1994,7 +2045,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -2016,7 +2067,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachineWithCompression() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeadersWithDeflateCompression, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeadersWithDeflateCompression, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -2040,7 +2091,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -2065,7 +2116,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) // Close client XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) @@ -2077,7 +2128,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) @@ -2104,7 +2155,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine() // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: self.receivedHeaders, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) From ed19ae7d999b6c5ee57fcc7051199fd9383a7fc4 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Fri, 16 Feb 2024 15:11:15 +0000 Subject: [PATCH 15/51] Formatting --- Sources/GRPCCore/Internal/Base64.swift | 131 +- Sources/GRPCCore/Metadata.swift | 2 +- .../GRPCStreamStateMachine.swift | 419 ++-- .../GRPCStreamStateMachineTests.swift | 1716 ++++++++++------- 4 files changed, 1309 insertions(+), 959 deletions(-) diff --git a/Sources/GRPCCore/Internal/Base64.swift b/Sources/GRPCCore/Internal/Base64.swift index 8ccf2b324..70864d099 100644 --- a/Sources/GRPCCore/Internal/Base64.swift +++ b/Sources/GRPCCore/Internal/Base64.swift @@ -20,18 +20,18 @@ Copyright (c) 2015-2016, Wojciech Muła, Alfred Klomp, Daniel Lemire (Unless otherwise stated in the source code) All rights reserved. - + Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - + 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - + 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A @@ -48,19 +48,19 @@ // https://github.com/client9/stringencoders/blob/master/src/modp_b64.c /* The MIT License (MIT) - + Copyright (c) 2016 Nick Galbreath - + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -74,45 +74,53 @@ enum Base64 { struct DecodingOptions: OptionSet { internal let rawValue: UInt internal init(rawValue: UInt) { self.rawValue = rawValue } - + internal static let base64UrlAlphabet = DecodingOptions(rawValue: UInt(1 << 0)) internal static let omitPaddingCharacter = DecodingOptions(rawValue: UInt(1 << 1)) } - + enum DecodingError: Error, Equatable { case invalidLength case invalidCharacter(UInt8) case unexpectedPaddingCharacter case unexpectedEnd } - + static func encode(bytes: Buffer) -> String where Buffer.Element == UInt8 { guard !bytes.isEmpty else { return "" } - + // In Base64, 3 bytes become 4 output characters, and we pad to the // nearest multiple of four. let base64StringLength = ((bytes.count + 2) / 3) * 4 let alphabet = Base64.encodeBase64 - + return String(customUnsafeUninitializedCapacity: base64StringLength) { backingStorage in var input = bytes.makeIterator() var offset = 0 while let firstByte = input.next() { let secondByte = input.next() let thirdByte = input.next() - + backingStorage[offset] = Base64.encode(alphabet: alphabet, firstByte: firstByte) - backingStorage[offset + 1] = Base64.encode(alphabet: alphabet, firstByte: firstByte, secondByte: secondByte) - backingStorage[offset + 2] = Base64.encode(alphabet: alphabet, secondByte: secondByte, thirdByte: thirdByte) + backingStorage[offset + 1] = Base64.encode( + alphabet: alphabet, + firstByte: firstByte, + secondByte: secondByte + ) + backingStorage[offset + 2] = Base64.encode( + alphabet: alphabet, + secondByte: secondByte, + thirdByte: thirdByte + ) backingStorage[offset + 3] = Base64.encode(alphabet: alphabet, thirdByte: thirdByte) offset += 4 } return offset } } - + static func decode( string encoded: String, options: DecodingOptions = [] @@ -122,25 +130,25 @@ enum Base64 { guard characterPointer.count > 0 else { return [] } - + let outputLength = ((characterPointer.count + 3) / 4) * 3 - + return try characterPointer.withMemoryRebound(to: UInt8.self) { (input) -> [UInt8] in try [UInt8](unsafeUninitializedCapacity: outputLength) { output, length in try Self._decodeChromium(from: input, into: output, length: &length, options: options) } } } - + if decoded != nil { return decoded! } - + var encoded = encoded encoded.makeContiguousUTF8() return try Self.decode(string: encoded, options: options) } - + private static func _decodeChromium( from inBuffer: UnsafeBufferPointer, into outBuffer: UnsafeMutableBufferPointer, @@ -157,13 +165,13 @@ enum Base64 { // everythin alright so far break } - + let outputLength = ((inBuffer.count + 3) / 4) * 3 let fullchunks = remaining == 0 ? inBuffer.count / 4 - 1 : inBuffer.count / 4 guard outBuffer.count >= outputLength else { preconditionFailure("Expected the out buffer to be at least as long as outputLength") } - + try Self.withUnsafeDecodingTablesAsBufferPointers(options: options) { d0, d1, d2, d3 in var outIndex = 0 if fullchunks > 0 { @@ -174,12 +182,12 @@ enum Base64 { let a2 = inBuffer[inIndex + 2] let a3 = inBuffer[inIndex + 3] var x: UInt32 = d0[Int(a0)] | d1[Int(a1)] | d2[Int(a2)] | d3[Int(a3)] - + if x >= Self.badCharacter { // TODO: Inspect characters here better throw DecodingError.invalidCharacter(inBuffer[inIndex]) } - + withUnsafePointer(to: &x) { ptr in ptr.withMemoryRebound(to: UInt8.self, capacity: 4) { newPtr in outBuffer[outIndex] = newPtr[0] @@ -190,7 +198,7 @@ enum Base64 { } } } - + // inIndex is the first index in the last chunk let inIndex = fullchunks * 4 let a0 = inBuffer[inIndex] @@ -203,13 +211,13 @@ enum Base64 { if inIndex + 3 < inBuffer.count, inBuffer[inIndex + 3] != Self.encodePaddingCharacter { a3 = inBuffer[inIndex + 3] } - + var x: UInt32 = d0[Int(a0)] | d1[Int(a1)] | d2[Int(a2 ?? 65)] | d3[Int(a3 ?? 65)] if x >= Self.badCharacter { // TODO: Inspect characters here better throw DecodingError.invalidCharacter(inBuffer[inIndex]) } - + withUnsafePointer(to: &x) { ptr in ptr.withMemoryRebound(to: UInt8.self, capacity: 4) { newPtr in outBuffer[outIndex] = newPtr[0] @@ -224,11 +232,11 @@ enum Base64 { } } } - + length = outIndex } } - + private static func withUnsafeDecodingTablesAsBufferPointers( options: Base64.DecodingOptions, _ body: ( @@ -240,12 +248,12 @@ enum Base64 { let decoding1 = options.contains(.base64UrlAlphabet) ? Self.decoding1url : Self.decoding1 let decoding2 = options.contains(.base64UrlAlphabet) ? Self.decoding2url : Self.decoding2 let decoding3 = options.contains(.base64UrlAlphabet) ? Self.decoding3url : Self.decoding3 - + assert(decoding0.count == 256) assert(decoding1.count == 256) assert(decoding2.count == 256) assert(decoding3.count == 256) - + return try decoding0.withUnsafeBufferPointer { (d0) -> R in try decoding1.withUnsafeBufferPointer { (d1) -> R in try decoding2.withUnsafeBufferPointer { (d2) -> R in @@ -256,9 +264,9 @@ enum Base64 { } } } - + private static let encodePaddingCharacter: UInt8 = 61 - + private static let encodeBase64: [UInt8] = [ UInt8(ascii: "A"), UInt8(ascii: "B"), UInt8(ascii: "C"), UInt8(ascii: "D"), UInt8(ascii: "E"), UInt8(ascii: "F"), UInt8(ascii: "G"), UInt8(ascii: "H"), @@ -277,12 +285,12 @@ enum Base64 { UInt8(ascii: "4"), UInt8(ascii: "5"), UInt8(ascii: "6"), UInt8(ascii: "7"), UInt8(ascii: "8"), UInt8(ascii: "9"), UInt8(ascii: "+"), UInt8(ascii: "/"), ] - + private static func encode(alphabet: [UInt8], firstByte: UInt8) -> UInt8 { let index = firstByte >> 2 return alphabet[Int(index)] } - + private static func encode(alphabet: [UInt8], firstByte: UInt8, secondByte: UInt8?) -> UInt8 { var index = (firstByte & 0b00000011) << 4 if let secondByte = secondByte { @@ -290,7 +298,7 @@ enum Base64 { } return alphabet[Int(index)] } - + private static func encode(alphabet: [UInt8], secondByte: UInt8?, thirdByte: UInt8?) -> UInt8 { guard let secondByte = secondByte else { // No second byte means we are just emitting padding. @@ -302,7 +310,7 @@ enum Base64 { } return alphabet[Int(index)] } - + private static func encode(alphabet: [UInt8], thirdByte: UInt8?) -> UInt8 { guard let thirdByte = thirdByte else { // No third byte means just padding. @@ -311,9 +319,9 @@ enum Base64 { let index = thirdByte & 0b00111111 return alphabet[Int(index)] } - + private static let badCharacter: UInt32 = 0x01FF_FFFF - + private static let decoding0: [UInt32] = [ 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, @@ -359,7 +367,7 @@ enum Base64 { 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, ] - + private static let decoding1: [UInt32] = [ 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, @@ -405,7 +413,7 @@ enum Base64 { 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, ] - + private static let decoding2: [UInt32] = [ 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, @@ -451,7 +459,7 @@ enum Base64 { 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, ] - + private static let decoding3: [UInt32] = [ 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, @@ -497,7 +505,7 @@ enum Base64 { 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, ] - + private static let decoding0url: [UInt32] = [ 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 0 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 6 @@ -543,7 +551,7 @@ enum Base64 { 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, ] - + private static let decoding1url: [UInt32] = [ 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 0 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 6 @@ -589,7 +597,7 @@ enum Base64 { 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, ] - + private static let decoding2url: [UInt32] = [ 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 0 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 6 @@ -635,7 +643,7 @@ enum Base64 { 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, ] - + private static let decoding3url: [UInt32] = [ 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 0 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 6 @@ -687,30 +695,37 @@ extension String { /// This is a backport of a proposed String initializer that will allow writing directly into an uninitialized String's backing memory. /// /// As this API does not exist prior to 5.3 on Linux, or on older Apple platforms, we fake it out with a pointer and accept the extra copy. - init(backportUnsafeUninitializedCapacity capacity: Int, - initializingUTF8With initializer: (_ buffer: UnsafeMutableBufferPointer) throws -> Int) rethrows { - + init( + backportUnsafeUninitializedCapacity capacity: Int, + initializingUTF8With initializer: (_ buffer: UnsafeMutableBufferPointer) throws -> Int + ) rethrows { + // The buffer will store zero terminated C string let buffer = UnsafeMutableBufferPointer.allocate(capacity: capacity + 1) defer { buffer.deallocate() } - + let initializedCount = try initializer(buffer) precondition(initializedCount <= capacity, "Overran buffer in initializer!") - + // add zero termination buffer[initializedCount] = 0 - + self = String(cString: buffer.baseAddress!) } - - init(customUnsafeUninitializedCapacity capacity: Int, - initializingUTF8With initializer: (_ buffer: UnsafeMutableBufferPointer) throws -> Int) rethrows { + + init( + customUnsafeUninitializedCapacity capacity: Int, + initializingUTF8With initializer: (_ buffer: UnsafeMutableBufferPointer) throws -> Int + ) rethrows { if #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) { try self.init(unsafeUninitializedCapacity: capacity, initializingUTF8With: initializer) } else { - try self.init(backportUnsafeUninitializedCapacity: capacity, initializingUTF8With: initializer) + try self.init( + backportUnsafeUninitializedCapacity: capacity, + initializingUTF8With: initializer + ) } } } diff --git a/Sources/GRPCCore/Metadata.swift b/Sources/GRPCCore/Metadata.swift index c9272acda..1b0154507 100644 --- a/Sources/GRPCCore/Metadata.swift +++ b/Sources/GRPCCore/Metadata.swift @@ -85,7 +85,7 @@ public struct Metadata: Sendable, Hashable { public enum Value: Sendable, Hashable { case string(String) case binary([UInt8]) - + /// The value as a String. If it was originally stored as a binary, the base64-encoded String version /// of the binary data will be returned instead. public var stringValue: String { diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 8c5e190f5..ea3ba3e06 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -33,14 +33,14 @@ enum Scheme: String { enum GRPCStreamStateMachineConfiguration { case client(ClientConfiguration) case server(ServerConfiguration) - + struct ClientConfiguration { var methodDescriptor: MethodDescriptor var scheme: Scheme var outboundEncoding: CompressionAlgorithm? var acceptedEncodings: [CompressionAlgorithm] } - + struct ServerConfiguration { var scheme: Scheme var acceptedEncodings: [CompressionAlgorithm] @@ -55,44 +55,44 @@ enum GRPCStreamStateMachineState { case clientClosedServerIdle(ClientClosedServerIdleState) case clientClosedServerOpen(ClientClosedServerOpenState) case clientClosedServerClosed(ClientClosedServerClosedState) - + struct ClientIdleServerIdleState { let maximumPayloadSize: Int } - + enum DecompressionConfiguration { case decompressionNotYetKnown case decompression(CompressionAlgorithm?) } - + struct ClientOpenServerIdleState { let maximumPayloadSize: Int var framer: GRPCMessageFramer var compressor: Zlib.Compressor? var outboundCompression: CompressionAlgorithm? - + // The deframer must be optional because the client will not have one configured // until the server opens and sends a grpc-encoding header. // It will be present for the server though, because even though it's idle, // it can still receive compressed messages from the client. let deframer: NIOSingleStepByteToMessageProcessor? var decompressor: Zlib.Decompressor? - + var inboundMessageBuffer: OneOrManyQueue<[UInt8]> - + init( previousState: ClientIdleServerIdleState, compressionAlgorithm: CompressionAlgorithm?, decompressionConfiguration: DecompressionConfiguration ) { self.maximumPayloadSize = previousState.maximumPayloadSize - + if let zlibMethod = Zlib.Method(encoding: compressionAlgorithm) { self.compressor = Zlib.Compressor(method: zlibMethod) } self.framer = GRPCMessageFramer() self.outboundCompression = compressionAlgorithm - + // In the case of the server, we will know what the decompression algorithm // will be, since we know what the inbound encoding is, as the client has // sent it when starting the request. @@ -110,27 +110,27 @@ enum GRPCStreamStateMachineState { } else { self.deframer = nil } - + self.inboundMessageBuffer = .init() } } - + struct ClientOpenServerOpenState { var framer: GRPCMessageFramer var compressor: Zlib.Compressor? - + let deframer: NIOSingleStepByteToMessageProcessor var decompressor: Zlib.Decompressor? - + var inboundMessageBuffer: OneOrManyQueue<[UInt8]> - + init( previousState: ClientOpenServerIdleState, decompressionAlgorithm: CompressionAlgorithm? = nil ) { self.framer = previousState.framer self.compressor = previousState.compressor - + // In the case of the server, it will already have a deframer set up, // because it already knows what encoding the client is using. // In the case of the client, it will only be able to set it up @@ -148,20 +148,20 @@ enum GRPCStreamStateMachineState { ) self.deframer = NIOSingleStepByteToMessageProcessor(decoder) } - + self.inboundMessageBuffer = previousState.inboundMessageBuffer } } - + struct ClientOpenServerClosedState { var framer: GRPCMessageFramer var compressor: Zlib.Compressor? - + let deframer: NIOSingleStepByteToMessageProcessor? var decompressor: Zlib.Decompressor? - + var inboundMessageBuffer: OneOrManyQueue<[UInt8]> - + init(previousState: ClientOpenServerOpenState) { self.framer = previousState.framer self.compressor = previousState.compressor @@ -169,7 +169,7 @@ enum GRPCStreamStateMachineState { self.decompressor = previousState.decompressor self.inboundMessageBuffer = previousState.inboundMessageBuffer } - + init(previousState: ClientOpenServerIdleState) { self.framer = previousState.framer self.compressor = previousState.compressor @@ -184,18 +184,18 @@ enum GRPCStreamStateMachineState { self.decompressor = previousState.decompressor } } - + struct ClientClosedServerIdleState { let maximumPayloadSize: Int var framer: GRPCMessageFramer var compressor: Zlib.Compressor? var outboundCompression: CompressionAlgorithm? - + let deframer: NIOSingleStepByteToMessageProcessor? var decompressor: Zlib.Decompressor? - + var inboundMessageBuffer: OneOrManyQueue<[UInt8]> - + init(previousState: ClientOpenServerIdleState) { self.maximumPayloadSize = previousState.maximumPayloadSize self.framer = previousState.framer @@ -206,16 +206,16 @@ enum GRPCStreamStateMachineState { self.inboundMessageBuffer = previousState.inboundMessageBuffer } } - + struct ClientClosedServerOpenState { var framer: GRPCMessageFramer var compressor: Zlib.Compressor? - + let deframer: NIOSingleStepByteToMessageProcessor var decompressor: Zlib.Decompressor? - + var inboundMessageBuffer: OneOrManyQueue<[UInt8]> - + init(previousState: ClientOpenServerOpenState) { self.framer = previousState.framer self.compressor = previousState.compressor @@ -223,7 +223,7 @@ enum GRPCStreamStateMachineState { self.decompressor = previousState.decompressor self.inboundMessageBuffer = previousState.inboundMessageBuffer } - + init( previousState: ClientClosedServerIdleState, decompressionAlgorithm: CompressionAlgorithm? = nil @@ -231,7 +231,7 @@ enum GRPCStreamStateMachineState { self.framer = previousState.framer self.compressor = previousState.compressor self.inboundMessageBuffer = previousState.inboundMessageBuffer - + // In the case of the server, it will already have a deframer set up, // because it already knows what encoding the client is using. // In the case of the client, it will only be able to set it up @@ -251,19 +251,19 @@ enum GRPCStreamStateMachineState { } } } - + struct ClientClosedServerClosedState { // These are already deframed, so we don't need the deframer anymore. var inboundMessageBuffer: OneOrManyQueue<[UInt8]> - + init(previousState: ClientClosedServerOpenState) { self.inboundMessageBuffer = previousState.inboundMessageBuffer } - + init(previousState: ClientClosedServerIdleState) { self.inboundMessageBuffer = previousState.inboundMessageBuffer } - + init(previousState: ClientOpenServerClosedState) { self.inboundMessageBuffer = previousState.inboundMessageBuffer } @@ -275,7 +275,7 @@ struct GRPCStreamStateMachine { private var state: GRPCStreamStateMachineState private var configuration: GRPCStreamStateMachineConfiguration private var skipAssertions: Bool - + init( configuration: GRPCStreamStateMachineConfiguration, maximumPayloadSize: Int, @@ -285,7 +285,7 @@ struct GRPCStreamStateMachine { self.configuration = configuration self.skipAssertions = skipAssertions } - + mutating func send(metadata: Metadata) throws -> HPACKHeaders { switch self.configuration { case .client(let clientConfiguration): @@ -294,7 +294,7 @@ struct GRPCStreamStateMachine { return try serverSend(metadata: metadata) } } - + mutating func send(message: [UInt8], endStream: Bool) throws { switch self.configuration { case .client: @@ -303,25 +303,32 @@ struct GRPCStreamStateMachine { try serverSend(message: message, endStream: endStream) } } - - mutating func send(status: Status, metadata: Metadata, trailersOnly: Bool) throws -> HPACKHeaders { + + mutating func send(status: Status, metadata: Metadata, trailersOnly: Bool) throws -> HPACKHeaders + { switch self.configuration { case .client: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client cannot send status and trailer.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError( + "Client cannot send status and trailer." + ) case .server: return try serverSend(status: status, metadata: metadata, trailersOnly: trailersOnly) } } - + mutating func receive(metadata: HPACKHeaders, endStream: Bool) throws -> OnMetadataReceived { switch self.configuration { case .client: return try clientReceive(metadata: metadata, endStream: endStream) case .server(let serverConfiguration): - return try serverReceive(metadata: metadata, endStream: endStream, configuration: serverConfiguration) + return try serverReceive( + metadata: metadata, + endStream: endStream, + configuration: serverConfiguration + ) } } - + mutating func receive(message: ByteBuffer, endStream: Bool) throws { switch self.configuration { case .client: @@ -330,7 +337,7 @@ struct GRPCStreamStateMachine { try serverReceive(bytes: message, endStream: endStream) } } - + mutating func nextOutboundMessage() throws -> ByteBuffer? { switch self.configuration { case .client: @@ -339,7 +346,7 @@ struct GRPCStreamStateMachine { return try serverNextOutboundMessage() } } - + mutating func nextInboundMessage() -> [UInt8]? { switch self.configuration { case .client: @@ -361,30 +368,30 @@ extension GRPCStreamStateMachine { ) -> HPACKHeaders { var headers = HPACKHeaders() headers.reserveCapacity(7 + customMetadata.count) - + // Add required headers // See https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests headers.path = methodDescriptor headers.scheme = scheme headers.method = "POST" headers.contentType = .protobuf - headers.te = "trailers" // Used to detect incompatible proxies - + headers.te = "trailers" // Used to detect incompatible proxies + if let encoding = outboundEncoding { headers.encoding = encoding } - + if !acceptedEncodings.isEmpty { headers.acceptedEncodings = acceptedEncodings } - + for metadataPair in customMetadata { headers.add(name: metadataPair.key, value: metadataPair.value.stringValue) } - + return headers } - + private mutating func clientSend( metadata: Metadata, configuration: GRPCStreamStateMachineConfiguration.ClientConfiguration @@ -392,11 +399,13 @@ extension GRPCStreamStateMachine { // Client sends metadata only when opening the stream. switch self.state { case .clientIdleServerIdle(let state): - self.state = .clientOpenServerIdle(.init( - previousState: state, - compressionAlgorithm: configuration.outboundEncoding, - decompressionConfiguration: .decompressionNotYetKnown - )) + self.state = .clientOpenServerIdle( + .init( + previousState: state, + compressionAlgorithm: configuration.outboundEncoding, + decompressionConfiguration: .decompressionNotYetKnown + ) + ) return self.makeClientHeaders( methodDescriptor: configuration.methodDescriptor, scheme: configuration.scheme, @@ -405,12 +414,16 @@ extension GRPCStreamStateMachine { customMetadata: metadata ) case .clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client is already open: shouldn't be sending metadata.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError( + "Client is already open: shouldn't be sending metadata." + ) case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client is closed: can't send metadata.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError( + "Client is closed: can't send metadata." + ) } } - + private mutating func clientSend(message: [UInt8], endStream: Bool) throws { // Client sends message. switch self.state { @@ -437,10 +450,12 @@ extension GRPCStreamStateMachine { self.state = .clientClosedServerClosed(.init(previousState: state)) } case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client is closed, cannot send a message.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError( + "Client is closed, cannot send a message." + ) } } - + /// Returns the client's next request to the server. /// - Returns: The request to be made to the server. private mutating func clientNextOutboundMessage() throws -> ByteBuffer? { @@ -474,18 +489,23 @@ extension GRPCStreamStateMachine { return nil } } - - private mutating func clientReceive(metadata: HPACKHeaders, endStream: Bool) throws -> OnMetadataReceived { + + private mutating func clientReceive( + metadata: HPACKHeaders, + endStream: Bool + ) throws -> OnMetadataReceived { switch self.state { case .clientOpenServerIdle(let state): if endStream { // This is a trailers-only response: close server. self.state = .clientOpenServerClosed(.init(previousState: state)) } else { - self.state = .clientOpenServerOpen(.init( - previousState: state, - decompressionAlgorithm: metadata.encoding - )) + self.state = .clientOpenServerOpen( + .init( + previousState: state, + decompressionAlgorithm: metadata.encoding + ) + ) } return .receivedMetadata(Metadata(headers: metadata)) case .clientOpenServerOpen(let state): @@ -506,10 +526,12 @@ extension GRPCStreamStateMachine { // This is a trailers-only response. self.state = .clientClosedServerClosed(.init(previousState: state)) } else { - self.state = .clientClosedServerOpen(.init( - previousState: state, - decompressionAlgorithm: metadata.encoding - )) + self.state = .clientClosedServerOpen( + .init( + previousState: state, + decompressionAlgorithm: metadata.encoding + ) + ) } return .receivedMetadata(Metadata(headers: metadata)) case .clientClosedServerOpen(let state): @@ -535,23 +557,33 @@ extension GRPCStreamStateMachine { // Note that we don't want to ignore it if EOS is not set here though, as // then it would be an invalid payload. if !endStream || metadata.count > 0 { - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server is closed, nothing could have been sent.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError( + "Server is closed, nothing could have been sent." + ) } return .receivedMetadata([]) case .clientIdleServerIdle: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server cannot have sent metadata if the client is idle.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError( + "Server cannot have sent metadata if the client is idle." + ) case .clientOpenServerClosed: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server is closed, nothing could have been sent.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError( + "Server is closed, nothing could have been sent." + ) } } - + private mutating func clientReceive(bytes: ByteBuffer, endStream: Bool) throws { // This is a message received by the client, from the server. switch self.state { case .clientIdleServerIdle: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Cannot have received anything from server if client is not yet open.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError( + "Cannot have received anything from server if client is not yet open." + ) case .clientOpenServerIdle, .clientClosedServerIdle: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server cannot have sent a message before sending the initial metadata.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError( + "Server cannot have sent a message before sending the initial metadata." + ) case .clientOpenServerOpen(var state): try state.deframer.process(buffer: bytes) { deframedMessage in state.inboundMessageBuffer.append(deframedMessage) @@ -573,12 +605,16 @@ extension GRPCStreamStateMachine { self.state = .clientClosedServerOpen(state) } case .clientOpenServerClosed: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Cannot have received anything from a closed server.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError( + "Cannot have received anything from a closed server." + ) case .clientClosedServerClosed: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Shouldn't have received anything if both client and server are closed.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError( + "Shouldn't have received anything if both client and server are closed." + ) } } - + private mutating func clientNextInboundMessage() -> [UInt8]? { switch self.state { case .clientOpenServerOpen(var state): @@ -598,13 +634,16 @@ extension GRPCStreamStateMachine { self.state = .clientClosedServerClosed(state) return message case .clientIdleServerIdle, - .clientOpenServerIdle, - .clientClosedServerIdle: + .clientOpenServerIdle, + .clientClosedServerIdle: return nil } } - - private func assertionFailureAndCreateRPCErrorOnInternalError(_ message: String, line: UInt = #line) -> RPCError { + + private func assertionFailureAndCreateRPCErrorOnInternalError( + _ message: String, + line: UInt = #line + ) -> RPCError { if !self.skipAssertions { assertionFailure(message, line: line) } @@ -614,48 +653,65 @@ extension GRPCStreamStateMachine { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension GRPCStreamStateMachine { - private func makeResponseHeaders(outboundEncoding: CompressionAlgorithm?, customMetadata: Metadata) -> HPACKHeaders { + private func makeResponseHeaders( + outboundEncoding: CompressionAlgorithm?, + customMetadata: Metadata + ) -> HPACKHeaders { // Response headers always contain :status (HTTP Status 200) and content-type. // They may also contain grpc-encoding, grpc-accept-encoding, and custom metadata. var headers = HPACKHeaders() headers.reserveCapacity(4 + customMetadata.count) - + headers.status = "200" headers.contentType = .protobuf - + if let outboundEncoding { headers.encoding = outboundEncoding } - + for metadataPair in customMetadata { headers.add(name: metadataPair.key, value: metadataPair.value.stringValue) } - + return headers } - + private mutating func serverSend(metadata: Metadata) throws -> HPACKHeaders { // Server sends initial metadata switch self.state { case .clientOpenServerIdle(let state): self.state = .clientOpenServerOpen(.init(previousState: state)) - return self.makeResponseHeaders(outboundEncoding: state.outboundCompression, customMetadata: metadata) + return self.makeResponseHeaders( + outboundEncoding: state.outboundCompression, + customMetadata: metadata + ) case .clientClosedServerIdle(let state): self.state = .clientClosedServerOpen(.init(previousState: state)) - return self.makeResponseHeaders(outboundEncoding: state.outboundCompression, customMetadata: metadata) + return self.makeResponseHeaders( + outboundEncoding: state.outboundCompression, + customMetadata: metadata + ) case .clientIdleServerIdle: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client cannot be idle if server is sending initial metadata: it must have opened.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError( + "Client cannot be idle if server is sending initial metadata: it must have opened." + ) case .clientOpenServerClosed, .clientClosedServerClosed: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server cannot send metadata if closed.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError( + "Server cannot send metadata if closed." + ) case .clientOpenServerOpen, .clientClosedServerOpen: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server has already sent initial metadata.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError( + "Server has already sent initial metadata." + ) } } - + private mutating func serverSend(message: [UInt8], endStream: Bool) throws { switch self.state { case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server must have sent initial metadata before sending a message.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError( + "Server must have sent initial metadata before sending a message." + ) case .clientOpenServerOpen(var state): state.framer.append(message) if endStream { @@ -671,10 +727,12 @@ extension GRPCStreamStateMachine { self.state = .clientClosedServerOpen(state) } case .clientOpenServerClosed, .clientClosedServerClosed: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server can't send a message if it's closed.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError( + "Server can't send a message if it's closed." + ) } } - + private func makeTrailers( status: Status, customMetadata: Metadata, @@ -685,7 +743,7 @@ extension GRPCStreamStateMachine { // If it's a trailers-only response, they will also contain :status and // content-type. var headers = HPACKHeaders() - + if trailersOnly { // Reserve 5 for capacity: 3 for the required headers, and 1 for the // optional status message. @@ -697,22 +755,26 @@ extension GRPCStreamStateMachine { // one for the optional message. headers.reserveCapacity(2 + customMetadata.count) } - + headers.grpcStatus = status.code - + if !status.message.isEmpty { // TODO: this message has to be percent-encoded headers.grpcStatusMessage = status.message } - + for metadataPair in customMetadata { headers.add(name: metadataPair.key, value: metadataPair.value.stringValue) } - + return headers } - - private mutating func serverSend(status: Status, metadata: Metadata, trailersOnly: Bool) throws -> HPACKHeaders { + + private mutating func serverSend( + status: Status, + metadata: Metadata, + trailersOnly: Bool + ) throws -> HPACKHeaders { // Close the server. switch self.state { case .clientOpenServerOpen(let state): @@ -724,12 +786,16 @@ extension GRPCStreamStateMachine { self.state = .clientClosedServerClosed(.init(previousState: state)) return self.makeTrailers(status: status, customMetadata: metadata, trailersOnly: trailersOnly) case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server can't send status if idle.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError( + "Server can't send status if idle." + ) case .clientOpenServerClosed, .clientClosedServerClosed: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server can't send anything if closed.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError( + "Server can't send anything if closed." + ) } } - + private mutating func serverReceive( metadata: HPACKHeaders, endStream: Bool, @@ -737,13 +803,13 @@ extension GRPCStreamStateMachine { ) throws -> OnMetadataReceived { if endStream, case .clientIdleServerIdle = self.state { throw self.assertionFailureAndCreateRPCErrorOnInternalError( - """ - Client should have opened before ending the stream: \ - stream shouldn't have been closed when sending initial metadata. - """ + """ + Client should have opened before ending the stream: \ + stream shouldn't have been closed when sending initial metadata. + """ ) } - + switch self.state { case .clientIdleServerIdle(let state): guard metadata.contentType != nil else { @@ -752,7 +818,7 @@ extension GRPCStreamStateMachine { trailers.status = "415" return .rejectRPC(trailers: trailers) } - + guard metadata.path != nil else { var trailers = HPACKHeaders() trailers.reserveCapacity(2) @@ -762,25 +828,31 @@ extension GRPCStreamStateMachine { } func isIdentityOrCompatibleEncoding(_ clientEncoding: CompressionAlgorithm) -> Bool { - clientEncoding == .identity || - configuration.acceptedEncodings.contains(where: { $0 == clientEncoding }) + clientEncoding == .identity + || configuration.acceptedEncodings.contains(where: { $0 == clientEncoding }) } - + // Firstly, find out if we support the client's chosen encoding, and reject // the RPC if we don't. var inboundEncoding: CompressionAlgorithm? = nil - let encodingValues = metadata.values(forHeader: GRPCHTTP2Keys.encoding.rawValue, canonicalForm: true) + let encodingValues = metadata.values( + forHeader: GRPCHTTP2Keys.encoding.rawValue, + canonicalForm: true + ) var encodingValuesIterator = encodingValues.makeIterator() if let rawEncoding = encodingValuesIterator.next() { guard encodingValuesIterator.next() == nil else { var trailers = HPACKHeaders() trailers.reserveCapacity(2) trailers.grpcStatus = .internalError - trailers.grpcStatusMessage = "\(GRPCHTTP2Keys.encoding) must contain no more than one value." + trailers.grpcStatusMessage = + "\(GRPCHTTP2Keys.encoding) must contain no more than one value." return .rejectRPC(trailers: trailers) } - - guard let clientEncoding = CompressionAlgorithm(rawValue: String(rawEncoding)), isIdentityOrCompatibleEncoding(clientEncoding) else { + + guard let clientEncoding = CompressionAlgorithm(rawValue: String(rawEncoding)), + isIdentityOrCompatibleEncoding(clientEncoding) + else { if configuration.acceptedEncodings.isEmpty { var trailers = HPACKHeaders() trailers.reserveCapacity(2) @@ -792,26 +864,27 @@ extension GRPCStreamStateMachine { trailers.reserveCapacity(3) trailers.grpcStatus = .unimplemented trailers.grpcStatusMessage = """ - \(rawEncoding) compression is not supported; \ - supported algorithms are listed in grpc-accept-encoding - """ + \(rawEncoding) compression is not supported; \ + supported algorithms are listed in grpc-accept-encoding + """ trailers.acceptedEncodings = configuration.acceptedEncodings return .rejectRPC(trailers: trailers) } } - + // Server supports client's encoding. // If it's identity, just skip it altogether. if clientEncoding != .identity { inboundEncoding = clientEncoding } } - + // Secondly, find a compatible encoding the server can use to compress outbound messages, // based on the encodings the client has advertised. var outboundEncoding: CompressionAlgorithm? = nil if let clientAdvertisedEncodings = metadata.acceptedEncodings { - for clientAcceptedEncoding in clientAdvertisedEncodings where isIdentityOrCompatibleEncoding(clientAcceptedEncoding) { + for clientAcceptedEncoding in clientAdvertisedEncodings + where isIdentityOrCompatibleEncoding(clientAcceptedEncoding) { // Found the preferred encoding: use it to compress responses. // If it's identity, just skip it altogether, since we won't be // compressing. @@ -821,25 +894,33 @@ extension GRPCStreamStateMachine { break } } - - self.state = .clientOpenServerIdle(.init( - previousState: state, - compressionAlgorithm: outboundEncoding, - decompressionConfiguration: .decompression(inboundEncoding) - )) - + + self.state = .clientOpenServerIdle( + .init( + previousState: state, + compressionAlgorithm: outboundEncoding, + decompressionConfiguration: .decompression(inboundEncoding) + ) + ) + return .receivedMetadata(Metadata(headers: metadata)) case .clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client shouldn't have sent metadata twice.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError( + "Client shouldn't have sent metadata twice." + ) case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client can't have sent metadata if closed.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError( + "Client can't have sent metadata if closed." + ) } } - + private mutating func serverReceive(bytes: ByteBuffer, endStream: Bool) throws { switch self.state { case .clientIdleServerIdle: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Can't have received a message if client is idle.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError( + "Can't have received a message if client is idle." + ) case .clientOpenServerIdle(var state): // Deframer must be present on the server side, as we know the decompression // algorithm from the moment the client opens. @@ -847,7 +928,7 @@ extension GRPCStreamStateMachine { try state.deframer!.process(buffer: bytes) { deframedMessage in state.inboundMessageBuffer.append(deframedMessage) } - + if endStream { self.state = .clientClosedServerIdle(.init(previousState: state)) } else { @@ -857,7 +938,7 @@ extension GRPCStreamStateMachine { try state.deframer.process(buffer: bytes) { deframedMessage in state.inboundMessageBuffer.append(deframedMessage) } - + if endStream { self.state = .clientClosedServerOpen(.init(previousState: state)) } else { @@ -868,10 +949,12 @@ extension GRPCStreamStateMachine { // Ignore the rest of the request: do nothing. () case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client can't send a message if closed.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError( + "Client can't send a message if closed." + ) } } - + private mutating func serverNextOutboundMessage() throws -> ByteBuffer? { switch self.state { case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: @@ -885,10 +968,12 @@ extension GRPCStreamStateMachine { self.state = .clientClosedServerOpen(state) return response case .clientOpenServerClosed, .clientClosedServerClosed: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Can't send response if server is closed.") + throw self.assertionFailureAndCreateRPCErrorOnInternalError( + "Can't send response if server is closed." + ) } } - + private mutating func serverNextInboundMessage() -> [UInt8]? { switch self.state { case .clientOpenServerIdle(var state): @@ -954,7 +1039,7 @@ extension HPACKHeaders { } } } - + var contentType: ContentType? { get { self.firstString(forKey: .contentType) @@ -968,7 +1053,7 @@ extension HPACKHeaders { } } } - + var encoding: CompressionAlgorithm? { get { self.firstString(forKey: .encoding).flatMap { CompressionAlgorithm(rawValue: $0) } @@ -981,7 +1066,7 @@ extension HPACKHeaders { } } } - + var acceptedEncodings: [CompressionAlgorithm]? { get { self.firstString(forKey: .acceptEncoding)? @@ -990,13 +1075,16 @@ extension HPACKHeaders { } set { if let newValue { - self.replaceOrAddString(newValue.map({ $0.name }).joined(separator: ","), forKey: .acceptEncoding) + self.replaceOrAddString( + newValue.map({ $0.name }).joined(separator: ","), + forKey: .acceptEncoding + ) } else { self.removeAllValues(forKey: .acceptEncoding) } } } - + var scheme: Scheme? { get { self.firstString(forKey: .scheme).flatMap { Scheme(rawValue: $0) } @@ -1009,7 +1097,7 @@ extension HPACKHeaders { } } } - + var method: String? { get { self.firstString(forKey: .method) @@ -1022,7 +1110,7 @@ extension HPACKHeaders { } } } - + var te: String? { get { self.firstString(forKey: .te) @@ -1035,7 +1123,7 @@ extension HPACKHeaders { } } } - + var status: String? { get { self.firstString(forKey: .status) @@ -1048,7 +1136,7 @@ extension HPACKHeaders { } } } - + var grpcStatus: Status.Code? { get { self.firstString(forKey: .grpcStatus) @@ -1063,7 +1151,7 @@ extension HPACKHeaders { } } } - + var grpcStatusMessage: String? { get { self.firstString(forKey: .grpcStatusMessage) @@ -1076,15 +1164,17 @@ extension HPACKHeaders { } } } - + private func firstString(forKey key: GRPCHTTP2Keys) -> String? { - self.values(forHeader: key.rawValue, canonicalForm: true).first(where: { _ in true }).map { String($0) } + self.values(forHeader: key.rawValue, canonicalForm: true).first(where: { _ in true }).map { + String($0) + } } - + private mutating func replaceOrAddString(_ value: String, forKey key: GRPCHTTP2Keys) { self.replaceOrAdd(name: key.rawValue, value: value) } - + private mutating func removeAllValues(forKey key: GRPCHTTP2Keys) { self.remove(name: key.rawValue) } @@ -1095,7 +1185,7 @@ extension Zlib.Method { guard let encoding else { return nil } - + switch encoding { case .identity: return nil @@ -1114,7 +1204,8 @@ extension Metadata { var metadata = Metadata() // TODO: since this is what we'll pass on to the user, I was wondering if it would be useful // to filter out the headers that relate to the protocol, and just leave the user-defined ones. - for header in headers where !GRPCHTTP2Keys.allCases.contains(where: { $0.rawValue == header.name}) { + for header in headers + where !GRPCHTTP2Keys.allCases.contains(where: { $0.rawValue == header.name }) { if header.name.hasSuffix("-bin") { do { let decodedBinary = try header.value.base64Decoded() diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index 1c02a0bd1..53c9525f1 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -15,891 +15,1000 @@ */ import GRPCCore -import XCTest import NIOCore import NIOHPACK +import XCTest @testable import GRPCHTTP2Core final class GRPCStreamClientStateMachineTests: XCTestCase { private func makeClientStateMachine() -> GRPCStreamStateMachine { GRPCStreamStateMachine( - configuration: .client( .init( - methodDescriptor: .init(service: "test", method: "test"), - scheme: .http, - outboundEncoding: nil, - acceptedEncodings: [.deflate] - )), + configuration: .client( + .init( + methodDescriptor: .init(service: "test", method: "test"), + scheme: .http, + outboundEncoding: nil, + acceptedEncodings: [.deflate] + ) + ), maximumPayloadSize: 100, skipAssertions: true ) } - + private func makeClientStateMachineWithCompression() -> GRPCStreamStateMachine { GRPCStreamStateMachine( - configuration: .client(.init( - methodDescriptor: .init(service: "test", method: "test"), - scheme: .http, - outboundEncoding: .deflate, - acceptedEncodings: [.deflate] - )), + configuration: .client( + .init( + methodDescriptor: .init(service: "test", method: "test"), + scheme: .http, + outboundEncoding: .deflate, + acceptedEncodings: [.deflate] + ) + ), maximumPayloadSize: 100, skipAssertions: true ) } - + // - MARK: Send Metadata func testSendMetadataWhenClientIdleAndServerIdle() throws { var stateMachine = makeClientStateMachine() XCTAssertNoThrow(try stateMachine.send(metadata: [])) } - + func testSendMetadataWhenClientOpenAndServerIdle() throws { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Try sending metadata again: should throw XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client is already open: shouldn't be sending metadata.") } } - + func testSendMetadataWhenClientOpenAndServerOpen() throws { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - + // Try sending metadata again: should throw XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client is already open: shouldn't be sending metadata.") } } - + func testSendMetadataWhenClientOpenAndServerClosed() throws { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - + // Close server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) - + // Try sending metadata again: should throw XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client is already open: shouldn't be sending metadata.") } } - + func testSendMetadataWhenClientClosedAndServerIdle() throws { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - + // Try sending metadata again: should throw XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client is closed: can't send metadata.") } } - + func testSendMetadataWhenClientClosedAndServerOpen() throws { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - + // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - + // Try sending metadata again: should throw XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client is closed: can't send metadata.") } } - + func testSendMetadataWhenClientClosedAndServerClosed() throws { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - + // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - + // Close server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) - + // Try sending metadata again: should throw XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client is closed: can't send metadata.") } } - + // - MARK: Send Message func testSendMessageWhenClientIdleAndServerIdle() { var stateMachine = makeClientStateMachine() - + // Try to send a message without opening (i.e. without sending initial metadata) - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(message: [], endStream: false)) { error in + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send(message: [], endStream: false) + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client not yet open.") } } - + func testSendMessageWhenClientOpenAndServerIdle() { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Now send a message XCTAssertNoThrow(try stateMachine.send(message: [], endStream: false)) } - + func testSendMessageWhenClientOpenAndServerOpen() { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - + // Now send a message XCTAssertNoThrow(try stateMachine.send(message: [], endStream: false)) } - + func testSendMessageWhenClientOpenAndServerClosed() { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - + // Close server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) - + // Now send a message XCTAssertNoThrow(try stateMachine.send(message: [], endStream: false)) } - + func testSendMessageWhenClientClosedAndServerIdle() { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - + // Try sending another message: it should fail - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(message: [], endStream: false)) { error in + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send(message: [], endStream: false) + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client is closed, cannot send a message.") } } - + func testSendMessageWhenClientClosedAndServerOpen() { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - + // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - + // Try sending another message: it should fail - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(message: [], endStream: false)) { error in + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send(message: [], endStream: false) + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client is closed, cannot send a message.") } } - + func testSendMessageWhenClientClosedAndServerClosed() { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - + // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - + // Close server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) - + // Try sending another message: it should fail - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(message: [], endStream: false)) { error in + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send(message: [], endStream: false) + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client is closed, cannot send a message.") } } - + // - MARK: Send Status and Trailers - + func testSendStatusAndTrailersWhenClientIdleAndServerIdle() { var stateMachine = makeClientStateMachine() - + // This operation is never allowed on the client. - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(status: Status(code: .ok, message: ""), metadata: .init(), trailersOnly: false)) { error in + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send( + status: Status(code: .ok, message: ""), + metadata: .init(), + trailersOnly: false + ) + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client cannot send status and trailer.") } } - + func testSendStatusAndTrailersWhenClientOpenAndServerIdle() { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // This operation is never allowed on the client. - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(status: Status(code: .ok, message: ""), metadata: .init(), trailersOnly: false)) { error in + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send( + status: Status(code: .ok, message: ""), + metadata: .init(), + trailersOnly: false + ) + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client cannot send status and trailer.") } } - + func testSendStatusAndTrailersWhenClientOpenAndServerOpen() { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - + // This operation is never allowed on the client. - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(status: Status(code: .ok, message: ""), metadata: .init(), trailersOnly: false)) { error in + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send( + status: Status(code: .ok, message: ""), + metadata: .init(), + trailersOnly: false + ) + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client cannot send status and trailer.") } } - + func testSendStatusAndTrailersWhenClientOpenAndServerClosed() { var stateMachine = makeClientStateMachine() - + // Open stream XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - + // Close server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) - + // This operation is never allowed on the client. - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(status: Status(code: .ok, message: ""), metadata: .init(), trailersOnly: false)) { error in + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send( + status: Status(code: .ok, message: ""), + metadata: .init(), + trailersOnly: false + ) + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client cannot send status and trailer.") } } - + func testSendStatusAndTrailersWhenClientClosedAndServerIdle() { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - + // This operation is never allowed on the client. - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(status: Status(code: .ok, message: ""), metadata: .init(), trailersOnly: false)) { error in + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send( + status: Status(code: .ok, message: ""), + metadata: .init(), + trailersOnly: false + ) + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client cannot send status and trailer.") } } - + func testSendStatusAndTrailersWhenClientClosedAndServerOpen() { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - + // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - + // This operation is never allowed on the client. - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(status: Status(code: .ok, message: ""), metadata: .init(), trailersOnly: false)) { error in + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send( + status: Status(code: .ok, message: ""), + metadata: .init(), + trailersOnly: false + ) + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client cannot send status and trailer.") } } - + func testSendStatusAndTrailersWhenClientClosedAndServerClosed() { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - + // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - + // Close server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) - + // This operation is never allowed on the client. - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(status: Status(code: .ok, message: ""), metadata: .init(), trailersOnly: false)) { error in + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send( + status: Status(code: .ok, message: ""), + metadata: .init(), + trailersOnly: false + ) + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client cannot send status and trailer.") } } - + // - MARK: Receive initial metadata - + func testReceiveInitialMetadataWhenClientIdleAndServerIdle() { var stateMachine = makeClientStateMachine() - - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: .init(), endStream: false)) { error in + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(metadata: .init(), endStream: false) + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server cannot have sent metadata if the client is idle.") } } - + func testReceiveInitialMetadataWhenClientOpenAndServerIdle() throws { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Receive metadata = open server - let action = try stateMachine.receive(metadata: [GRPCHTTP2Keys.encoding.rawValue: "deflate", "custom": "123", "custom-bin": String(base64Encoding: [42,43,44])], endStream: false) + let action = try stateMachine.receive( + metadata: [ + GRPCHTTP2Keys.encoding.rawValue: "deflate", "custom": "123", + "custom-bin": String(base64Encoding: [42, 43, 44]), + ], + endStream: false + ) guard case .receivedMetadata(let customMetadata) = action else { XCTFail("Expected action to be receivedMetadata") return } - + XCTAssertEqual(customMetadata.count, 2) let customHeaderStringValues = try XCTUnwrap(customMetadata[stringValues: "custom"]) var headerStringValuesIterator = customHeaderStringValues.makeIterator() XCTAssertEqual(headerStringValuesIterator.next(), "123") XCTAssertNil(headerStringValuesIterator.next()) - + let customHeaderBinaryValues = try XCTUnwrap(customMetadata[binaryValues: "custom-bin"]) var headerBinaryValuesIterator = customHeaderBinaryValues.makeIterator() - XCTAssertEqual(headerBinaryValuesIterator.next()!, [42,43,44]) + XCTAssertEqual(headerBinaryValuesIterator.next()!, [42, 43, 44]) XCTAssertNil(headerBinaryValuesIterator.next()) } - + func testReceiveInitialMetadataWhenClientOpenAndServerOpen() throws { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - + // Try opening server again - let action = try stateMachine.receive(metadata: [GRPCHTTP2Keys.encoding.rawValue: "deflate", "custom": "123"], endStream: false) + let action = try stateMachine.receive( + metadata: [GRPCHTTP2Keys.encoding.rawValue: "deflate", "custom": "123"], + endStream: false + ) guard case .receivedMetadata(let customMetadata) = action else { XCTFail("Expected action to be receivedMetadata") return } - + XCTAssertEqual(customMetadata.count, 1) let customHeaderStringValues = try XCTUnwrap(customMetadata[stringValues: "custom"]) var headerStringValuesIterator = customHeaderStringValues.makeIterator() XCTAssertEqual(headerStringValuesIterator.next(), "123") XCTAssertNil(headerStringValuesIterator.next()) } - + func testReceiveInitialMetadataWhenClientOpenAndServerClosed() { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - + // Close server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) - - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: .init(), endStream: false)) { error in + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(metadata: .init(), endStream: false) + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server is closed, nothing could have been sent.") } } - + func testReceiveInitialMetadataWhenClientClosedAndServerIdle() throws { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - + // Receive metadata = open server - let action = try stateMachine.receive(metadata: [GRPCHTTP2Keys.encoding.rawValue: "deflate", "custom": "123"], endStream: false) + let action = try stateMachine.receive( + metadata: [GRPCHTTP2Keys.encoding.rawValue: "deflate", "custom": "123"], + endStream: false + ) guard case .receivedMetadata(let customMetadata) = action else { XCTFail("Expected action to be receivedMetadata") return } - + XCTAssertEqual(customMetadata.count, 1) let customHeaderStringValues = try XCTUnwrap(customMetadata[stringValues: "custom"]) var headerStringValuesIterator = customHeaderStringValues.makeIterator() XCTAssertEqual(headerStringValuesIterator.next(), "123") XCTAssertNil(headerStringValuesIterator.next()) } - + func testReceiveInitialMetadataWhenClientClosedAndServerOpen() throws { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - + // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - + // Receive metadata = open server - let action = try stateMachine.receive(metadata: [GRPCHTTP2Keys.encoding.rawValue: "deflate", "custom": "123"], endStream: false) + let action = try stateMachine.receive( + metadata: [GRPCHTTP2Keys.encoding.rawValue: "deflate", "custom": "123"], + endStream: false + ) guard case .receivedMetadata(let customMetadata) = action else { XCTFail("Expected action to be receivedMetadata") return } - + XCTAssertEqual(customMetadata.count, 1) let customHeaderStringValues = try XCTUnwrap(customMetadata[stringValues: "custom"]) var headerStringValuesIterator = customHeaderStringValues.makeIterator() XCTAssertEqual(headerStringValuesIterator.next(), "123") XCTAssertNil(headerStringValuesIterator.next()) } - + func testReceiveInitialMetadataWhenClientClosedAndServerClosed() { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - + // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - + // Close server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) - + // This time receive metadata but with endStream = false. We should throw // here, since this would be invalid. - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: .init(), endStream: false)) { error in + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(metadata: .init(), endStream: false) + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server is closed, nothing could have been sent.") } } - + // - MARK: Receive end trailers - + func testReceiveEndTrailerWhenClientIdleAndServerIdle() { var stateMachine = makeClientStateMachine() - + // Receive an end trailer - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: .init(), endStream: true)) { error in + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(metadata: .init(), endStream: true) + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server cannot have sent metadata if the client is idle.") } } - + func testReceiveEndTrailerWhenClientOpenAndServerIdle() { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Receive a trailer-only response XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) } - + func testReceiveEndTrailerWhenClientOpenAndServerOpen() throws { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - + // Receive an end trailer - let action = try stateMachine.receive(metadata: [GRPCHTTP2Keys.encoding.rawValue: "deflate", "custom": "123"], endStream: true) + let action = try stateMachine.receive( + metadata: [GRPCHTTP2Keys.encoding.rawValue: "deflate", "custom": "123"], + endStream: true + ) guard case .receivedMetadata(let customMetadata) = action else { XCTFail("Expected action to be receivedMetadata") return } - + XCTAssertEqual(customMetadata.count, 1) let customHeaderStringValues = try XCTUnwrap(customMetadata[stringValues: "custom"]) var headerStringValuesIterator = customHeaderStringValues.makeIterator() XCTAssertEqual(headerStringValuesIterator.next(), "123") XCTAssertNil(headerStringValuesIterator.next()) } - + func testReceiveEndTrailerWhenClientOpenAndServerClosed() { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - + // Close server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) - + // Receive another end trailer - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: .init(), endStream: true)) { error in + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(metadata: .init(), endStream: true) + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server is closed, nothing could have been sent.") } } - + func testReceiveEndTrailerWhenClientClosedAndServerIdle() { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - + // Server sends a trailers-only response XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) } - + func testReceiveEndTrailerWhenClientClosedAndServerOpen() throws { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - + // Close the client stream XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - + // Closing the server now should not throw - let action = try stateMachine.receive(metadata: [GRPCHTTP2Keys.encoding.rawValue: "deflate", "custom": "123"], endStream: true) + let action = try stateMachine.receive( + metadata: [GRPCHTTP2Keys.encoding.rawValue: "deflate", "custom": "123"], + endStream: true + ) guard case .receivedMetadata(let customMetadata) = action else { XCTFail("Expected action to be receivedMetadata") return } - + XCTAssertEqual(customMetadata.count, 1) let customHeaderStringValues = try XCTUnwrap(customMetadata[stringValues: "custom"]) var headerStringValuesIterator = customHeaderStringValues.makeIterator() XCTAssertEqual(headerStringValuesIterator.next(), "123") XCTAssertNil(headerStringValuesIterator.next()) } - + func testReceiveEndTrailerWhenClientClosedAndServerClosed() { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - + // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - + // Close server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) - + // Close server again (endStream = true) and assert we don't throw. // This can happen if the previous close was caused by a grpc-status header // and then the server sends an empty frame with EOS set. XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) } - + // - MARK: Receive message - + func testReceiveMessageWhenClientIdleAndServerIdle() { var stateMachine = makeClientStateMachine() - - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(message: .init(), endStream: false)) { error in + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(message: .init(), endStream: false) + ) { error in XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Cannot have received anything from server if client is not yet open.") + XCTAssertEqual( + error.message, + "Cannot have received anything from server if client is not yet open." + ) } } - + func testReceiveMessageWhenClientOpenAndServerIdle() { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(message: .init(), endStream: false)) { error in + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(message: .init(), endStream: false) + ) { error in XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Server cannot have sent a message before sending the initial metadata.") + XCTAssertEqual( + error.message, + "Server cannot have sent a message before sending the initial metadata." + ) } } - + func testReceiveMessageWhenClientOpenAndServerOpen() throws { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: false)) XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) } - + func testReceiveMessageWhenClientOpenAndServerClosed() { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - + // Close server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) - - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(message: .init(), endStream: false)) { error in + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(message: .init(), endStream: false) + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Cannot have received anything from a closed server.") } } - + func testReceiveMessageWhenClientClosedAndServerIdle() { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(message: .init(), endStream: false)) { error in + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(message: .init(), endStream: false) + ) { error in XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Server cannot have sent a message before sending the initial metadata.") + XCTAssertEqual( + error.message, + "Server cannot have sent a message before sending the initial metadata." + ) } } - + func testReceiveMessageWhenClientClosedAndServerOpen() { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - + // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: false)) XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) } - + func testReceiveMessageWhenClientClosedAndServerClosed() { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - + // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - + // Close server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) - - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(message: .init(), endStream: false)) { error in + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(message: .init(), endStream: false) + ) { error in XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Shouldn't have received anything if both client and server are closed.") + XCTAssertEqual( + error.message, + "Shouldn't have received anything if both client and server are closed." + ) } } - + // - MARK: Next outbound message - + func testNextOutboundMessageWhenClientIdleAndServerIdle() { var stateMachine = makeClientStateMachine() - - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.nextOutboundMessage()) { error in + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.nextOutboundMessage() + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client is not open yet.") } } - + func testNextOutboundMessageWhenClientOpenAndServerIdle() throws { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + XCTAssertNil(try stateMachine.nextOutboundMessage()) - + XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) let request = try XCTUnwrap(stateMachine.nextOutboundMessage()) - + let expectedBytes: [UInt8] = [ - 0, // compression flag: unset - 0, 0, 0, 2, // message length: 2 bytes - 42, 42 // original message + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42, // original message ] XCTAssertEqual(Array(buffer: request), expectedBytes) } - + func testNextOutboundMessageWhenClientOpenAndServerIdle_WithCompression() throws { var stateMachine = makeClientStateMachineWithCompression() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + XCTAssertNil(try stateMachine.nextOutboundMessage()) - + let originalMessage = [UInt8]([42, 42, 43, 43]) XCTAssertNoThrow(try stateMachine.send(message: originalMessage, endStream: false)) let request = try XCTUnwrap(stateMachine.nextOutboundMessage()) - + var framer = GRPCMessageFramer() let compressor = Zlib.Compressor(method: .deflate) defer { compressor.end() } framer.append(originalMessage) - + let framedMessage = try XCTUnwrap(framer.next(compressor: compressor)) let expectedBytes = Array(buffer: framedMessage) XCTAssertEqual(Array(buffer: request), expectedBytes) } - + func testNextOutboundMessageWhenClientOpenAndServerOpen() throws { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - + XCTAssertNil(try stateMachine.nextOutboundMessage()) - + XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) let request = try XCTUnwrap(stateMachine.nextOutboundMessage()) - + let expectedBytes: [UInt8] = [ - 0, // compression flag: unset - 0, 0, 0, 2, // message length: 2 bytes - 42, 42 // original message + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42, // original message ] XCTAssertEqual(Array(buffer: request), expectedBytes) } - + func testNextOutboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { var stateMachine = makeClientStateMachineWithCompression() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - + XCTAssertNil(try stateMachine.nextOutboundMessage()) - + let originalMessage = [UInt8]([42, 42, 43, 43]) XCTAssertNoThrow(try stateMachine.send(message: originalMessage, endStream: false)) let request = try XCTUnwrap(stateMachine.nextOutboundMessage()) - + var framer = GRPCMessageFramer() let compressor = Zlib.Compressor(method: .deflate) defer { compressor.end() } framer.append(originalMessage) - + let framedMessage = try XCTUnwrap(framer.next(compressor: compressor)) let expectedBytes = Array(buffer: framedMessage) XCTAssertEqual(Array(buffer: request), expectedBytes) } - + func testNextOutboundMessageWhenClientOpenAndServerClosed() throws { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - + // Close server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) - + // No messages to send, so make sure nil is returned XCTAssertNil(try stateMachine.nextOutboundMessage()) - + // Queue a message, but assert the next outbound message is nil nevertheless, // because the server is closed. XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) @@ -908,231 +1017,233 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { func testNextOutboundMessageWhenClientClosedAndServerIdle() throws { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Send a message and close client XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: true)) - + // Make sure that getting the next outbound message _does_ return the message // we have enqueued. let request = try XCTUnwrap(stateMachine.nextOutboundMessage()) let expectedBytes: [UInt8] = [ - 0, // compression flag: unset - 0, 0, 0, 2, // message length: 2 bytes - 42, 42 // original message + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42, // original message ] XCTAssertEqual(Array(buffer: request), expectedBytes) - + // And then make sure that nothing else is returned anymore XCTAssertNil(try stateMachine.nextOutboundMessage()) } - + func testNextOutboundMessageWhenClientClosedAndServerOpen() throws { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - + // Send a message and close client XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: true)) - + // Make sure that getting the next outbound message _does_ return the message // we have enqueued. let request = try XCTUnwrap(stateMachine.nextOutboundMessage()) let expectedBytes: [UInt8] = [ - 0, // compression flag: unset - 0, 0, 0, 2, // message length: 2 bytes - 42, 42 // original message + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42, // original message ] XCTAssertEqual(Array(buffer: request), expectedBytes) - + // And then make sure that nothing else is returned anymore XCTAssertNil(try stateMachine.nextOutboundMessage()) } - + func testNextOutboundMessageWhenClientClosedAndServerClosed() { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - + // Send a message XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) - + // Close server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) - + // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - + // Even though we have enqueued a message, don't send it, because the server // is closed. XCTAssertNil(try stateMachine.nextOutboundMessage()) } - + // - MARK: Next inbound message - + func testNextInboundMessageWhenClientIdleAndServerIdle() { var stateMachine = makeClientStateMachine() XCTAssertNil(stateMachine.nextInboundMessage()) } - + func testNextInboundMessageWhenClientOpenAndServerIdle() { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + XCTAssertNil(stateMachine.nextInboundMessage()) } - + func testNextInboundMessageWhenClientOpenAndServerOpen() throws { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - + let receivedBytes = ByteBuffer(bytes: [ - 0, // compression flag: unset - 0, 0, 0, 2, // message length: 2 bytes - 42, 42 // original message + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42, // original message ]) try stateMachine.receive(message: receivedBytes, endStream: false) - + let receivedMessage = try XCTUnwrap(stateMachine.nextInboundMessage()) XCTAssertEqual(receivedMessage, [42, 42]) - + XCTAssertNil(stateMachine.nextInboundMessage()) } - + func testNextInboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { var stateMachine = makeClientStateMachineWithCompression() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Open server - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeadersWithDeflateCompression, endStream: false)) - + XCTAssertNoThrow( + try stateMachine.receive(metadata: .receivedHeadersWithDeflateCompression, endStream: false) + ) + let originalMessage = [UInt8]([42, 42, 43, 43]) var framer = GRPCMessageFramer() let compressor = Zlib.Compressor(method: .deflate) defer { compressor.end() } framer.append(originalMessage) let receivedBytes = try framer.next(compressor: compressor)! - + try stateMachine.receive(message: receivedBytes, endStream: false) - + let receivedMessage = try XCTUnwrap(stateMachine.nextInboundMessage()) XCTAssertEqual(receivedMessage, originalMessage) - + XCTAssertNil(stateMachine.nextInboundMessage()) } - + func testNextInboundMessageWhenClientOpenAndServerClosed() throws { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - + let receivedBytes = ByteBuffer(bytes: [ - 0, // compression flag: unset - 0, 0, 0, 2, // message length: 2 bytes - 42, 42 // original message + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42, // original message ]) try stateMachine.receive(message: receivedBytes, endStream: false) - + // Close server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) - + let receivedMessage = try XCTUnwrap(stateMachine.nextInboundMessage()) XCTAssertEqual(receivedMessage, [42, 42]) - + XCTAssertNil(stateMachine.nextInboundMessage()) } - + func testNextInboundMessageWhenClientClosedAndServerIdle() { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - + // If server is idle it means we never got any messages, assert no inbound // message is present. XCTAssertNil(stateMachine.nextInboundMessage()) } - + func testNextInboundMessageWhenClientClosedAndServerOpen() throws { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - + let receivedBytes = ByteBuffer(bytes: [ - 0, // compression flag: unset - 0, 0, 0, 2, // message length: 2 bytes - 42, 42 // original message + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42, // original message ]) try stateMachine.receive(message: receivedBytes, endStream: false) - + // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - + // Even though the client is closed, because it received a message while open, // we must get the message now. let receivedMessage = try XCTUnwrap(stateMachine.nextInboundMessage()) XCTAssertEqual(receivedMessage, [42, 42]) - + XCTAssertNil(stateMachine.nextInboundMessage()) } - + func testNextInboundMessageWhenClientClosedAndServerClosed() throws { var stateMachine = makeClientStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) - + // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - + let receivedBytes = ByteBuffer(bytes: [ - 0, // compression flag: unset - 0, 0, 0, 2, // message length: 2 bytes - 42, 42 // original message + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42, // original message ]) try stateMachine.receive(message: receivedBytes, endStream: false) - + // Close server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) - + // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - + // Even though the client is closed, because it received a message while open, // we must get the message now. let receivedMessage = try XCTUnwrap(stateMachine.nextInboundMessage()) XCTAssertEqual(receivedMessage, [42, 42]) - + XCTAssertNil(stateMachine.nextInboundMessage()) } } @@ -1140,408 +1251,488 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { extension HPACKHeaders { static let receivedHeaders: Self = [ GRPCHTTP2Keys.path.rawValue: "test/test", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc" + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", ] static let receivedHeadersWithDeflateCompression: Self = [ GRPCHTTP2Keys.path.rawValue: "test/test", GRPCHTTP2Keys.contentType.rawValue: "application/grpc", GRPCHTTP2Keys.encoding.rawValue: "deflate", - GRPCHTTP2Keys.acceptEncoding.rawValue: "deflate" + GRPCHTTP2Keys.acceptEncoding.rawValue: "deflate", ] static let receivedHeadersWithoutContentType: Self = [GRPCHTTP2Keys.path.rawValue: "test/test"] - static let receivedHeadersWithInvalidContentType: Self = [GRPCHTTP2Keys.contentType.rawValue: "invalid/invalid"] - static let receivedHeadersWithoutEndpoint: Self = [GRPCHTTP2Keys.contentType.rawValue: "application/grpc"] + static let receivedHeadersWithInvalidContentType: Self = [ + GRPCHTTP2Keys.contentType.rawValue: "invalid/invalid" + ] + static let receivedHeadersWithoutEndpoint: Self = [ + GRPCHTTP2Keys.contentType.rawValue: "application/grpc" + ] } final class GRPCStreamServerStateMachineTests: XCTestCase { private func makeServerStateMachine() -> GRPCStreamStateMachine { GRPCStreamStateMachine( - configuration: .server(.init( - scheme: .http, - acceptedEncodings: [] - )), + configuration: .server( + .init( + scheme: .http, + acceptedEncodings: [] + ) + ), maximumPayloadSize: 100, skipAssertions: true ) } - + private func makeServerStateMachineWithCompression() -> GRPCStreamStateMachine { GRPCStreamStateMachine( - configuration: .server(.init( - scheme: .http, - acceptedEncodings: [.deflate] - )), + configuration: .server( + .init( + scheme: .http, + acceptedEncodings: [.deflate] + ) + ), maximumPayloadSize: 100, skipAssertions: true ) } - + // - MARK: Send Metadata func testSendMetadataWhenClientIdleAndServerIdle() throws { var stateMachine = makeServerStateMachine() - - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(metadata: .init())) { error in + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send(metadata: .init()) + ) { error in XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Client cannot be idle if server is sending initial metadata: it must have opened.") + XCTAssertEqual( + error.message, + "Client cannot be idle if server is sending initial metadata: it must have opened." + ) } } - + func testSendMetadataWhenClientOpenAndServerIdle() throws { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) } - + func testSendMetadataWhenClientOpenAndServerOpen() throws { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - + // Try sending metadata again: should throw - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(metadata: .init())) { error in + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send(metadata: .init()) + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server has already sent initial metadata.") } } - + func testSendMetadataWhenClientOpenAndServerClosed() throws { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - + // Close server XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - + // Try sending metadata again: should throw XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server cannot send metadata if closed.") } } - + func testSendMetadataWhenClientClosedAndServerIdle() throws { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + // Close client XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) - + // We should be allowed to send initial metadata if client is closed: // client may be finished sending request but may still be awaiting response. XCTAssertNoThrow(try stateMachine.send(metadata: .init())) } - + func testSendMetadataWhenClientClosedAndServerOpen() throws { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - + // Close client XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) - + // Try sending metadata again: should throw XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server has already sent initial metadata.") } } - + func testSendMetadataWhenClientClosedAndServerClosed() throws { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - + // Close client XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) - + // Close server XCTAssertNoThrow(try stateMachine.send(message: .init(), endStream: true)) - + // Try sending metadata again: should throw XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server cannot send metadata if closed.") } } - + // - MARK: Send Message func testSendMessageWhenClientIdleAndServerIdle() { var stateMachine = makeServerStateMachine() - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(message: [], endStream: false)) { error in + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send(message: [], endStream: false) + ) { error in XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Server must have sent initial metadata before sending a message.") + XCTAssertEqual( + error.message, + "Server must have sent initial metadata before sending a message." + ) } } - + func testSendMessageWhenClientOpenAndServerIdle() { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + // Now send a message - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(message: [], endStream: false)) { error in + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send(message: [], endStream: false) + ) { error in XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Server must have sent initial metadata before sending a message.") + XCTAssertEqual( + error.message, + "Server must have sent initial metadata before sending a message." + ) } } - + func testSendMessageWhenClientOpenAndServerOpen() { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - + // Now send a message XCTAssertNoThrow(try stateMachine.send(message: [], endStream: false)) } - + func testSendMessageWhenClientOpenAndServerClosed() { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - + // Close server XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - + // Try sending another message: it should fail - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(message: [], endStream: false)) { error in + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send(message: [], endStream: false) + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send a message if it's closed.") } } - + func testSendMessageWhenClientClosedAndServerIdle() { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + // Close client XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) - - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(message: [], endStream: false)) { error in + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send(message: [], endStream: false) + ) { error in XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Server must have sent initial metadata before sending a message.") + XCTAssertEqual( + error.message, + "Server must have sent initial metadata before sending a message." + ) } } - + func testSendMessageWhenClientClosedAndServerOpen() { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - + // Close client XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) - + // Try sending a message: even though client is closed, we should send it // because it may be expecting a response. XCTAssertNoThrow(try stateMachine.send(message: [], endStream: false)) } - + func testSendMessageWhenClientClosedAndServerClosed() { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - + // Close client XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) - + // Close server XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - + // Try sending another message: it should fail - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(message: [], endStream: false)) { error in + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send(message: [], endStream: false) + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send a message if it's closed.") } } - + // - MARK: Send Status and Trailers - + func testSendStatusAndTrailersWhenClientIdleAndServerIdle() { var stateMachine = makeServerStateMachine() - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(status: .init(code: .ok, message: ""), metadata: .init(), trailersOnly: false)) { error in + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send( + status: .init(code: .ok, message: ""), + metadata: .init(), + trailersOnly: false + ) + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send status if idle.") } } - + func testSendStatusAndTrailersWhenClientOpenAndServerIdle() { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(status: .init(code: .ok, message: ""), metadata: .init(), trailersOnly: false)) { error in + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send( + status: .init(code: .ok, message: ""), + metadata: .init(), + trailersOnly: false + ) + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send status if idle.") } } - + func testSendStatusAndTrailersWhenClientOpenAndServerOpen() { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - - XCTAssertNoThrow(try stateMachine.send(status: .init(code: .ok, message: ""), metadata: .init(), trailersOnly: false)) - + + XCTAssertNoThrow( + try stateMachine.send( + status: .init(code: .ok, message: ""), + metadata: .init(), + trailersOnly: false + ) + ) + // Try sending another message: it should fail because server is now closed. - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(message: [], endStream: false)) { error in + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send(message: [], endStream: false) + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send a message if it's closed.") } } - + func testSendStatusAndTrailersWhenClientOpenAndServerClosed() { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - + // Close server XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(status: .init(code: .ok, message: ""), metadata: .init(), trailersOnly: false)) { error in + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send( + status: .init(code: .ok, message: ""), + metadata: .init(), + trailersOnly: false + ) + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send anything if closed.") } } - + func testSendStatusAndTrailersWhenClientClosedAndServerIdle() { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + // Close client XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) - - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(status: .init(code: .ok, message: ""), metadata: .init(), trailersOnly: false)) { error in + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send( + status: .init(code: .ok, message: ""), + metadata: .init(), + trailersOnly: false + ) + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send status if idle.") } } - + func testSendStatusAndTrailersWhenClientClosedAndServerOpen() { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - + // Close client XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) - + // Client is closed but may still be awaiting response, so we should be able to send it. - XCTAssertNoThrow(try stateMachine.send(status: .init(code: .ok, message: ""), metadata: .init(), trailersOnly: false)) + XCTAssertNoThrow( + try stateMachine.send( + status: .init(code: .ok, message: ""), + metadata: .init(), + trailersOnly: false + ) + ) } - + func testSendStatusAndTrailersWhenClientClosedAndServerClosed() { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - + // Close client XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) - + // Close server XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.send(status: .init(code: .ok, message: ""), metadata: .init(), trailersOnly: false)) { error in + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send( + status: .init(code: .ok, message: ""), + metadata: .init(), + trailersOnly: false + ) + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send anything if closed.") } } - + // - MARK: Receive metadata - + func testReceiveMetadataWhenClientIdleAndServerIdle() throws { var stateMachine = makeServerStateMachine() - + let action = try stateMachine.receive(metadata: .receivedHeaders, endStream: false) guard case .receivedMetadata(let metadata) = action else { XCTFail("Expected action to be doNothing") return } - + XCTAssertTrue(metadata.isEmpty) } - + func testReceiveMetadataWhenClientIdleAndServerIdle_WithEndStream() { var stateMachine = makeServerStateMachine() - + // If endStream is set, we should fail, because the client can only close by // sending a message with endStream set. If they send metadata it has to be // to open the stream (initial metadata). - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: .receivedHeaders, endStream: true)) { error in + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(metadata: .receivedHeaders, endStream: true) + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual( error.message, @@ -1552,12 +1743,15 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { ) } } - + func testReceiveMetadataWhenClientIdleAndServerIdle_MissingContentType() throws { var stateMachine = makeServerStateMachine() - - let action = try stateMachine.receive(metadata: .receivedHeadersWithoutContentType, endStream: false) - + + let action = try stateMachine.receive( + metadata: .receivedHeadersWithoutContentType, + endStream: false + ) + guard case .rejectRPC(let trailers) = action else { XCTFail("RPC should have been rejected.") return @@ -1566,12 +1760,15 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertEqual(trailers.count, 1) XCTAssertEqual(trailers.status, "415") } - + func testReceiveMetadataWhenClientIdleAndServerIdle_InvalidContentType() throws { var stateMachine = makeServerStateMachine() - - let action = try stateMachine.receive(metadata: .receivedHeadersWithInvalidContentType, endStream: false) - + + let action = try stateMachine.receive( + metadata: .receivedHeadersWithInvalidContentType, + endStream: false + ) + guard case .rejectRPC(let trailers) = action else { XCTFail("RPC should have been rejected.") return @@ -1580,12 +1777,15 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertEqual(trailers.count, 1) XCTAssertEqual(trailers.status, "415") } - + func testReceiveMetadataWhenClientIdleAndServerIdle_MissingPath() throws { var stateMachine = makeServerStateMachine() - - let action = try stateMachine.receive(metadata: .receivedHeadersWithoutEndpoint, endStream: false) - + + let action = try stateMachine.receive( + metadata: .receivedHeadersWithoutEndpoint, + endStream: false + ) + guard case .rejectRPC(let trailers) = action else { XCTFail("RPC should have been rejected.") return @@ -1595,352 +1795,390 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertEqual(trailers.grpcStatus, .unimplemented) XCTAssertEqual(trailers.grpcStatusMessage, "No :path header has been set.") } - + func testReceiveMetadataWhenClientIdleAndServerIdle_Encoding() { var noCompressionStateMachine = makeServerStateMachine() - + // Try opening client if no compression has been configured in the server: // should fail. - XCTAssertThrowsError(ofType: RPCError.self, - try noCompressionStateMachine.receive(metadata: .receivedHeadersWithDeflateCompression, endStream: false)) { error in + XCTAssertThrowsError( + ofType: RPCError.self, + try noCompressionStateMachine.receive( + metadata: .receivedHeadersWithDeflateCompression, + endStream: false + ) + ) { error in XCTAssertEqual(error.code, .unimplemented) XCTAssertEqual(error.message, "Compression is not supported") } - + var stateMachine = makeServerStateMachineWithCompression() //TODO: add tests for encoding validation } - + func testReceiveMetadataWhenClientOpenAndServerIdle() throws { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + // Try receiving initial metadata again - should fail - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) { error in + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(metadata: .receivedHeaders, endStream: false) + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client shouldn't have sent metadata twice.") } } - + func testReceiveMetadataWhenClientOpenAndServerOpen() throws { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) { error in + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(metadata: .receivedHeaders, endStream: false) + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client shouldn't have sent metadata twice.") } } - + func testReceiveMetadataWhenClientOpenAndServerClosed() { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - + // Close server XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) { error in + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(metadata: .receivedHeaders, endStream: false) + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client shouldn't have sent metadata twice.") } } - + func testReceiveMetadataWhenClientClosedAndServerIdle() { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + // Close client XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) - - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) { error in + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(metadata: .receivedHeaders, endStream: false) + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't have sent metadata if closed.") } } - + func testReceiveMetadataWhenClientClosedAndServerOpen() { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - + // Close client XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) - - - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) { error in + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(metadata: .receivedHeaders, endStream: false) + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't have sent metadata if closed.") } } - + func testReceiveMetadataWhenClientClosedAndServerClosed() { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - + // Close client XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) - + // Close server XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) { error in + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(metadata: .receivedHeaders, endStream: false) + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't have sent metadata if closed.") } } - + // - MARK: Receive message - + func testReceiveMessageWhenClientIdleAndServerIdle() { var stateMachine = makeServerStateMachine() - - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(message: .init(), endStream: false)) { error in + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(message: .init(), endStream: false) + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Can't have received a message if client is idle.") } } - + func testReceiveMessageWhenClientOpenAndServerIdle() { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + // Receive messages successfully: the second one should close client. XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: false)) XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) - + // Verify client is now closed - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(message: .init(), endStream: false)) { error in + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(message: .init(), endStream: false) + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't send a message if closed.") } } - + func testReceiveMessageWhenClientOpenAndServerOpen() throws { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - + // Receive messages successfully: the second one should close client. XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: false)) XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) - + // Verify client is now closed - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(message: .init(), endStream: false)) { error in + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(message: .init(), endStream: false) + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't send a message if closed.") } } - + func testReceiveMessageWhenClientOpenAndServerClosed() { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - + // Close server XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - + // Client is not done sending request, don't fail. XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: false)) } - + func testReceiveMessageWhenClientClosedAndServerIdle() { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + // Close client XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) - - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(message: .init(), endStream: false)) { error in + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(message: .init(), endStream: false) + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't send a message if closed.") } } - + func testReceiveMessageWhenClientClosedAndServerOpen() { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - + // Close client XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) - - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(message: .init(), endStream: false)) { error in + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(message: .init(), endStream: false) + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't send a message if closed.") } } - + func testReceiveMessageWhenClientClosedAndServerClosed() { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - + // Close client XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) - + // Close server XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.receive(message: .init(), endStream: false)) { error in + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(message: .init(), endStream: false) + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't send a message if closed.") } } - + // - MARK: Next outbound message - + func testNextOutboundMessageWhenClientIdleAndServerIdle() { var stateMachine = makeServerStateMachine() - - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.nextOutboundMessage()) { error in + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.nextOutboundMessage() + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server is not open yet.") } } - + func testNextOutboundMessageWhenClientOpenAndServerIdle() throws { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.nextOutboundMessage()) { error in + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.nextOutboundMessage() + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server is not open yet.") } } - + func testNextOutboundMessageWhenClientOpenAndServerIdle_WithCompression() throws { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.nextOutboundMessage()) { error in + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.nextOutboundMessage() + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server is not open yet.") } } - + func testNextOutboundMessageWhenClientOpenAndServerOpen() throws { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - + XCTAssertNil(try stateMachine.nextOutboundMessage()) - + XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) let request = try XCTUnwrap(stateMachine.nextOutboundMessage()) - + let expectedBytes: [UInt8] = [ - 0, // compression flag: unset - 0, 0, 0, 2, // message length: 2 bytes - 42, 42 // original message + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42, // original message ] XCTAssertEqual(Array(buffer: request), expectedBytes) } - + func testNextOutboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { var stateMachine = makeServerStateMachineWithCompression() - + // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeadersWithDeflateCompression, endStream: false)) - + XCTAssertNoThrow( + try stateMachine.receive(metadata: .receivedHeadersWithDeflateCompression, endStream: false) + ) + // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - + XCTAssertNil(try stateMachine.nextOutboundMessage()) - + let originalMessage = [UInt8]([42, 42, 43, 43]) XCTAssertNoThrow(try stateMachine.send(message: originalMessage, endStream: false)) let request = try XCTUnwrap(stateMachine.nextOutboundMessage()) - + var framer = GRPCMessageFramer() let compressor = Zlib.Compressor(method: .deflate) defer { compressor.end() } framer.append(originalMessage) - + let framedMessage = try XCTUnwrap(framer.next(compressor: compressor)) let expectedBytes = Array(buffer: framedMessage) XCTAssertEqual(Array(buffer: request), expectedBytes) } - + func testNextOutboundMessageWhenClientOpenAndServerClosed() throws { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - + // Close server XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.nextOutboundMessage()) { error in + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.nextOutboundMessage() + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Can't send response if server is closed.") } @@ -1948,236 +2186,242 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { func testNextOutboundMessageWhenClientClosedAndServerIdle() throws { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + // Close client XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) - - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.nextOutboundMessage()) { error in + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.nextOutboundMessage() + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server is not open yet.") } } - + func testNextOutboundMessageWhenClientClosedAndServerOpen() throws { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - + // Send a message XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) - + // Close client XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) - + // Send another message XCTAssertNoThrow(try stateMachine.send(message: [43, 43], endStream: false)) - + // Make sure that getting the next outbound message _does_ return the message // we have enqueued. let request = try XCTUnwrap(stateMachine.nextOutboundMessage()) let expectedBytes: [UInt8] = [ - 0, // compression flag: unset - 0, 0, 0, 2, // message length: 2 bytes - 42, 42, // original message + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42, // original message // End of first message - beginning of second - 0, // compression flag: unset - 0, 0, 0, 2, // message length: 2 bytes - 43, 43 // original message + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 43, 43, // original message ] XCTAssertEqual(Array(buffer: request), expectedBytes) // And then make sure that nothing else is returned anymore XCTAssertNil(try stateMachine.nextOutboundMessage()) } - + func testNextOutboundMessageWhenClientClosedAndServerClosed() { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - + // Send a message XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) - + // Close client XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) - + // Close server XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - + // Even though we have enqueued a message, don't send it, because the server // is closed. - XCTAssertThrowsError(ofType: RPCError.self, - try stateMachine.nextOutboundMessage()) { error in + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.nextOutboundMessage() + ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Can't send response if server is closed.") } } - + // - MARK: Next inbound message - + func testNextInboundMessageWhenClientIdleAndServerIdle() { var stateMachine = makeServerStateMachine() XCTAssertNil(stateMachine.nextInboundMessage()) } - + func testNextInboundMessageWhenClientOpenAndServerIdle() { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + XCTAssertNil(stateMachine.nextInboundMessage()) } - + func testNextInboundMessageWhenClientOpenAndServerOpen() throws { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - + let receivedBytes = ByteBuffer(bytes: [ - 0, // compression flag: unset - 0, 0, 0, 2, // message length: 2 bytes - 42, 42 // original message + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42, // original message ]) try stateMachine.receive(message: receivedBytes, endStream: false) - + let receivedMessage = try XCTUnwrap(stateMachine.nextInboundMessage()) XCTAssertEqual(receivedMessage, [42, 42]) - + XCTAssertNil(stateMachine.nextInboundMessage()) } - + func testNextInboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { var stateMachine = makeServerStateMachineWithCompression() - + // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeadersWithDeflateCompression, endStream: false)) - + XCTAssertNoThrow( + try stateMachine.receive(metadata: .receivedHeadersWithDeflateCompression, endStream: false) + ) + // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - + let originalMessage = [UInt8]([42, 42, 43, 43]) var framer = GRPCMessageFramer() let compressor = Zlib.Compressor(method: .deflate) defer { compressor.end() } framer.append(originalMessage) let receivedBytes = try framer.next(compressor: compressor)! - + try stateMachine.receive(message: receivedBytes, endStream: false) - + let receivedMessage = try XCTUnwrap(stateMachine.nextInboundMessage()) XCTAssertEqual(receivedMessage, originalMessage) - + XCTAssertNil(stateMachine.nextInboundMessage()) } - + func testNextInboundMessageWhenClientOpenAndServerClosed() throws { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - + let receivedBytes = ByteBuffer(bytes: [ - 0, // compression flag: unset - 0, 0, 0, 2, // message length: 2 bytes - 42, 42 // original message + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42, // original message ]) try stateMachine.receive(message: receivedBytes, endStream: false) - + // Close server XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - + let receivedMessage = try XCTUnwrap(stateMachine.nextInboundMessage()) XCTAssertEqual(receivedMessage, [42, 42]) - + XCTAssertNil(stateMachine.nextInboundMessage()) } - + func testNextInboundMessageWhenClientClosedAndServerIdle() { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + // Close client XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) - + XCTAssertNil(stateMachine.nextInboundMessage()) } - + func testNextInboundMessageWhenClientClosedAndServerOpen() throws { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - + let receivedBytes = ByteBuffer(bytes: [ - 0, // compression flag: unset - 0, 0, 0, 2, // message length: 2 bytes - 42, 42 // original message + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42, // original message ]) try stateMachine.receive(message: receivedBytes, endStream: false) - + // Close client XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) - + // Even though the client is closed, because the server received a message // while it was still open, we must get the message now. let receivedMessage = try XCTUnwrap(stateMachine.nextInboundMessage()) XCTAssertEqual(receivedMessage, [42, 42]) - + XCTAssertNil(stateMachine.nextInboundMessage()) } - + func testNextInboundMessageWhenClientClosedAndServerClosed() throws { var stateMachine = makeServerStateMachine() - + // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - + let receivedBytes = ByteBuffer(bytes: [ - 0, // compression flag: unset - 0, 0, 0, 2, // message length: 2 bytes - 42, 42 // original message + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42, // original message ]) try stateMachine.receive(message: receivedBytes, endStream: false) - + // Close server XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - + // Close client XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) - + // Even though the client and server are closed, because the server received // a message while the client was still open, we must get the message now. let receivedMessage = try XCTUnwrap(stateMachine.nextInboundMessage()) XCTAssertEqual(receivedMessage, [42, 42]) - + XCTAssertNil(stateMachine.nextInboundMessage()) } From c9d1eca69950703f789afc95a360a612846af818 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Fri, 16 Feb 2024 17:08:07 +0000 Subject: [PATCH 16/51] PR changes --- .../GRPCStreamStateMachine.swift | 104 ++++++++- .../GRPCStreamStateMachineTests.swift | 218 +++++++++++++----- 2 files changed, 256 insertions(+), 66 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index ea3ba3e06..bf25bc0a3 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -17,10 +17,16 @@ import GRPCCore import NIOCore import NIOHPACK +import NIOHTTP1 @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) enum OnMetadataReceived { case receivedMetadata(Metadata) + + // Client-specific actions + case failedRequest(Status) + case doNothing + // Server-specific actions case rejectRPC(trailers: HPACKHeaders) } @@ -496,6 +502,41 @@ extension GRPCStreamStateMachine { ) throws -> OnMetadataReceived { switch self.state { case .clientOpenServerIdle(let state): + guard metadata.grpcStatus != nil || metadata.status == "200" else { + let httpStatusCode = metadata.status + .flatMap { Int($0) } + .map { HTTPResponseStatus(statusCode: $0) } + + guard let httpStatusCode else { + return .failedRequest( + .init(code: .unknown, message: "Unexpected non-200 HTTP Status Code.") + ) + } + + if (100 ... 199).contains(httpStatusCode.code) { + // For 1xx status codes, the entire header should be skipped and a + // subsequent header should be read. + // See https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md + return .doNothing + } + + // Close the client and forward the mapped status code. + self.state = .clientClosedServerIdle(.init(previousState: state)) + return .failedRequest( + .init( + code: Status.Code(httpStatusCode: httpStatusCode), + message: "Unexpected non-200 HTTP Status Code." + ) + ) + } + + let contentTypeHeader = metadata.first(name: GRPCHTTP2Keys.contentType.rawValue) + guard contentTypeHeader.flatMap(ContentType.init) != nil else { + return .failedRequest( + .init(code: .internalError, message: "Missing \(GRPCHTTP2Keys.contentType) header") + ) + } + if endStream { // This is a trailers-only response: close server. self.state = .clientOpenServerClosed(.init(previousState: state)) @@ -522,6 +563,40 @@ extension GRPCStreamStateMachine { } return .receivedMetadata(Metadata(headers: metadata)) case .clientClosedServerIdle(let state): + guard metadata.grpcStatus != nil || metadata.status == "200" else { + let httpStatusCode = metadata.status + .flatMap { Int($0) } + .map { HTTPResponseStatus(statusCode: $0) } + + guard let httpStatusCode else { + return .failedRequest( + .init(code: .unknown, message: "Unexpected non-200 HTTP Status Code.") + ) + } + + if (100 ... 199).contains(httpStatusCode.code) { + // For 1xx status codes, the entire header should be skipped and a + // subsequent header should be read. + // See https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md + return .doNothing + } + + // Forward the mapped status code. + return .failedRequest( + .init( + code: Status.Code(httpStatusCode: httpStatusCode), + message: "Unexpected non-200 HTTP Status Code." + ) + ) + } + + let contentTypeHeader = metadata.first(name: GRPCHTTP2Keys.contentType.rawValue) + guard contentTypeHeader.flatMap(ContentType.init) != nil else { + return .failedRequest( + .init(code: .internalError, message: "Missing \(GRPCHTTP2Keys.contentType) header") + ) + } + if endStream { // This is a trailers-only response. self.state = .clientClosedServerClosed(.init(previousState: state)) @@ -823,7 +898,7 @@ extension GRPCStreamStateMachine { var trailers = HPACKHeaders() trailers.reserveCapacity(2) trailers.grpcStatus = .unimplemented - trailers.grpcStatusMessage = "No \(GRPCHTTP2Keys.path.rawValue) header has been set." + trailers.grpcStatusMessage = "No \(GRPCHTTP2Keys.path) header has been set." return .rejectRPC(trailers: trailers) } @@ -1012,7 +1087,7 @@ extension MethodDescriptor { } } -internal enum GRPCHTTP2Keys: String, CaseIterable { +internal enum GRPCHTTP2Keys: String { case path = ":path" case contentType = "content-type" case encoding = "grpc-encoding" @@ -1202,10 +1277,7 @@ extension Zlib.Method { extension Metadata { init(headers: HPACKHeaders) { var metadata = Metadata() - // TODO: since this is what we'll pass on to the user, I was wondering if it would be useful - // to filter out the headers that relate to the protocol, and just leave the user-defined ones. - for header in headers - where !GRPCHTTP2Keys.allCases.contains(where: { $0.rawValue == header.name }) { + for header in headers { if header.name.hasSuffix("-bin") { do { let decodedBinary = try header.value.base64Decoded() @@ -1220,3 +1292,23 @@ extension Metadata { self = metadata } } + +extension Status.Code { + // See https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md + init(httpStatusCode: HTTPResponseStatus) { + switch httpStatusCode { + case .badRequest: + self = .internalError + case .unauthorized: + self = .unauthenticated + case .forbidden: + self = .permissionDenied + case .notFound: + self = .unimplemented + case .tooManyRequests, .badGateway, .serviceUnavailable, .gatewayTimeout: + self = .unavailable + default: + self = .unknown + } + } +} diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index 53c9525f1..d935048c6 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -473,26 +473,27 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Receive metadata = open server let action = try stateMachine.receive( metadata: [ - GRPCHTTP2Keys.encoding.rawValue: "deflate", "custom": "123", + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, + GRPCHTTP2Keys.encoding.rawValue: "deflate", + "custom": "123", "custom-bin": String(base64Encoding: [42, 43, 44]), ], endStream: false ) guard case .receivedMetadata(let customMetadata) = action else { - XCTFail("Expected action to be receivedMetadata") + XCTFail("Expected action to be receivedMetadata but was \(action)") return } - XCTAssertEqual(customMetadata.count, 2) - let customHeaderStringValues = try XCTUnwrap(customMetadata[stringValues: "custom"]) - var headerStringValuesIterator = customHeaderStringValues.makeIterator() - XCTAssertEqual(headerStringValuesIterator.next(), "123") - XCTAssertNil(headerStringValuesIterator.next()) - - let customHeaderBinaryValues = try XCTUnwrap(customMetadata[binaryValues: "custom-bin"]) - var headerBinaryValuesIterator = customHeaderBinaryValues.makeIterator() - XCTAssertEqual(headerBinaryValuesIterator.next()!, [42, 43, 44]) - XCTAssertNil(headerBinaryValuesIterator.next()) + var expectedMetadata: Metadata = [ + ":status": "200", + "content-type": "application/grpc", + "grpc-encoding": "deflate", + "custom": "123", + ] + expectedMetadata.addBinary([42, 43, 44], forKey: "custom-bin") + XCTAssertEqual(customMetadata, expectedMetadata) } func testReceiveInitialMetadataWhenClientOpenAndServerOpen() throws { @@ -506,19 +507,26 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Try opening server again let action = try stateMachine.receive( - metadata: [GRPCHTTP2Keys.encoding.rawValue: "deflate", "custom": "123"], + metadata: [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, + GRPCHTTP2Keys.encoding.rawValue: "deflate", + "custom": "123", + ], endStream: false ) guard case .receivedMetadata(let customMetadata) = action else { - XCTFail("Expected action to be receivedMetadata") + XCTFail("Expected action to be receivedMetadata but was \(action)") return } - XCTAssertEqual(customMetadata.count, 1) - let customHeaderStringValues = try XCTUnwrap(customMetadata[stringValues: "custom"]) - var headerStringValuesIterator = customHeaderStringValues.makeIterator() - XCTAssertEqual(headerStringValuesIterator.next(), "123") - XCTAssertNil(headerStringValuesIterator.next()) + let expectedMetadata: Metadata = [ + ":status": "200", + "content-type": "application/grpc", + "grpc-encoding": "deflate", + "custom": "123", + ] + XCTAssertEqual(customMetadata, expectedMetadata) } func testReceiveInitialMetadataWhenClientOpenAndServerClosed() { @@ -528,7 +536,11 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + let headers: HPACKHeaders = [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, + ] + XCTAssertNoThrow(try stateMachine.receive(metadata: headers, endStream: false)) // Close server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) @@ -553,19 +565,26 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Receive metadata = open server let action = try stateMachine.receive( - metadata: [GRPCHTTP2Keys.encoding.rawValue: "deflate", "custom": "123"], + metadata: [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, + GRPCHTTP2Keys.encoding.rawValue: "deflate", + "custom": "123", + ], endStream: false ) guard case .receivedMetadata(let customMetadata) = action else { - XCTFail("Expected action to be receivedMetadata") + XCTFail("Expected action to be receivedMetadata but was \(action)") return } - XCTAssertEqual(customMetadata.count, 1) - let customHeaderStringValues = try XCTUnwrap(customMetadata[stringValues: "custom"]) - var headerStringValuesIterator = customHeaderStringValues.makeIterator() - XCTAssertEqual(headerStringValuesIterator.next(), "123") - XCTAssertNil(headerStringValuesIterator.next()) + let expectedMetadata: Metadata = [ + ":status": "200", + "content-type": "application/grpc", + "grpc-encoding": "deflate", + "custom": "123", + ] + XCTAssertEqual(customMetadata, expectedMetadata) } func testReceiveInitialMetadataWhenClientClosedAndServerOpen() throws { @@ -582,19 +601,26 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Receive metadata = open server let action = try stateMachine.receive( - metadata: [GRPCHTTP2Keys.encoding.rawValue: "deflate", "custom": "123"], + metadata: [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, + GRPCHTTP2Keys.encoding.rawValue: "deflate", + "custom": "123", + ], endStream: false ) guard case .receivedMetadata(let customMetadata) = action else { - XCTFail("Expected action to be receivedMetadata") + XCTFail("Expected action to be receivedMetadata but was \(action)") return } - XCTAssertEqual(customMetadata.count, 1) - let customHeaderStringValues = try XCTUnwrap(customMetadata[stringValues: "custom"]) - var headerStringValuesIterator = customHeaderStringValues.makeIterator() - XCTAssertEqual(headerStringValuesIterator.next(), "123") - XCTAssertNil(headerStringValuesIterator.next()) + let expectedMetadata: Metadata = [ + ":status": "200", + "content-type": "application/grpc", + "grpc-encoding": "deflate", + "custom": "123", + ] + XCTAssertEqual(customMetadata, expectedMetadata) } func testReceiveInitialMetadataWhenClientClosedAndServerClosed() { @@ -604,7 +630,11 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + let headers: HPACKHeaders = [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, + ] + XCTAssertNoThrow(try stateMachine.receive(metadata: headers, endStream: false)) // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) @@ -659,19 +689,26 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Receive an end trailer let action = try stateMachine.receive( - metadata: [GRPCHTTP2Keys.encoding.rawValue: "deflate", "custom": "123"], + metadata: [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, + GRPCHTTP2Keys.encoding.rawValue: "deflate", + "custom": "123", + ], endStream: true ) guard case .receivedMetadata(let customMetadata) = action else { - XCTFail("Expected action to be receivedMetadata") + XCTFail("Expected action to be receivedMetadata but was \(action)") return } - XCTAssertEqual(customMetadata.count, 1) - let customHeaderStringValues = try XCTUnwrap(customMetadata[stringValues: "custom"]) - var headerStringValuesIterator = customHeaderStringValues.makeIterator() - XCTAssertEqual(headerStringValuesIterator.next(), "123") - XCTAssertNil(headerStringValuesIterator.next()) + let expectedMetadata: Metadata = [ + ":status": "200", + "content-type": "application/grpc", + "grpc-encoding": "deflate", + "custom": "123", + ] + XCTAssertEqual(customMetadata, expectedMetadata) } func testReceiveEndTrailerWhenClientOpenAndServerClosed() { @@ -681,7 +718,11 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + let headers: HPACKHeaders = [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, + ] + XCTAssertNoThrow(try stateMachine.receive(metadata: headers, endStream: false)) // Close server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) @@ -723,19 +764,26 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Closing the server now should not throw let action = try stateMachine.receive( - metadata: [GRPCHTTP2Keys.encoding.rawValue: "deflate", "custom": "123"], + metadata: [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, + GRPCHTTP2Keys.encoding.rawValue: "deflate", + "custom": "123", + ], endStream: true ) guard case .receivedMetadata(let customMetadata) = action else { - XCTFail("Expected action to be receivedMetadata") + XCTFail("Expected action to be receivedMetadata but was \(action)") return } - XCTAssertEqual(customMetadata.count, 1) - let customHeaderStringValues = try XCTUnwrap(customMetadata[stringValues: "custom"]) - var headerStringValuesIterator = customHeaderStringValues.makeIterator() - XCTAssertEqual(headerStringValuesIterator.next(), "123") - XCTAssertNil(headerStringValuesIterator.next()) + let expectedMetadata: Metadata = [ + ":status": "200", + "content-type": "application/grpc", + "grpc-encoding": "deflate", + "custom": "123", + ] + XCTAssertEqual(customMetadata, expectedMetadata) } func testReceiveEndTrailerWhenClientClosedAndServerClosed() { @@ -801,7 +849,11 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + let headers: HPACKHeaders = [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, + ] + XCTAssertNoThrow(try stateMachine.receive(metadata: headers, endStream: false)) XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: false)) XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) @@ -814,7 +866,11 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + let headers: HPACKHeaders = [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, + ] + XCTAssertNoThrow(try stateMachine.receive(metadata: headers, endStream: false)) // Close server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) @@ -856,7 +912,11 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + let headers: HPACKHeaders = [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, + ] + XCTAssertNoThrow(try stateMachine.receive(metadata: headers, endStream: false)) // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) @@ -872,7 +932,11 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + let headers: HPACKHeaders = [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, + ] + XCTAssertNoThrow(try stateMachine.receive(metadata: headers, endStream: false)) // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) @@ -923,6 +987,9 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { 42, 42, // original message ] XCTAssertEqual(Array(buffer: request), expectedBytes) + + // And then make sure that nothing else is returned anymore + XCTAssertNil(try stateMachine.nextOutboundMessage()) } func testNextOutboundMessageWhenClientOpenAndServerIdle_WithCompression() throws { @@ -967,6 +1034,9 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { 42, 42, // original message ] XCTAssertEqual(Array(buffer: request), expectedBytes) + + // And then make sure that nothing else is returned anymore + XCTAssertNil(try stateMachine.nextOutboundMessage()) } func testNextOutboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { @@ -1001,7 +1071,11 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + let headers: HPACKHeaders = [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, + ] + XCTAssertNoThrow(try stateMachine.receive(metadata: headers, endStream: false)) // Close server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) @@ -1071,7 +1145,11 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + let headers: HPACKHeaders = [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, + ] + XCTAssertNoThrow(try stateMachine.receive(metadata: headers, endStream: false)) // Send a message XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) @@ -1110,7 +1188,11 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + let headers: HPACKHeaders = [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, + ] + XCTAssertNoThrow(try stateMachine.receive(metadata: headers, endStream: false)) let receivedBytes = ByteBuffer(bytes: [ 0, // compression flag: unset @@ -1158,7 +1240,11 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + let headers: HPACKHeaders = [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, + ] + XCTAssertNoThrow(try stateMachine.receive(metadata: headers, endStream: false)) let receivedBytes = ByteBuffer(bytes: [ 0, // compression flag: unset @@ -1197,7 +1283,11 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + let headers: HPACKHeaders = [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, + ] + XCTAssertNoThrow(try stateMachine.receive(metadata: headers, endStream: false)) let receivedBytes = ByteBuffer(bytes: [ 0, // compression flag: unset @@ -1224,7 +1314,11 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + let headers: HPACKHeaders = [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, + ] + XCTAssertNoThrow(try stateMachine.receive(metadata: headers, endStream: false)) let receivedBytes = ByteBuffer(bytes: [ 0, // compression flag: unset @@ -1254,6 +1348,7 @@ extension HPACKHeaders { GRPCHTTP2Keys.contentType.rawValue: "application/grpc", ] static let receivedHeadersWithDeflateCompression: Self = [ + GRPCHTTP2Keys.status.rawValue: "200", GRPCHTTP2Keys.path.rawValue: "test/test", GRPCHTTP2Keys.contentType.rawValue: "application/grpc", GRPCHTTP2Keys.encoding.rawValue: "deflate", @@ -2134,6 +2229,9 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { 42, 42, // original message ] XCTAssertEqual(Array(buffer: request), expectedBytes) + + // And then make sure that nothing else is returned anymore + XCTAssertNil(try stateMachine.nextOutboundMessage()) } func testNextOutboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { From cf325f5d7d2360a1f7c9d9b6b9757dd76cbe25df Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Mon, 19 Feb 2024 10:30:29 +0000 Subject: [PATCH 17/51] Refactor client tests --- .../GRPCStreamStateMachine.swift | 6 +- .../GRPCStreamStateMachineTests.swift | 1314 ++++------------- 2 files changed, 306 insertions(+), 1014 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index bf25bc0a3..4c9c9c89c 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -679,14 +679,10 @@ extension GRPCStreamStateMachine { } else { self.state = .clientClosedServerOpen(state) } - case .clientOpenServerClosed: + case .clientOpenServerClosed, .clientClosedServerClosed: throw self.assertionFailureAndCreateRPCErrorOnInternalError( "Cannot have received anything from a closed server." ) - case .clientClosedServerClosed: - throw self.assertionFailureAndCreateRPCErrorOnInternalError( - "Shouldn't have received anything if both client and server are closed." - ) } } diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index d935048c6..c11f88be8 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -22,152 +22,110 @@ import XCTest @testable import GRPCHTTP2Core final class GRPCStreamClientStateMachineTests: XCTestCase { - private func makeClientStateMachine() -> GRPCStreamStateMachine { - GRPCStreamStateMachine( - configuration: .client( - .init( - methodDescriptor: .init(service: "test", method: "test"), - scheme: .http, - outboundEncoding: nil, - acceptedEncodings: [.deflate] - ) - ), - maximumPayloadSize: 100, - skipAssertions: true - ) - } - - private func makeClientStateMachineWithCompression() -> GRPCStreamStateMachine { - GRPCStreamStateMachine( + enum TargetStateMachineState: CaseIterable { + case clientIdleServerIdle + case clientOpenServerIdle + case clientOpenServerOpen + case clientOpenServerClosed + case clientClosedServerIdle + case clientClosedServerOpen + case clientClosedServerClosed + } + + private func makeClientStateMachine(targetState: TargetStateMachineState, compressionEnabled: Bool = false) -> GRPCStreamStateMachine { + var stateMachine = GRPCStreamStateMachine( configuration: .client( .init( methodDescriptor: .init(service: "test", method: "test"), scheme: .http, - outboundEncoding: .deflate, + outboundEncoding: compressionEnabled ? .deflate : nil, acceptedEncodings: [.deflate] ) ), maximumPayloadSize: 100, skipAssertions: true ) + + let serverMetadata: HPACKHeaders = compressionEnabled ? .serverInitialMetadataWithDeflateCompression : .serverInitialMetadata + switch targetState { + case .clientIdleServerIdle: + break + case .clientOpenServerIdle: + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: [])) + case .clientOpenServerOpen: + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: [])) + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: serverMetadata, endStream: false)) + case .clientOpenServerClosed: + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: [])) + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: serverMetadata, endStream: false)) + // Close server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) + case .clientClosedServerIdle: + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: [])) + // Close client + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + case .clientClosedServerOpen: + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: [])) + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: serverMetadata, endStream: false)) + // Close client + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + case .clientClosedServerClosed: + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: [])) + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: serverMetadata, endStream: false)) + // Close client + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + // Close server + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) + } + + return stateMachine } // - MARK: Send Metadata func testSendMetadataWhenClientIdleAndServerIdle() throws { - var stateMachine = makeClientStateMachine() - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - } - - func testSendMetadataWhenClientOpenAndServerIdle() throws { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Try sending metadata again: should throw - XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Client is already open: shouldn't be sending metadata.") - } - } - - func testSendMetadataWhenClientOpenAndServerOpen() throws { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Open server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - - // Try sending metadata again: should throw - XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Client is already open: shouldn't be sending metadata.") - } - } - - func testSendMetadataWhenClientOpenAndServerClosed() throws { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Open server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - - // Close server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) - - // Try sending metadata again: should throw - XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Client is already open: shouldn't be sending metadata.") - } - } - - func testSendMetadataWhenClientClosedAndServerIdle() throws { - var stateMachine = makeClientStateMachine() - - // Open client + var stateMachine = self.makeClientStateMachine(targetState: .clientIdleServerIdle) XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Close client - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - - // Try sending metadata again: should throw - XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Client is closed: can't send metadata.") - } } - func testSendMetadataWhenClientClosedAndServerOpen() throws { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Open server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - - // Close client - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + func testSendMetadataWhenClientAlreadyOpen() throws { + for targetState in [TargetStateMachineState.clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed] { + var stateMachine = self.makeClientStateMachine(targetState: targetState) - // Try sending metadata again: should throw - XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Client is closed: can't send metadata.") + // Try sending metadata again: should throw + XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Client is already open: shouldn't be sending metadata.") + } } } - func testSendMetadataWhenClientClosedAndServerClosed() throws { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Open server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - - // Close client - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - - // Close server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) - - // Try sending metadata again: should throw - XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Client is closed: can't send metadata.") + func testSendMetadataWhenClientAlreadyClosed() throws { + for targetState in [TargetStateMachineState.clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed] { + var stateMachine = self.makeClientStateMachine(targetState: targetState) + + // Try sending metadata again: should throw + XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Client is closed: can't send metadata.") + } } } // - MARK: Send Message func testSendMessageWhenClientIdleAndServerIdle() { - var stateMachine = makeClientStateMachine() + var stateMachine = self.makeClientStateMachine(targetState: .clientIdleServerIdle) // Try to send a message without opening (i.e. without sending initial metadata) XCTAssertThrowsError( @@ -179,770 +137,195 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { } } - func testSendMessageWhenClientOpenAndServerIdle() { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Now send a message - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: false)) - } - - func testSendMessageWhenClientOpenAndServerOpen() { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Open server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - - // Now send a message - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: false)) - } - - func testSendMessageWhenClientOpenAndServerClosed() { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Open server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - - // Close server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) - - // Now send a message - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: false)) - } - - func testSendMessageWhenClientClosedAndServerIdle() { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Close client - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - - // Try sending another message: it should fail - XCTAssertThrowsError( - ofType: RPCError.self, - try stateMachine.send(message: [], endStream: false) - ) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Client is closed, cannot send a message.") - } - } - - func testSendMessageWhenClientClosedAndServerOpen() { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Open server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - - // Close client - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - - // Try sending another message: it should fail - XCTAssertThrowsError( - ofType: RPCError.self, - try stateMachine.send(message: [], endStream: false) - ) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Client is closed, cannot send a message.") - } - } - - func testSendMessageWhenClientClosedAndServerClosed() { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Open server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - - // Close client - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - - // Close server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) - - // Try sending another message: it should fail - XCTAssertThrowsError( - ofType: RPCError.self, - try stateMachine.send(message: [], endStream: false) - ) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Client is closed, cannot send a message.") - } - } - - // - MARK: Send Status and Trailers - - func testSendStatusAndTrailersWhenClientIdleAndServerIdle() { - var stateMachine = makeClientStateMachine() - - // This operation is never allowed on the client. - XCTAssertThrowsError( - ofType: RPCError.self, - try stateMachine.send( - status: Status(code: .ok, message: ""), - metadata: .init(), - trailersOnly: false - ) - ) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Client cannot send status and trailer.") - } - } - - func testSendStatusAndTrailersWhenClientOpenAndServerIdle() { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // This operation is never allowed on the client. - XCTAssertThrowsError( - ofType: RPCError.self, - try stateMachine.send( - status: Status(code: .ok, message: ""), - metadata: .init(), - trailersOnly: false - ) - ) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Client cannot send status and trailer.") - } - } - - func testSendStatusAndTrailersWhenClientOpenAndServerOpen() { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Open server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - - // This operation is never allowed on the client. - XCTAssertThrowsError( - ofType: RPCError.self, - try stateMachine.send( - status: Status(code: .ok, message: ""), - metadata: .init(), - trailersOnly: false - ) - ) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Client cannot send status and trailer.") + func testSendMessageWhenClientOpen() { + for targetState in [TargetStateMachineState.clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed] { + var stateMachine = self.makeClientStateMachine(targetState: targetState) + + // Now send a message + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: false)) } } - func testSendStatusAndTrailersWhenClientOpenAndServerClosed() { - var stateMachine = makeClientStateMachine() - - // Open stream - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Open server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - - // Close server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) - - // This operation is never allowed on the client. - XCTAssertThrowsError( - ofType: RPCError.self, - try stateMachine.send( - status: Status(code: .ok, message: ""), - metadata: .init(), - trailersOnly: false - ) - ) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Client cannot send status and trailer.") + func testSendMessageWhenClientClosed() { + for targetState in [TargetStateMachineState.clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed] { + var stateMachine = self.makeClientStateMachine(targetState: targetState) + + // Try sending another message: it should fail + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send(message: [], endStream: false) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Client is closed, cannot send a message.") + } } - } - - func testSendStatusAndTrailersWhenClientClosedAndServerIdle() { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Close client - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - - // This operation is never allowed on the client. - XCTAssertThrowsError( - ofType: RPCError.self, - try stateMachine.send( - status: Status(code: .ok, message: ""), - metadata: .init(), - trailersOnly: false - ) - ) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Client cannot send status and trailer.") - } - } - - func testSendStatusAndTrailersWhenClientClosedAndServerOpen() { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Open server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - - // Close client - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - - // This operation is never allowed on the client. - XCTAssertThrowsError( - ofType: RPCError.self, - try stateMachine.send( - status: Status(code: .ok, message: ""), - metadata: .init(), - trailersOnly: false - ) - ) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Client cannot send status and trailer.") - } - } - - func testSendStatusAndTrailersWhenClientClosedAndServerClosed() { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Open server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - - // Close client - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - - // Close server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) - - // This operation is never allowed on the client. - XCTAssertThrowsError( - ofType: RPCError.self, - try stateMachine.send( - status: Status(code: .ok, message: ""), - metadata: .init(), - trailersOnly: false - ) - ) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Client cannot send status and trailer.") - } - } - - // - MARK: Receive initial metadata - - func testReceiveInitialMetadataWhenClientIdleAndServerIdle() { - var stateMachine = makeClientStateMachine() - - XCTAssertThrowsError( - ofType: RPCError.self, - try stateMachine.receive(metadata: .init(), endStream: false) - ) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Server cannot have sent metadata if the client is idle.") - } - } - - func testReceiveInitialMetadataWhenClientOpenAndServerIdle() throws { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Receive metadata = open server - let action = try stateMachine.receive( - metadata: [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, - GRPCHTTP2Keys.encoding.rawValue: "deflate", - "custom": "123", - "custom-bin": String(base64Encoding: [42, 43, 44]), - ], - endStream: false - ) - guard case .receivedMetadata(let customMetadata) = action else { - XCTFail("Expected action to be receivedMetadata but was \(action)") - return - } - - var expectedMetadata: Metadata = [ - ":status": "200", - "content-type": "application/grpc", - "grpc-encoding": "deflate", - "custom": "123", - ] - expectedMetadata.addBinary([42, 43, 44], forKey: "custom-bin") - XCTAssertEqual(customMetadata, expectedMetadata) - } - - func testReceiveInitialMetadataWhenClientOpenAndServerOpen() throws { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Open server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - - // Try opening server again - let action = try stateMachine.receive( - metadata: [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, - GRPCHTTP2Keys.encoding.rawValue: "deflate", - "custom": "123", - ], - endStream: false - ) - guard case .receivedMetadata(let customMetadata) = action else { - XCTFail("Expected action to be receivedMetadata but was \(action)") - return - } - - let expectedMetadata: Metadata = [ - ":status": "200", - "content-type": "application/grpc", - "grpc-encoding": "deflate", - "custom": "123", - ] - XCTAssertEqual(customMetadata, expectedMetadata) - } - - func testReceiveInitialMetadataWhenClientOpenAndServerClosed() { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Open server - let headers: HPACKHeaders = [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, - ] - XCTAssertNoThrow(try stateMachine.receive(metadata: headers, endStream: false)) - - // Close server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) - - XCTAssertThrowsError( - ofType: RPCError.self, - try stateMachine.receive(metadata: .init(), endStream: false) - ) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Server is closed, nothing could have been sent.") - } - } - - func testReceiveInitialMetadataWhenClientClosedAndServerIdle() throws { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Close client - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - - // Receive metadata = open server - let action = try stateMachine.receive( - metadata: [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, - GRPCHTTP2Keys.encoding.rawValue: "deflate", - "custom": "123", - ], - endStream: false - ) - guard case .receivedMetadata(let customMetadata) = action else { - XCTFail("Expected action to be receivedMetadata but was \(action)") - return - } - - let expectedMetadata: Metadata = [ - ":status": "200", - "content-type": "application/grpc", - "grpc-encoding": "deflate", - "custom": "123", - ] - XCTAssertEqual(customMetadata, expectedMetadata) - } - - func testReceiveInitialMetadataWhenClientClosedAndServerOpen() throws { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Open server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - - // Close client - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - - // Receive metadata = open server - let action = try stateMachine.receive( - metadata: [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, - GRPCHTTP2Keys.encoding.rawValue: "deflate", - "custom": "123", - ], - endStream: false - ) - guard case .receivedMetadata(let customMetadata) = action else { - XCTFail("Expected action to be receivedMetadata but was \(action)") - return - } - - let expectedMetadata: Metadata = [ - ":status": "200", - "content-type": "application/grpc", - "grpc-encoding": "deflate", - "custom": "123", - ] - XCTAssertEqual(customMetadata, expectedMetadata) - } - - func testReceiveInitialMetadataWhenClientClosedAndServerClosed() { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Open server - let headers: HPACKHeaders = [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, - ] - XCTAssertNoThrow(try stateMachine.receive(metadata: headers, endStream: false)) - - // Close client - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - - // Close server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) - - // This time receive metadata but with endStream = false. We should throw - // here, since this would be invalid. - XCTAssertThrowsError( - ofType: RPCError.self, - try stateMachine.receive(metadata: .init(), endStream: false) - ) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Server is closed, nothing could have been sent.") - } - } - - // - MARK: Receive end trailers - - func testReceiveEndTrailerWhenClientIdleAndServerIdle() { - var stateMachine = makeClientStateMachine() - - // Receive an end trailer - XCTAssertThrowsError( - ofType: RPCError.self, - try stateMachine.receive(metadata: .init(), endStream: true) - ) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Server cannot have sent metadata if the client is idle.") - } - } - - func testReceiveEndTrailerWhenClientOpenAndServerIdle() { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Receive a trailer-only response - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) - } - - func testReceiveEndTrailerWhenClientOpenAndServerOpen() throws { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Open server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - - // Receive an end trailer - let action = try stateMachine.receive( - metadata: [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, - GRPCHTTP2Keys.encoding.rawValue: "deflate", - "custom": "123", - ], - endStream: true - ) - guard case .receivedMetadata(let customMetadata) = action else { - XCTFail("Expected action to be receivedMetadata but was \(action)") - return - } - - let expectedMetadata: Metadata = [ - ":status": "200", - "content-type": "application/grpc", - "grpc-encoding": "deflate", - "custom": "123", - ] - XCTAssertEqual(customMetadata, expectedMetadata) - } - - func testReceiveEndTrailerWhenClientOpenAndServerClosed() { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Open server - let headers: HPACKHeaders = [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, - ] - XCTAssertNoThrow(try stateMachine.receive(metadata: headers, endStream: false)) - - // Close server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) - - // Receive another end trailer - XCTAssertThrowsError( - ofType: RPCError.self, - try stateMachine.receive(metadata: .init(), endStream: true) - ) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Server is closed, nothing could have been sent.") - } - } - - func testReceiveEndTrailerWhenClientClosedAndServerIdle() { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Close client - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - - // Server sends a trailers-only response - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) - } - - func testReceiveEndTrailerWhenClientClosedAndServerOpen() throws { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Open server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - - // Close the client stream - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - - // Closing the server now should not throw - let action = try stateMachine.receive( - metadata: [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, - GRPCHTTP2Keys.encoding.rawValue: "deflate", - "custom": "123", - ], - endStream: true - ) - guard case .receivedMetadata(let customMetadata) = action else { - XCTFail("Expected action to be receivedMetadata but was \(action)") - return - } - - let expectedMetadata: Metadata = [ - ":status": "200", - "content-type": "application/grpc", - "grpc-encoding": "deflate", - "custom": "123", - ] - XCTAssertEqual(customMetadata, expectedMetadata) - } - - func testReceiveEndTrailerWhenClientClosedAndServerClosed() { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Open server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - - // Close client - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - - // Close server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) - - // Close server again (endStream = true) and assert we don't throw. - // This can happen if the previous close was caused by a grpc-status header - // and then the server sends an empty frame with EOS set. - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) - } - - // - MARK: Receive message + } - func testReceiveMessageWhenClientIdleAndServerIdle() { - var stateMachine = makeClientStateMachine() + // - MARK: Send Status and Trailers - XCTAssertThrowsError( - ofType: RPCError.self, - try stateMachine.receive(message: .init(), endStream: false) - ) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual( - error.message, - "Cannot have received anything from server if client is not yet open." - ) + func testSendStatusAndTrailers() { + for targetState in TargetStateMachineState.allCases { + var stateMachine = self.makeClientStateMachine(targetState: targetState) + + // This operation is never allowed on the client. + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send( + status: Status(code: .ok, message: ""), + metadata: .init(), + trailersOnly: false + ) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Client cannot send status and trailer.") + } } + } - func testReceiveMessageWhenClientOpenAndServerIdle() { - var stateMachine = makeClientStateMachine() + // - MARK: Receive initial metadata - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) + func testReceiveInitialMetadataWhenClientIdleAndServerIdle() { + var stateMachine = self.makeClientStateMachine(targetState: .clientIdleServerIdle) XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.receive(message: .init(), endStream: false) + try stateMachine.receive(metadata: .init(), endStream: false) ) { error in XCTAssertEqual(error.code, .internalError) - XCTAssertEqual( - error.message, - "Server cannot have sent a message before sending the initial metadata." - ) + XCTAssertEqual(error.message, "Server cannot have sent metadata if the client is idle.") } } - func testReceiveMessageWhenClientOpenAndServerOpen() throws { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) + func testReceiveInitialMetadataWhenServerIdleOrOpen() throws { + for targetState in [TargetStateMachineState.clientOpenServerIdle, .clientOpenServerOpen, .clientClosedServerIdle, .clientClosedServerOpen] { + var stateMachine = self.makeClientStateMachine(targetState: targetState) - // Open server - let headers: HPACKHeaders = [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, - ] - XCTAssertNoThrow(try stateMachine.receive(metadata: headers, endStream: false)) - - XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: false)) - XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + // Receive metadata = open server + let action = try stateMachine.receive( + metadata: [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, + GRPCHTTP2Keys.encoding.rawValue: "deflate", + "custom": "123", + "custom-bin": String(base64Encoding: [42, 43, 44]), + ], + endStream: false + ) + guard case .receivedMetadata(let customMetadata) = action else { + XCTFail("Expected action to be receivedMetadata but was \(action)") + return + } + + var expectedMetadata: Metadata = [ + ":status": "200", + "content-type": "application/grpc", + "grpc-encoding": "deflate", + "custom": "123", + ] + expectedMetadata.addBinary([42, 43, 44], forKey: "custom-bin") + XCTAssertEqual(customMetadata, expectedMetadata) + } } - func testReceiveMessageWhenClientOpenAndServerClosed() { - var stateMachine = makeClientStateMachine() + func testReceiveInitialMetadataWhenServerClosed() { + for targetState in [TargetStateMachineState.clientOpenServerClosed, .clientClosedServerClosed] { + var stateMachine = self.makeClientStateMachine(targetState: targetState) - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(metadata: .init(), endStream: false) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Server is closed, nothing could have been sent.") + } + } + } - // Open server - let headers: HPACKHeaders = [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, - ] - XCTAssertNoThrow(try stateMachine.receive(metadata: headers, endStream: false)) + // - MARK: Receive end trailers - // Close server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) + func testReceiveEndTrailerWhenClientIdleAndServerIdle() { + var stateMachine = self.makeClientStateMachine(targetState: .clientIdleServerIdle) + // Receive an end trailer XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.receive(message: .init(), endStream: false) + try stateMachine.receive(metadata: .init(), endStream: true) ) { error in XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Cannot have received anything from a closed server.") + XCTAssertEqual(error.message, "Server cannot have sent metadata if the client is idle.") } } - func testReceiveMessageWhenClientClosedAndServerIdle() { - var stateMachine = makeClientStateMachine() + func testReceiveEndTrailerWhenClientOpenAndServerIdle() { + var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerIdle) - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) + // Receive a trailer-only response + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) + } - // Close client - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + func testReceiveEndTrailerWhenServerOpen() throws { + for targetState in [TargetStateMachineState.clientOpenServerOpen, .clientClosedServerOpen] { + var stateMachine = self.makeClientStateMachine(targetState: targetState) + + // Receive an end trailer + let action = try stateMachine.receive( + metadata: [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, + GRPCHTTP2Keys.encoding.rawValue: "deflate", + "custom": "123", + ], + endStream: true + ) + guard case .receivedMetadata(let customMetadata) = action else { + XCTFail("Expected action to be receivedMetadata but was \(action)") + return + } + + let expectedMetadata: Metadata = [ + ":status": "200", + "content-type": "application/grpc", + "grpc-encoding": "deflate", + "custom": "123", + ] + XCTAssertEqual(customMetadata, expectedMetadata) + } + } + + func testReceiveEndTrailerWhenClientOpenAndServerClosed() { + var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerClosed) + // Receive another end trailer XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.receive(message: .init(), endStream: false) + try stateMachine.receive(metadata: .init(), endStream: true) ) { error in XCTAssertEqual(error.code, .internalError) - XCTAssertEqual( - error.message, - "Server cannot have sent a message before sending the initial metadata." - ) + XCTAssertEqual(error.message, "Server is closed, nothing could have been sent.") } } - func testReceiveMessageWhenClientClosedAndServerOpen() { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Open server - let headers: HPACKHeaders = [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, - ] - XCTAssertNoThrow(try stateMachine.receive(metadata: headers, endStream: false)) - - // Close client - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + func testReceiveEndTrailerWhenClientClosedAndServerIdle() { + var stateMachine = self.makeClientStateMachine(targetState: .clientClosedServerIdle) - XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: false)) - XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + // Server sends a trailers-only response + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) } - func testReceiveMessageWhenClientClosedAndServerClosed() { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) + func testReceiveEndTrailerWhenClientClosedAndServerClosed() { + var stateMachine = self.makeClientStateMachine(targetState: .clientClosedServerClosed) - // Open server - let headers: HPACKHeaders = [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, - ] - XCTAssertNoThrow(try stateMachine.receive(metadata: headers, endStream: false)) + // Close server again (endStream = true) and assert we don't throw. + // This can happen if the previous close was caused by a grpc-status header + // and then the server sends an empty frame with EOS set. + XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) + } - // Close client - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + // - MARK: Receive message - // Close server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) + func testReceiveMessageWhenClientIdleAndServerIdle() { + var stateMachine = self.makeClientStateMachine(targetState: .clientIdleServerIdle) XCTAssertThrowsError( ofType: RPCError.self, @@ -951,15 +334,55 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertEqual(error.code, .internalError) XCTAssertEqual( error.message, - "Shouldn't have received anything if both client and server are closed." + "Cannot have received anything from server if client is not yet open." ) } } + func testReceiveMessageWhenServerIdle() { + for targetState in [TargetStateMachineState.clientOpenServerIdle, .clientClosedServerIdle] { + var stateMachine = self.makeClientStateMachine(targetState: targetState) + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(message: .init(), endStream: false) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual( + error.message, + "Server cannot have sent a message before sending the initial metadata." + ) + } + } + } + + func testReceiveMessageWhenServerOpen() throws { + for targetState in [TargetStateMachineState.clientOpenServerOpen, .clientClosedServerOpen] { + var stateMachine = self.makeClientStateMachine(targetState: targetState) + + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + } + } + + func testReceiveMessageWhenServerClosed() { + for targetState in [TargetStateMachineState.clientOpenServerClosed, .clientClosedServerClosed] { + var stateMachine = self.makeClientStateMachine(targetState: targetState) + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(message: .init(), endStream: false) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Cannot have received anything from a closed server.") + } + } + } + // - MARK: Next outbound message func testNextOutboundMessageWhenClientIdleAndServerIdle() { - var stateMachine = makeClientStateMachine() + var stateMachine = self.makeClientStateMachine(targetState: .clientIdleServerIdle) XCTAssertThrowsError( ofType: RPCError.self, @@ -970,33 +393,29 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { } } - func testNextOutboundMessageWhenClientOpenAndServerIdle() throws { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - XCTAssertNil(try stateMachine.nextOutboundMessage()) - - XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) - let request = try XCTUnwrap(stateMachine.nextOutboundMessage()) - - let expectedBytes: [UInt8] = [ - 0, // compression flag: unset - 0, 0, 0, 2, // message length: 2 bytes - 42, 42, // original message - ] - XCTAssertEqual(Array(buffer: request), expectedBytes) - - // And then make sure that nothing else is returned anymore - XCTAssertNil(try stateMachine.nextOutboundMessage()) + func testNextOutboundMessageWhenClientOpenAndServerOpenOrIdle() throws { + for targetState in [TargetStateMachineState.clientOpenServerIdle, .clientOpenServerOpen] { + var stateMachine = self.makeClientStateMachine(targetState: targetState) + + XCTAssertNil(try stateMachine.nextOutboundMessage()) + + XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) + let request = try XCTUnwrap(stateMachine.nextOutboundMessage()) + + let expectedBytes: [UInt8] = [ + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42, // original message + ] + XCTAssertEqual(Array(buffer: request), expectedBytes) + + // And then make sure that nothing else is returned anymore + XCTAssertNil(try stateMachine.nextOutboundMessage()) + } } func testNextOutboundMessageWhenClientOpenAndServerIdle_WithCompression() throws { - var stateMachine = makeClientStateMachineWithCompression() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) + var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerIdle, compressionEnabled: true) XCTAssertNil(try stateMachine.nextOutboundMessage()) @@ -1014,39 +433,8 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertEqual(Array(buffer: request), expectedBytes) } - func testNextOutboundMessageWhenClientOpenAndServerOpen() throws { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Open server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) - - XCTAssertNil(try stateMachine.nextOutboundMessage()) - - XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) - let request = try XCTUnwrap(stateMachine.nextOutboundMessage()) - - let expectedBytes: [UInt8] = [ - 0, // compression flag: unset - 0, 0, 0, 2, // message length: 2 bytes - 42, 42, // original message - ] - XCTAssertEqual(Array(buffer: request), expectedBytes) - - // And then make sure that nothing else is returned anymore - XCTAssertNil(try stateMachine.nextOutboundMessage()) - } - func testNextOutboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { - var stateMachine = makeClientStateMachineWithCompression() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Open server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerOpen, compressionEnabled: true) XCTAssertNil(try stateMachine.nextOutboundMessage()) @@ -1065,20 +453,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { } func testNextOutboundMessageWhenClientOpenAndServerClosed() throws { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Open server - let headers: HPACKHeaders = [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, - ] - XCTAssertNoThrow(try stateMachine.receive(metadata: headers, endStream: false)) - - // Close server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) + var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerClosed) // No messages to send, so make sure nil is returned XCTAssertNil(try stateMachine.nextOutboundMessage()) @@ -1090,10 +465,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { } func testNextOutboundMessageWhenClientClosedAndServerIdle() throws { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) + var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerIdle) // Send a message and close client XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: true)) @@ -1113,13 +485,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { } func testNextOutboundMessageWhenClientClosedAndServerOpen() throws { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Open server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: false)) + var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerOpen) // Send a message and close client XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: true)) @@ -1139,18 +505,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { } func testNextOutboundMessageWhenClientClosedAndServerClosed() { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Open server - let headers: HPACKHeaders = [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, - ] - XCTAssertNoThrow(try stateMachine.receive(metadata: headers, endStream: false)) - + var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerOpen) // Send a message XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) @@ -1167,32 +522,15 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // - MARK: Next inbound message - func testNextInboundMessageWhenClientIdleAndServerIdle() { - var stateMachine = makeClientStateMachine() - XCTAssertNil(stateMachine.nextInboundMessage()) - } - - func testNextInboundMessageWhenClientOpenAndServerIdle() { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - XCTAssertNil(stateMachine.nextInboundMessage()) + func testNextInboundMessageWhenServerIdle() { + for targetState in [TargetStateMachineState.clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle] { + var stateMachine = self.makeClientStateMachine(targetState: targetState) + XCTAssertNil(stateMachine.nextInboundMessage()) + } } func testNextInboundMessageWhenClientOpenAndServerOpen() throws { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Open server - let headers: HPACKHeaders = [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, - ] - XCTAssertNoThrow(try stateMachine.receive(metadata: headers, endStream: false)) + var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerOpen) let receivedBytes = ByteBuffer(bytes: [ 0, // compression flag: unset @@ -1208,15 +546,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { } func testNextInboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { - var stateMachine = makeClientStateMachineWithCompression() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Open server - XCTAssertNoThrow( - try stateMachine.receive(metadata: .receivedHeadersWithDeflateCompression, endStream: false) - ) + var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerOpen, compressionEnabled: true) let originalMessage = [UInt8]([42, 42, 43, 43]) var framer = GRPCMessageFramer() @@ -1234,17 +564,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { } func testNextInboundMessageWhenClientOpenAndServerClosed() throws { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Open server - let headers: HPACKHeaders = [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, - ] - XCTAssertNoThrow(try stateMachine.receive(metadata: headers, endStream: false)) + var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerOpen) let receivedBytes = ByteBuffer(bytes: [ 0, // compression flag: unset @@ -1262,32 +582,8 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertNil(stateMachine.nextInboundMessage()) } - func testNextInboundMessageWhenClientClosedAndServerIdle() { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Close client - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) - - // If server is idle it means we never got any messages, assert no inbound - // message is present. - XCTAssertNil(stateMachine.nextInboundMessage()) - } - func testNextInboundMessageWhenClientClosedAndServerOpen() throws { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Open server - let headers: HPACKHeaders = [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, - ] - XCTAssertNoThrow(try stateMachine.receive(metadata: headers, endStream: false)) + var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerOpen) let receivedBytes = ByteBuffer(bytes: [ 0, // compression flag: unset @@ -1308,18 +604,8 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { } func testNextInboundMessageWhenClientClosedAndServerClosed() throws { - var stateMachine = makeClientStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - - // Open server - let headers: HPACKHeaders = [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, - ] - XCTAssertNoThrow(try stateMachine.receive(metadata: headers, endStream: false)) - + var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerOpen) + let receivedBytes = ByteBuffer(bytes: [ 0, // compression flag: unset 0, 0, 0, 2, // message length: 2 bytes @@ -1361,6 +647,16 @@ extension HPACKHeaders { static let receivedHeadersWithoutEndpoint: Self = [ GRPCHTTP2Keys.contentType.rawValue: "application/grpc" ] + static let serverInitialMetadata: Self = [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue + ] + static let serverInitialMetadataWithDeflateCompression: Self = [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, + GRPCHTTP2Keys.encoding.rawValue: "deflate", + GRPCHTTP2Keys.acceptEncoding.rawValue: "deflate", + ] } final class GRPCStreamServerStateMachineTests: XCTestCase { From 91ad5d804f6e03a7c13ca9dd61e35564545b3d0c Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Mon, 19 Feb 2024 16:21:33 +0000 Subject: [PATCH 18/51] More PR changes --- Sources/GRPCCore/Metadata.swift | 2 +- .../GRPCStreamStateMachine.swift | 183 ++++++++++-------- 2 files changed, 106 insertions(+), 79 deletions(-) diff --git a/Sources/GRPCCore/Metadata.swift b/Sources/GRPCCore/Metadata.swift index 1b0154507..217c8a784 100644 --- a/Sources/GRPCCore/Metadata.swift +++ b/Sources/GRPCCore/Metadata.swift @@ -88,7 +88,7 @@ public struct Metadata: Sendable, Hashable { /// The value as a String. If it was originally stored as a binary, the base64-encoded String version /// of the binary data will be returned instead. - public var stringValue: String { + public func encoded() -> String { switch self { case .string(let string): return string diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 4c9c9c89c..def1c83c7 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -276,6 +276,8 @@ enum GRPCStreamStateMachineState { } } +// - MARK: Client + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) struct GRPCStreamStateMachine { private var state: GRPCStreamStateMachineState @@ -295,18 +297,18 @@ struct GRPCStreamStateMachine { mutating func send(metadata: Metadata) throws -> HPACKHeaders { switch self.configuration { case .client(let clientConfiguration): - return try clientSend(metadata: metadata, configuration: clientConfiguration) - case .server: - return try serverSend(metadata: metadata) + return try self.clientSend(metadata: metadata, configuration: clientConfiguration) + case .server(let serverConfiguration): + return try self.serverSend(metadata: metadata, configuration: serverConfiguration) } } mutating func send(message: [UInt8], endStream: Bool) throws { switch self.configuration { case .client: - try clientSend(message: message, endStream: endStream) + try self.clientSend(message: message, endStream: endStream) case .server: - try serverSend(message: message, endStream: endStream) + try self.serverSend(message: message, endStream: endStream) } } @@ -318,16 +320,16 @@ struct GRPCStreamStateMachine { "Client cannot send status and trailer." ) case .server: - return try serverSend(status: status, metadata: metadata, trailersOnly: trailersOnly) + return try self.serverSend(status: status, metadata: metadata, trailersOnly: trailersOnly) } } mutating func receive(metadata: HPACKHeaders, endStream: Bool) throws -> OnMetadataReceived { switch self.configuration { case .client: - return try clientReceive(metadata: metadata, endStream: endStream) + return try self.clientReceive(metadata: metadata, endStream: endStream) case .server(let serverConfiguration): - return try serverReceive( + return try self.serverReceive( metadata: metadata, endStream: endStream, configuration: serverConfiguration @@ -338,27 +340,27 @@ struct GRPCStreamStateMachine { mutating func receive(message: ByteBuffer, endStream: Bool) throws { switch self.configuration { case .client: - try clientReceive(bytes: message, endStream: endStream) + try self.clientReceive(bytes: message, endStream: endStream) case .server: - try serverReceive(bytes: message, endStream: endStream) + try self.serverReceive(bytes: message, endStream: endStream) } } mutating func nextOutboundMessage() throws -> ByteBuffer? { switch self.configuration { case .client: - return try clientNextOutboundMessage() + return try self.clientNextOutboundMessage() case .server: - return try serverNextOutboundMessage() + return try self.serverNextOutboundMessage() } } mutating func nextInboundMessage() -> [UInt8]? { switch self.configuration { case .client: - return clientNextInboundMessage() + return self.clientNextInboundMessage() case .server: - return serverNextInboundMessage() + return self.serverNextInboundMessage() } } } @@ -392,7 +394,7 @@ extension GRPCStreamStateMachine { } for metadataPair in customMetadata { - headers.add(name: metadataPair.key, value: metadataPair.value.stringValue) + headers.add(name: metadataPair.key, value: metadataPair.value.encoded()) } return headers @@ -461,38 +463,38 @@ extension GRPCStreamStateMachine { ) } } + + enum OnClientNextOutboundMessage { + case sendMessage(ByteBuffer) + case keepAwaitingNewMessages + case requestEnded + } /// Returns the client's next request to the server. /// - Returns: The request to be made to the server. - private mutating func clientNextOutboundMessage() throws -> ByteBuffer? { + private mutating func clientNextOutboundMessage() throws -> OnClientNextOutboundMessage { switch self.state { case .clientIdleServerIdle: throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client is not open yet.") case .clientOpenServerIdle(var state): let request = try state.framer.next(compressor: state.compressor) self.state = .clientOpenServerIdle(state) - return request + return request != nil ? .sendMessage(request!) : .keepAwaitingNewMessages case .clientOpenServerOpen(var state): let request = try state.framer.next(compressor: state.compressor) self.state = .clientOpenServerOpen(state) - return request - case .clientOpenServerClosed(var state): - // Server may have closed but still be waiting for client messages, - // for example if it's a client-streaming RPC. - let request = try state.framer.next(compressor: state.compressor) - self.state = .clientOpenServerClosed(state) - return request + return request != nil ? .sendMessage(request!) : .keepAwaitingNewMessages case .clientClosedServerIdle(var state): let request = try state.framer.next(compressor: state.compressor) self.state = .clientClosedServerIdle(state) - return request + return request != nil ? .sendMessage(request!) : .keepAwaitingNewMessages case .clientClosedServerOpen(var state): let request = try state.framer.next(compressor: state.compressor) self.state = .clientClosedServerOpen(state) - return request - case .clientClosedServerClosed: - // Nothing to do if both are closed. - return nil + return request != nil ? .sendMessage(request!) : .keepAwaitingNewMessages + case .clientOpenServerClosed, .clientClosedServerClosed: + // Nothing to do if server is closed. + return .requestEnded } } @@ -541,10 +543,21 @@ extension GRPCStreamStateMachine { // This is a trailers-only response: close server. self.state = .clientOpenServerClosed(.init(previousState: state)) } else { + var inboundEncoding: CompressionAlgorithm? + if let serverEncoding = metadata.first(name: GRPCHTTP2Keys.encoding.rawValue) { + guard let parsedEncoding = CompressionAlgorithm(rawValue: serverEncoding) else { + return .failedRequest(.init( + code: .internalError, + message: "The server picked a compression algorithm the client does not know about." + )) + } + inboundEncoding = parsedEncoding + } + self.state = .clientOpenServerOpen( .init( previousState: state, - decompressionAlgorithm: metadata.encoding + decompressionAlgorithm: inboundEncoding ) ) } @@ -553,8 +566,8 @@ extension GRPCStreamStateMachine { if endStream { self.state = .clientOpenServerClosed(.init(previousState: state)) } else { - // This state is valid: server can send trailing metadata without END_STREAM - // set, and follow it with an empty message frame where the flag *is* set. + // This state is valid: server can send trailing metadata without grpc-status + // or END_STREAM set, and follow it with an empty message frame where they are set. () // TODO: I believe we should set some flag in the state to signal that // we're expecting an empty data frame with END_STREAM set; otherwise, @@ -601,10 +614,21 @@ extension GRPCStreamStateMachine { // This is a trailers-only response. self.state = .clientClosedServerClosed(.init(previousState: state)) } else { + var inboundEncoding: CompressionAlgorithm? + if let serverEncoding = metadata.first(name: GRPCHTTP2Keys.encoding.rawValue) { + guard let parsedEncoding = CompressionAlgorithm(rawValue: serverEncoding) else { + return .failedRequest(.init( + code: .internalError, + message: "The server picked a compression algorithm the client does not know about." + )) + } + inboundEncoding = parsedEncoding + } + self.state = .clientClosedServerOpen( .init( previousState: state, - decompressionAlgorithm: metadata.encoding + decompressionAlgorithm: inboundEncoding ) ) } @@ -615,8 +639,8 @@ extension GRPCStreamStateMachine { state.decompressor?.end() self.state = .clientClosedServerClosed(.init(previousState: state)) } else { - // This state is valid: server can send trailing metadata without END_STREAM - // set, and follow it with an empty message frame where the flag *is* set. + // This state is valid: server can send trailing metadata without grpc-status + // or END_STREAM set, and follow it with an empty message frame where they are set. () // TODO: I believe we should set some flag in the state to signal that // we're expecting an empty data frame with END_STREAM set; otherwise, @@ -722,10 +746,13 @@ extension GRPCStreamStateMachine { } } +// - MARK: Server + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension GRPCStreamStateMachine { private func makeResponseHeaders( outboundEncoding: CompressionAlgorithm?, + configuration: GRPCStreamStateMachineConfiguration.ServerConfiguration, customMetadata: Metadata ) -> HPACKHeaders { // Response headers always contain :status (HTTP Status 200) and content-type. @@ -740,26 +767,32 @@ extension GRPCStreamStateMachine { headers.encoding = outboundEncoding } + if !configuration.acceptedEncodings.isEmpty { + headers.acceptedEncodings = configuration.acceptedEncodings + } + for metadataPair in customMetadata { - headers.add(name: metadataPair.key, value: metadataPair.value.stringValue) + headers.add(name: metadataPair.key, value: metadataPair.value.encoded()) } return headers } - private mutating func serverSend(metadata: Metadata) throws -> HPACKHeaders { + private mutating func serverSend(metadata: Metadata, configuration: GRPCStreamStateMachineConfiguration.ServerConfiguration) throws -> HPACKHeaders { // Server sends initial metadata switch self.state { case .clientOpenServerIdle(let state): self.state = .clientOpenServerOpen(.init(previousState: state)) return self.makeResponseHeaders( outboundEncoding: state.outboundCompression, + configuration: configuration, customMetadata: metadata ) case .clientClosedServerIdle(let state): self.state = .clientClosedServerOpen(.init(previousState: state)) return self.makeResponseHeaders( outboundEncoding: state.outboundCompression, + configuration: configuration, customMetadata: metadata ) case .clientIdleServerIdle: @@ -816,7 +849,7 @@ extension GRPCStreamStateMachine { var headers = HPACKHeaders() if trailersOnly { - // Reserve 5 for capacity: 3 for the required headers, and 1 for the + // Reserve 4 for capacity: 3 for the required headers, and 1 for the // optional status message. headers.reserveCapacity(4 + customMetadata.count) headers.status = "200" @@ -835,7 +868,7 @@ extension GRPCStreamStateMachine { } for metadataPair in customMetadata { - headers.add(name: metadataPair.key, value: metadataPair.value.stringValue) + headers.add(name: metadataPair.key, value: metadataPair.value.encoded()) } return headers @@ -872,17 +905,17 @@ extension GRPCStreamStateMachine { endStream: Bool, configuration: GRPCStreamStateMachineConfiguration.ServerConfiguration ) throws -> OnMetadataReceived { - if endStream, case .clientIdleServerIdle = self.state { - throw self.assertionFailureAndCreateRPCErrorOnInternalError( - """ - Client should have opened before ending the stream: \ - stream shouldn't have been closed when sending initial metadata. - """ - ) - } - switch self.state { case .clientIdleServerIdle(let state): + if endStream { + throw self.assertionFailureAndCreateRPCErrorOnInternalError( + """ + Client should have opened before ending the stream: \ + stream shouldn't have been closed when sending initial metadata. + """ + ) + } + guard metadata.contentType != nil else { // Respond with HTTP-level Unsupported Media Type status code. var trailers = HPACKHeaders() @@ -899,8 +932,7 @@ extension GRPCStreamStateMachine { } func isIdentityOrCompatibleEncoding(_ clientEncoding: CompressionAlgorithm) -> Bool { - clientEncoding == .identity - || configuration.acceptedEncodings.contains(where: { $0 == clientEncoding }) + clientEncoding == .identity || configuration.acceptedEncodings.contains(clientEncoding) } // Firstly, find out if we support the client's chosen encoding, and reject @@ -954,16 +986,12 @@ extension GRPCStreamStateMachine { // based on the encodings the client has advertised. var outboundEncoding: CompressionAlgorithm? = nil if let clientAdvertisedEncodings = metadata.acceptedEncodings { - for clientAcceptedEncoding in clientAdvertisedEncodings - where isIdentityOrCompatibleEncoding(clientAcceptedEncoding) { - // Found the preferred encoding: use it to compress responses. - // If it's identity, just skip it altogether, since we won't be - // compressing. - if clientAcceptedEncoding != .identity { - outboundEncoding = clientAcceptedEncoding - } - break - } + // Find the preferred encoding and use it to compress responses. + // If it's identity, just skip it altogether, since we won't be + // compressing. + outboundEncoding = clientAdvertisedEncodings + .first { isIdentityOrCompatibleEncoding($0) } + .flatMap { $0 == .identity ? nil : $0 } } self.state = .clientOpenServerIdle( @@ -1104,7 +1132,7 @@ extension HPACKHeaders { } set { if let newValue { - self.replaceOrAddString(newValue.fullyQualifiedMethod, forKey: .path) + self.add(newValue.fullyQualifiedMethod, forKey: .path) } else { self.removeAllValues(forKey: .path) } @@ -1118,7 +1146,7 @@ extension HPACKHeaders { } set { if let newValue { - self.replaceOrAddString(newValue.canonicalValue, forKey: .contentType) + self.add(newValue.canonicalValue, forKey: .contentType) } else { self.removeAllValues(forKey: .contentType) } @@ -1127,11 +1155,12 @@ extension HPACKHeaders { var encoding: CompressionAlgorithm? { get { - self.firstString(forKey: .encoding).flatMap { CompressionAlgorithm(rawValue: $0) } + self.firstString(forKey: .encoding) + .flatMap { CompressionAlgorithm(rawValue: $0) } } set { if let newValue { - self.replaceOrAddString(newValue.name, forKey: .encoding) + self.add(newValue.name, forKey: .encoding) } else { self.removeAllValues(forKey: .encoding) } @@ -1140,16 +1169,14 @@ extension HPACKHeaders { var acceptedEncodings: [CompressionAlgorithm]? { get { - self.firstString(forKey: .acceptEncoding)? - .split(separator: ",") + self.values(forHeader: GRPCHTTP2Keys.acceptEncoding.rawValue, canonicalForm: true) .compactMap { CompressionAlgorithm(rawValue: String($0)) } } set { if let newValue { - self.replaceOrAddString( - newValue.map({ $0.name }).joined(separator: ","), - forKey: .acceptEncoding - ) + for value in newValue { + self.add(value.name, forKey: .acceptEncoding) + } } else { self.removeAllValues(forKey: .acceptEncoding) } @@ -1162,7 +1189,7 @@ extension HPACKHeaders { } set { if let newValue { - self.replaceOrAddString(newValue.rawValue, forKey: .scheme) + self.add(newValue.rawValue, forKey: .scheme) } else { self.removeAllValues(forKey: .scheme) } @@ -1175,7 +1202,7 @@ extension HPACKHeaders { } set { if let newValue { - self.replaceOrAddString(newValue, forKey: .method) + self.add(newValue, forKey: .method) } else { self.removeAllValues(forKey: .method) } @@ -1188,7 +1215,7 @@ extension HPACKHeaders { } set { if let newValue { - self.replaceOrAddString(newValue, forKey: .te) + self.add(newValue, forKey: .te) } else { self.removeAllValues(forKey: .te) } @@ -1201,7 +1228,7 @@ extension HPACKHeaders { } set { if let newValue { - self.replaceOrAddString(newValue, forKey: .status) + self.add(newValue, forKey: .status) } else { self.removeAllValues(forKey: .status) } @@ -1216,7 +1243,7 @@ extension HPACKHeaders { } set { if let newValue { - self.replaceOrAddString(String(newValue.rawValue), forKey: .grpcStatus) + self.add(String(newValue.rawValue), forKey: .grpcStatus) } else { self.removeAllValues(forKey: .grpcStatus) } @@ -1229,7 +1256,7 @@ extension HPACKHeaders { } set { if let newValue { - self.replaceOrAddString(newValue, forKey: .grpcStatusMessage) + self.add(newValue, forKey: .grpcStatusMessage) } else { self.removeAllValues(forKey: .grpcStatusMessage) } @@ -1242,8 +1269,8 @@ extension HPACKHeaders { } } - private mutating func replaceOrAddString(_ value: String, forKey key: GRPCHTTP2Keys) { - self.replaceOrAdd(name: key.rawValue, value: value) + private mutating func add(_ value: String, forKey key: GRPCHTTP2Keys) { + self.add(name: key.rawValue, value: value) } private mutating func removeAllValues(forKey key: GRPCHTTP2Keys) { From 4061768e6ef3181a6231811dbd98eca95bb0b93f Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Mon, 19 Feb 2024 16:23:09 +0000 Subject: [PATCH 19/51] Formatting --- .../GRPCStreamStateMachine.swift | 74 +++++---- .../GRPCStreamStateMachineTests.swift | 140 +++++++++++------- 2 files changed, 132 insertions(+), 82 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index def1c83c7..a07bd57a5 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -259,18 +259,29 @@ enum GRPCStreamStateMachineState { } struct ClientClosedServerClosedState { + // We still need the framer and compressor in case the server has closed + // but its buffer is not yet empty and still needs to send messages out to + // the client. + var framer: GRPCMessageFramer + var compressor: Zlib.Compressor? // These are already deframed, so we don't need the deframer anymore. var inboundMessageBuffer: OneOrManyQueue<[UInt8]> init(previousState: ClientClosedServerOpenState) { + self.framer = previousState.framer + self.compressor = previousState.compressor self.inboundMessageBuffer = previousState.inboundMessageBuffer } init(previousState: ClientClosedServerIdleState) { + self.framer = previousState.framer + self.compressor = previousState.compressor self.inboundMessageBuffer = previousState.inboundMessageBuffer } init(previousState: ClientOpenServerClosedState) { + self.framer = previousState.framer + self.compressor = previousState.compressor self.inboundMessageBuffer = previousState.inboundMessageBuffer } } @@ -463,38 +474,31 @@ extension GRPCStreamStateMachine { ) } } - - enum OnClientNextOutboundMessage { - case sendMessage(ByteBuffer) - case keepAwaitingNewMessages - case requestEnded - } - /// Returns the client's next request to the server. /// - Returns: The request to be made to the server. - private mutating func clientNextOutboundMessage() throws -> OnClientNextOutboundMessage { + private mutating func clientNextOutboundMessage() throws -> ByteBuffer? { switch self.state { case .clientIdleServerIdle: throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client is not open yet.") case .clientOpenServerIdle(var state): let request = try state.framer.next(compressor: state.compressor) self.state = .clientOpenServerIdle(state) - return request != nil ? .sendMessage(request!) : .keepAwaitingNewMessages + return request case .clientOpenServerOpen(var state): let request = try state.framer.next(compressor: state.compressor) self.state = .clientOpenServerOpen(state) - return request != nil ? .sendMessage(request!) : .keepAwaitingNewMessages + return request case .clientClosedServerIdle(var state): let request = try state.framer.next(compressor: state.compressor) self.state = .clientClosedServerIdle(state) - return request != nil ? .sendMessage(request!) : .keepAwaitingNewMessages + return request case .clientClosedServerOpen(var state): let request = try state.framer.next(compressor: state.compressor) self.state = .clientClosedServerOpen(state) - return request != nil ? .sendMessage(request!) : .keepAwaitingNewMessages + return request case .clientOpenServerClosed, .clientClosedServerClosed: // Nothing to do if server is closed. - return .requestEnded + return nil } } @@ -546,10 +550,12 @@ extension GRPCStreamStateMachine { var inboundEncoding: CompressionAlgorithm? if let serverEncoding = metadata.first(name: GRPCHTTP2Keys.encoding.rawValue) { guard let parsedEncoding = CompressionAlgorithm(rawValue: serverEncoding) else { - return .failedRequest(.init( - code: .internalError, - message: "The server picked a compression algorithm the client does not know about." - )) + return .failedRequest( + .init( + code: .internalError, + message: "The server picked a compression algorithm the client does not know about." + ) + ) } inboundEncoding = parsedEncoding } @@ -617,10 +623,12 @@ extension GRPCStreamStateMachine { var inboundEncoding: CompressionAlgorithm? if let serverEncoding = metadata.first(name: GRPCHTTP2Keys.encoding.rawValue) { guard let parsedEncoding = CompressionAlgorithm(rawValue: serverEncoding) else { - return .failedRequest(.init( - code: .internalError, - message: "The server picked a compression algorithm the client does not know about." - )) + return .failedRequest( + .init( + code: .internalError, + message: "The server picked a compression algorithm the client does not know about." + ) + ) } inboundEncoding = parsedEncoding } @@ -770,7 +778,7 @@ extension GRPCStreamStateMachine { if !configuration.acceptedEncodings.isEmpty { headers.acceptedEncodings = configuration.acceptedEncodings } - + for metadataPair in customMetadata { headers.add(name: metadataPair.key, value: metadataPair.value.encoded()) } @@ -778,7 +786,10 @@ extension GRPCStreamStateMachine { return headers } - private mutating func serverSend(metadata: Metadata, configuration: GRPCStreamStateMachineConfiguration.ServerConfiguration) throws -> HPACKHeaders { + private mutating func serverSend( + metadata: Metadata, + configuration: GRPCStreamStateMachineConfiguration.ServerConfiguration + ) throws -> HPACKHeaders { // Server sends initial metadata switch self.state { case .clientOpenServerIdle(let state): @@ -927,7 +938,7 @@ extension GRPCStreamStateMachine { var trailers = HPACKHeaders() trailers.reserveCapacity(2) trailers.grpcStatus = .unimplemented - trailers.grpcStatusMessage = "No \(GRPCHTTP2Keys.path) header has been set." + trailers.grpcStatusMessage = "No \(GRPCHTTP2Keys.path.rawValue) header has been set." return .rejectRPC(trailers: trailers) } @@ -989,7 +1000,8 @@ extension GRPCStreamStateMachine { // Find the preferred encoding and use it to compress responses. // If it's identity, just skip it altogether, since we won't be // compressing. - outboundEncoding = clientAdvertisedEncodings + outboundEncoding = + clientAdvertisedEncodings .first { isIdentityOrCompatibleEncoding($0) } .flatMap { $0 == .identity ? nil : $0 } } @@ -1066,10 +1078,14 @@ extension GRPCStreamStateMachine { let response = try state.framer.next(compressor: state.compressor) self.state = .clientClosedServerOpen(state) return response - case .clientOpenServerClosed, .clientClosedServerClosed: - throw self.assertionFailureAndCreateRPCErrorOnInternalError( - "Can't send response if server is closed." - ) + case .clientOpenServerClosed(var state): + let response = try state.framer.next(compressor: state.compressor) + self.state = .clientOpenServerClosed(state) + return response + case .clientClosedServerClosed(var state): + let response = try state.framer.next(compressor: state.compressor) + self.state = .clientClosedServerClosed(state) + return response } } diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index c11f88be8..71efc165e 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -32,7 +32,10 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { case clientClosedServerClosed } - private func makeClientStateMachine(targetState: TargetStateMachineState, compressionEnabled: Bool = false) -> GRPCStreamStateMachine { + private func makeClientStateMachine( + targetState: TargetStateMachineState, + compressionEnabled: Bool = false + ) -> GRPCStreamStateMachine { var stateMachine = GRPCStreamStateMachine( configuration: .client( .init( @@ -45,8 +48,9 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { maximumPayloadSize: 100, skipAssertions: true ) - - let serverMetadata: HPACKHeaders = compressionEnabled ? .serverInitialMetadataWithDeflateCompression : .serverInitialMetadata + + let serverMetadata: HPACKHeaders = + compressionEnabled ? .serverInitialMetadataWithDeflateCompression : .serverInitialMetadata switch targetState { case .clientIdleServerIdle: break @@ -87,7 +91,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Close server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) } - + return stateMachine } @@ -99,11 +103,14 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { } func testSendMetadataWhenClientAlreadyOpen() throws { - for targetState in [TargetStateMachineState.clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed] { + for targetState in [ + TargetStateMachineState.clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed, + ] { var stateMachine = self.makeClientStateMachine(targetState: targetState) // Try sending metadata again: should throw - XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in + XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { + error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client is already open: shouldn't be sending metadata.") } @@ -111,11 +118,15 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { } func testSendMetadataWhenClientAlreadyClosed() throws { - for targetState in [TargetStateMachineState.clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed] { + for targetState in [ + TargetStateMachineState.clientClosedServerIdle, .clientClosedServerOpen, + .clientClosedServerClosed, + ] { var stateMachine = self.makeClientStateMachine(targetState: targetState) - + // Try sending metadata again: should throw - XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in + XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { + error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client is closed: can't send metadata.") } @@ -138,18 +149,23 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { } func testSendMessageWhenClientOpen() { - for targetState in [TargetStateMachineState.clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed] { + for targetState in [ + TargetStateMachineState.clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed, + ] { var stateMachine = self.makeClientStateMachine(targetState: targetState) - + // Now send a message XCTAssertNoThrow(try stateMachine.send(message: [], endStream: false)) } } func testSendMessageWhenClientClosed() { - for targetState in [TargetStateMachineState.clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed] { + for targetState in [ + TargetStateMachineState.clientClosedServerIdle, .clientClosedServerOpen, + .clientClosedServerClosed, + ] { var stateMachine = self.makeClientStateMachine(targetState: targetState) - + // Try sending another message: it should fail XCTAssertThrowsError( ofType: RPCError.self, @@ -180,7 +196,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertEqual(error.message, "Client cannot send status and trailer.") } } - + } // - MARK: Receive initial metadata @@ -198,7 +214,10 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { } func testReceiveInitialMetadataWhenServerIdleOrOpen() throws { - for targetState in [TargetStateMachineState.clientOpenServerIdle, .clientOpenServerOpen, .clientClosedServerIdle, .clientClosedServerOpen] { + for targetState in [ + TargetStateMachineState.clientOpenServerIdle, .clientOpenServerOpen, .clientClosedServerIdle, + .clientClosedServerOpen, + ] { var stateMachine = self.makeClientStateMachine(targetState: targetState) // Receive metadata = open server @@ -267,7 +286,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { func testReceiveEndTrailerWhenServerOpen() throws { for targetState in [TargetStateMachineState.clientOpenServerOpen, .clientClosedServerOpen] { var stateMachine = self.makeClientStateMachine(targetState: targetState) - + // Receive an end trailer let action = try stateMachine.receive( metadata: [ @@ -282,7 +301,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTFail("Expected action to be receivedMetadata but was \(action)") return } - + let expectedMetadata: Metadata = [ ":status": "200", "content-type": "application/grpc", @@ -368,7 +387,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { func testReceiveMessageWhenServerClosed() { for targetState in [TargetStateMachineState.clientOpenServerClosed, .clientClosedServerClosed] { var stateMachine = self.makeClientStateMachine(targetState: targetState) - + XCTAssertThrowsError( ofType: RPCError.self, try stateMachine.receive(message: .init(), endStream: false) @@ -396,26 +415,29 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { func testNextOutboundMessageWhenClientOpenAndServerOpenOrIdle() throws { for targetState in [TargetStateMachineState.clientOpenServerIdle, .clientOpenServerOpen] { var stateMachine = self.makeClientStateMachine(targetState: targetState) - + XCTAssertNil(try stateMachine.nextOutboundMessage()) - + XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) let request = try XCTUnwrap(stateMachine.nextOutboundMessage()) - + let expectedBytes: [UInt8] = [ 0, // compression flag: unset 0, 0, 0, 2, // message length: 2 bytes 42, 42, // original message ] XCTAssertEqual(Array(buffer: request), expectedBytes) - + // And then make sure that nothing else is returned anymore XCTAssertNil(try stateMachine.nextOutboundMessage()) } } func testNextOutboundMessageWhenClientOpenAndServerIdle_WithCompression() throws { - var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerIdle, compressionEnabled: true) + var stateMachine = self.makeClientStateMachine( + targetState: .clientOpenServerIdle, + compressionEnabled: true + ) XCTAssertNil(try stateMachine.nextOutboundMessage()) @@ -434,7 +456,10 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { } func testNextOutboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { - var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerOpen, compressionEnabled: true) + var stateMachine = self.makeClientStateMachine( + targetState: .clientOpenServerOpen, + compressionEnabled: true + ) XCTAssertNil(try stateMachine.nextOutboundMessage()) @@ -523,7 +548,9 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // - MARK: Next inbound message func testNextInboundMessageWhenServerIdle() { - for targetState in [TargetStateMachineState.clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle] { + for targetState in [ + TargetStateMachineState.clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle, + ] { var stateMachine = self.makeClientStateMachine(targetState: targetState) XCTAssertNil(stateMachine.nextInboundMessage()) } @@ -546,7 +573,10 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { } func testNextInboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { - var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerOpen, compressionEnabled: true) + var stateMachine = self.makeClientStateMachine( + targetState: .clientOpenServerOpen, + compressionEnabled: true + ) let originalMessage = [UInt8]([42, 42, 43, 43]) var framer = GRPCMessageFramer() @@ -605,7 +635,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { func testNextInboundMessageWhenClientClosedAndServerClosed() throws { var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerOpen) - + let receivedBytes = ByteBuffer(bytes: [ 0, // compression flag: unset 0, 0, 0, 2, // message length: 2 bytes @@ -649,7 +679,7 @@ extension HPACKHeaders { ] static let serverInitialMetadata: Self = [ GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue + GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, ] static let serverInitialMetadataWithDeflateCompression: Self = [ GRPCHTTP2Keys.status.rawValue: "200", @@ -1111,7 +1141,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { return } - XCTAssertTrue(metadata.isEmpty) + XCTAssertEqual(metadata, [":path": "test/test", "content-type": "application/grpc"]) } func testReceiveMetadataWhenClientIdleAndServerIdle_WithEndStream() { @@ -1566,16 +1596,19 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - // Close server - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + // Send message and close server + XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: true)) + let response = try XCTUnwrap(stateMachine.nextOutboundMessage()) - XCTAssertThrowsError( - ofType: RPCError.self, - try stateMachine.nextOutboundMessage() - ) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Can't send response if server is closed.") - } + let expectedBytes: [UInt8] = [ + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42, // original message + ] + XCTAssertEqual(Array(buffer: response), expectedBytes) + + // And then make sure that nothing else is returned anymore + XCTAssertNil(try stateMachine.nextOutboundMessage()) } func testNextOutboundMessageWhenClientClosedAndServerIdle() throws { @@ -1632,7 +1665,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertNil(try stateMachine.nextOutboundMessage()) } - func testNextOutboundMessageWhenClientClosedAndServerClosed() { + func testNextOutboundMessageWhenClientClosedAndServerClosed() throws { var stateMachine = makeServerStateMachine() // Open client @@ -1641,24 +1674,25 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Open server XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - // Send a message - XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) - // Close client XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) - // Close server - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + // Send a message and close server + XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: true)) - // Even though we have enqueued a message, don't send it, because the server - // is closed. - XCTAssertThrowsError( - ofType: RPCError.self, - try stateMachine.nextOutboundMessage() - ) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Can't send response if server is closed.") - } + // We have enqueued a message, make sure we return it even though server is closed, + // because we haven't yet drained all of the pending messages. + let response = try XCTUnwrap(stateMachine.nextOutboundMessage()) + + let expectedBytes: [UInt8] = [ + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42, // original message + ] + XCTAssertEqual(Array(buffer: response), expectedBytes) + + // And then make sure that nothing else is returned anymore + XCTAssertNil(try stateMachine.nextOutboundMessage()) } // - MARK: Next inbound message From e01293e615bc486ff123c1d9371c0c67cadc62bc Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Tue, 20 Feb 2024 14:26:35 +0000 Subject: [PATCH 20/51] Refactor server tests --- .../GRPCStreamStateMachineTests.swift | 642 ++++++------------ 1 file changed, 192 insertions(+), 450 deletions(-) diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index 71efc165e..3326c8002 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -21,17 +21,59 @@ import XCTest @testable import GRPCHTTP2Core -final class GRPCStreamClientStateMachineTests: XCTestCase { - enum TargetStateMachineState: CaseIterable { - case clientIdleServerIdle - case clientOpenServerIdle - case clientOpenServerOpen - case clientOpenServerClosed - case clientClosedServerIdle - case clientClosedServerOpen - case clientClosedServerClosed - } +enum TargetStateMachineState: CaseIterable { + case clientIdleServerIdle + case clientOpenServerIdle + case clientOpenServerOpen + case clientOpenServerClosed + case clientClosedServerIdle + case clientClosedServerOpen + case clientClosedServerClosed +} + +extension HPACKHeaders { + // Client + static let clientInitialMetadata: Self = [ + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + ] + static let clientInitialMetadataWithDeflateCompression: Self = [ + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.encoding.rawValue: "deflate", + GRPCHTTP2Keys.acceptEncoding.rawValue: "deflate", + ] + static let clientInitialMetadataWithGzipCompression: Self = [ + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.encoding.rawValue: "gzip", + GRPCHTTP2Keys.acceptEncoding.rawValue: "gzip", + ] + static let receivedHeadersWithoutContentType: Self = [ + GRPCHTTP2Keys.path.rawValue: "test/test" + ] + static let receivedHeadersWithInvalidContentType: Self = [ + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.contentType.rawValue: "invalid/invalid" + ] + static let receivedHeadersWithoutEndpoint: Self = [ + GRPCHTTP2Keys.contentType.rawValue: "application/grpc" + ] + + // Server + static let serverInitialMetadata: Self = [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, + ] + static let serverInitialMetadataWithDeflateCompression: Self = [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, + GRPCHTTP2Keys.encoding.rawValue: "deflate", + GRPCHTTP2Keys.acceptEncoding.rawValue: "deflate", + ] +} +final class GRPCStreamClientStateMachineTests: XCTestCase { private func makeClientStateMachine( targetState: TargetStateMachineState, compressionEnabled: Bool = false @@ -658,53 +700,13 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { } } -extension HPACKHeaders { - static let receivedHeaders: Self = [ - GRPCHTTP2Keys.path.rawValue: "test/test", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - ] - static let receivedHeadersWithDeflateCompression: Self = [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.path.rawValue: "test/test", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.encoding.rawValue: "deflate", - GRPCHTTP2Keys.acceptEncoding.rawValue: "deflate", - ] - static let receivedHeadersWithoutContentType: Self = [GRPCHTTP2Keys.path.rawValue: "test/test"] - static let receivedHeadersWithInvalidContentType: Self = [ - GRPCHTTP2Keys.contentType.rawValue: "invalid/invalid" - ] - static let receivedHeadersWithoutEndpoint: Self = [ - GRPCHTTP2Keys.contentType.rawValue: "application/grpc" - ] - static let serverInitialMetadata: Self = [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, - ] - static let serverInitialMetadataWithDeflateCompression: Self = [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, - GRPCHTTP2Keys.encoding.rawValue: "deflate", - GRPCHTTP2Keys.acceptEncoding.rawValue: "deflate", - ] -} - final class GRPCStreamServerStateMachineTests: XCTestCase { - private func makeServerStateMachine() -> GRPCStreamStateMachine { - GRPCStreamStateMachine( - configuration: .server( - .init( - scheme: .http, - acceptedEncodings: [] - ) - ), - maximumPayloadSize: 100, - skipAssertions: true - ) - } - - private func makeServerStateMachineWithCompression() -> GRPCStreamStateMachine { - GRPCStreamStateMachine( + private func makeServerStateMachine( + targetState: TargetStateMachineState, + compressionEnabled: Bool = false + ) -> GRPCStreamStateMachine { + + var stateMachine = GRPCStreamStateMachine( configuration: .server( .init( scheme: .http, @@ -714,12 +716,57 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { maximumPayloadSize: 100, skipAssertions: true ) + + let clientMetadata: HPACKHeaders = + compressionEnabled ? .clientInitialMetadataWithDeflateCompression : .clientInitialMetadata + switch targetState { + case .clientIdleServerIdle: + break + case .clientOpenServerIdle: + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: clientMetadata, endStream: false)) + case .clientOpenServerOpen: + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: clientMetadata, endStream: false)) + // Open server + XCTAssertNoThrow(try stateMachine.send(metadata: Metadata(headers: .serverInitialMetadata))) + case .clientOpenServerClosed: + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: clientMetadata, endStream: false)) + // Open server + XCTAssertNoThrow(try stateMachine.send(metadata: Metadata(headers: .serverInitialMetadata))) + // Close server + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + case .clientClosedServerIdle: + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: clientMetadata, endStream: false)) + // Close client + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + case .clientClosedServerOpen: + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: clientMetadata, endStream: false)) + // Open server + XCTAssertNoThrow(try stateMachine.send(metadata: Metadata(headers: .serverInitialMetadata))) + // Close client + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + case .clientClosedServerClosed: + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: clientMetadata, endStream: false)) + // Open server + XCTAssertNoThrow(try stateMachine.send(metadata: Metadata(headers: .serverInitialMetadata))) + // Close client + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + // Close server + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + } + + return stateMachine } // - MARK: Send Metadata func testSendMetadataWhenClientIdleAndServerIdle() throws { - var stateMachine = makeServerStateMachine() + var stateMachine = makeServerStateMachine(targetState: .clientIdleServerIdle) XCTAssertThrowsError( ofType: RPCError.self, @@ -734,22 +781,12 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testSendMetadataWhenClientOpenAndServerIdle() throws { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - + var stateMachine = makeServerStateMachine(targetState: .clientOpenServerIdle) XCTAssertNoThrow(try stateMachine.send(metadata: .init())) } func testSendMetadataWhenClientOpenAndServerOpen() throws { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - - // Open server - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + var stateMachine = makeServerStateMachine(targetState: .clientOpenServerOpen) // Try sending metadata again: should throw XCTAssertThrowsError( @@ -762,16 +799,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testSendMetadataWhenClientOpenAndServerClosed() throws { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - - // Open server - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - - // Close server - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + var stateMachine = makeServerStateMachine(targetState: .clientOpenServerClosed) // Try sending metadata again: should throw XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in @@ -781,13 +809,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testSendMetadataWhenClientClosedAndServerIdle() throws { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - - // Close client - XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + var stateMachine = makeServerStateMachine(targetState: .clientClosedServerIdle) // We should be allowed to send initial metadata if client is closed: // client may be finished sending request but may still be awaiting response. @@ -795,16 +817,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testSendMetadataWhenClientClosedAndServerOpen() throws { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - - // Open server - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - - // Close client - XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + var stateMachine = makeServerStateMachine(targetState: .clientClosedServerOpen) // Try sending metadata again: should throw XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in @@ -814,19 +827,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testSendMetadataWhenClientClosedAndServerClosed() throws { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - - // Open server - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - - // Close client - XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) - - // Close server - XCTAssertNoThrow(try stateMachine.send(message: .init(), endStream: true)) + var stateMachine = makeServerStateMachine(targetState: .clientClosedServerClosed) // Try sending metadata again: should throw XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in @@ -838,7 +839,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // - MARK: Send Message func testSendMessageWhenClientIdleAndServerIdle() { - var stateMachine = makeServerStateMachine() + var stateMachine = makeServerStateMachine(targetState: .clientIdleServerIdle) XCTAssertThrowsError( ofType: RPCError.self, @@ -853,10 +854,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testSendMessageWhenClientOpenAndServerIdle() { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) + var stateMachine = makeServerStateMachine(targetState: .clientOpenServerIdle) // Now send a message XCTAssertThrowsError( @@ -872,29 +870,14 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testSendMessageWhenClientOpenAndServerOpen() { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - - // Open server - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + var stateMachine = makeServerStateMachine(targetState: .clientOpenServerOpen) // Now send a message XCTAssertNoThrow(try stateMachine.send(message: [], endStream: false)) } func testSendMessageWhenClientOpenAndServerClosed() { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - - // Open server - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - - // Close server - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + var stateMachine = makeServerStateMachine(targetState: .clientOpenServerClosed) // Try sending another message: it should fail XCTAssertThrowsError( @@ -907,13 +890,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testSendMessageWhenClientClosedAndServerIdle() { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - - // Close client - XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + var stateMachine = makeServerStateMachine(targetState: .clientClosedServerIdle) XCTAssertThrowsError( ofType: RPCError.self, @@ -928,16 +905,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testSendMessageWhenClientClosedAndServerOpen() { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - - // Open server - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - - // Close client - XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + var stateMachine = makeServerStateMachine(targetState: .clientClosedServerOpen) // Try sending a message: even though client is closed, we should send it // because it may be expecting a response. @@ -945,19 +913,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testSendMessageWhenClientClosedAndServerClosed() { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - - // Open server - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - - // Close client - XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) - - // Close server - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + var stateMachine = makeServerStateMachine(targetState: .clientClosedServerClosed) // Try sending another message: it should fail XCTAssertThrowsError( @@ -972,7 +928,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // - MARK: Send Status and Trailers func testSendStatusAndTrailersWhenClientIdleAndServerIdle() { - var stateMachine = makeServerStateMachine() + var stateMachine = makeServerStateMachine(targetState: .clientIdleServerIdle) XCTAssertThrowsError( ofType: RPCError.self, @@ -988,10 +944,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testSendStatusAndTrailersWhenClientOpenAndServerIdle() { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) + var stateMachine = makeServerStateMachine(targetState: .clientOpenServerIdle) XCTAssertThrowsError( ofType: RPCError.self, @@ -1007,13 +960,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testSendStatusAndTrailersWhenClientOpenAndServerOpen() { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - - // Open server - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + var stateMachine = makeServerStateMachine(targetState: .clientOpenServerOpen) XCTAssertNoThrow( try stateMachine.send( @@ -1034,16 +981,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testSendStatusAndTrailersWhenClientOpenAndServerClosed() { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - - // Open server - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - - // Close server - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + var stateMachine = makeServerStateMachine(targetState: .clientOpenServerClosed) XCTAssertThrowsError( ofType: RPCError.self, @@ -1059,13 +997,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testSendStatusAndTrailersWhenClientClosedAndServerIdle() { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - - // Close client - XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + var stateMachine = makeServerStateMachine(targetState: .clientClosedServerIdle) XCTAssertThrowsError( ofType: RPCError.self, @@ -1081,16 +1013,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testSendStatusAndTrailersWhenClientClosedAndServerOpen() { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - - // Open server - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - - // Close client - XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + var stateMachine = makeServerStateMachine(targetState: .clientClosedServerOpen) // Client is closed but may still be awaiting response, so we should be able to send it. XCTAssertNoThrow( @@ -1103,19 +1026,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testSendStatusAndTrailersWhenClientClosedAndServerClosed() { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - - // Open server - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - - // Close client - XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) - - // Close server - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + var stateMachine = makeServerStateMachine(targetState: .clientClosedServerClosed) XCTAssertThrowsError( ofType: RPCError.self, @@ -1133,9 +1044,9 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // - MARK: Receive metadata func testReceiveMetadataWhenClientIdleAndServerIdle() throws { - var stateMachine = makeServerStateMachine() + var stateMachine = makeServerStateMachine(targetState: .clientIdleServerIdle) - let action = try stateMachine.receive(metadata: .receivedHeaders, endStream: false) + let action = try stateMachine.receive(metadata: .clientInitialMetadata, endStream: false) guard case .receivedMetadata(let metadata) = action else { XCTFail("Expected action to be doNothing") return @@ -1145,14 +1056,14 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testReceiveMetadataWhenClientIdleAndServerIdle_WithEndStream() { - var stateMachine = makeServerStateMachine() + var stateMachine = makeServerStateMachine(targetState: .clientIdleServerIdle) // If endStream is set, we should fail, because the client can only close by // sending a message with endStream set. If they send metadata it has to be // to open the stream (initial metadata). XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.receive(metadata: .receivedHeaders, endStream: true) + try stateMachine.receive(metadata: .clientInitialMetadata, endStream: true) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual( @@ -1166,7 +1077,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testReceiveMetadataWhenClientIdleAndServerIdle_MissingContentType() throws { - var stateMachine = makeServerStateMachine() + var stateMachine = makeServerStateMachine(targetState: .clientIdleServerIdle) let action = try stateMachine.receive( metadata: .receivedHeadersWithoutContentType, @@ -1183,7 +1094,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testReceiveMetadataWhenClientIdleAndServerIdle_InvalidContentType() throws { - var stateMachine = makeServerStateMachine() + var stateMachine = makeServerStateMachine(targetState: .clientIdleServerIdle) let action = try stateMachine.receive( metadata: .receivedHeadersWithInvalidContentType, @@ -1200,7 +1111,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testReceiveMetadataWhenClientIdleAndServerIdle_MissingPath() throws { - var stateMachine = makeServerStateMachine() + var stateMachine = makeServerStateMachine(targetState: .clientIdleServerIdle) let action = try stateMachine.receive( metadata: .receivedHeadersWithoutEndpoint, @@ -1217,36 +1128,42 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertEqual(trailers.grpcStatusMessage, "No :path header has been set.") } - func testReceiveMetadataWhenClientIdleAndServerIdle_Encoding() { - var noCompressionStateMachine = makeServerStateMachine() + func testReceiveMetadataWhenClientIdleAndServerIdle_ServerUnsupportedEncoding() throws { + var stateMachine = makeServerStateMachine(targetState: .clientIdleServerIdle) - // Try opening client if no compression has been configured in the server: - // should fail. - XCTAssertThrowsError( - ofType: RPCError.self, - try noCompressionStateMachine.receive( - metadata: .receivedHeadersWithDeflateCompression, - endStream: false - ) - ) { error in - XCTAssertEqual(error.code, .unimplemented) - XCTAssertEqual(error.message, "Compression is not supported") + // Try opening client with a compression algorithm that is not accepted + // by the server. + let action = try stateMachine.receive( + metadata: .clientInitialMetadataWithGzipCompression, + endStream: false + ) + + guard case .rejectRPC(let trailers) = action else { + XCTFail("RPC should have been rejected.") + return } - - var stateMachine = makeServerStateMachineWithCompression() - //TODO: add tests for encoding validation + + XCTAssertEqual(trailers.count, 3) + XCTAssertEqual(trailers.grpcStatus, .unimplemented) + XCTAssertEqual( + trailers.grpcStatusMessage, + """ + gzip compression is not supported; \ + supported algorithms are listed in grpc-accept-encoding + """ + ) + XCTAssertEqual(trailers.acceptedEncodings, [.deflate]) } + + //TODO: add more encoding-related validation tests (for both client and server) func testReceiveMetadataWhenClientOpenAndServerIdle() throws { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) + var stateMachine = makeServerStateMachine(targetState: .clientOpenServerIdle) // Try receiving initial metadata again - should fail XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.receive(metadata: .receivedHeaders, endStream: false) + try stateMachine.receive(metadata: .clientInitialMetadata, endStream: false) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client shouldn't have sent metadata twice.") @@ -1254,17 +1171,11 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testReceiveMetadataWhenClientOpenAndServerOpen() throws { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - - // Open server - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + var stateMachine = makeServerStateMachine(targetState: .clientOpenServerOpen) XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.receive(metadata: .receivedHeaders, endStream: false) + try stateMachine.receive(metadata: .clientInitialMetadata, endStream: false) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client shouldn't have sent metadata twice.") @@ -1272,20 +1183,11 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testReceiveMetadataWhenClientOpenAndServerClosed() { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - - // Open server - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - - // Close server - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + var stateMachine = makeServerStateMachine(targetState: .clientOpenServerClosed) XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.receive(metadata: .receivedHeaders, endStream: false) + try stateMachine.receive(metadata: .clientInitialMetadata, endStream: false) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client shouldn't have sent metadata twice.") @@ -1293,17 +1195,11 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testReceiveMetadataWhenClientClosedAndServerIdle() { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - - // Close client - XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + var stateMachine = makeServerStateMachine(targetState: .clientClosedServerIdle) XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.receive(metadata: .receivedHeaders, endStream: false) + try stateMachine.receive(metadata: .clientInitialMetadata, endStream: false) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't have sent metadata if closed.") @@ -1311,20 +1207,11 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testReceiveMetadataWhenClientClosedAndServerOpen() { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - - // Open server - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - - // Close client - XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + var stateMachine = makeServerStateMachine(targetState: .clientClosedServerOpen) XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.receive(metadata: .receivedHeaders, endStream: false) + try stateMachine.receive(metadata: .clientInitialMetadata, endStream: false) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't have sent metadata if closed.") @@ -1332,23 +1219,11 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testReceiveMetadataWhenClientClosedAndServerClosed() { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - - // Open server - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - - // Close client - XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) - - // Close server - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + var stateMachine = makeServerStateMachine(targetState: .clientClosedServerClosed) XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.receive(metadata: .receivedHeaders, endStream: false) + try stateMachine.receive(metadata: .clientInitialMetadata, endStream: false) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't have sent metadata if closed.") @@ -1358,7 +1233,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // - MARK: Receive message func testReceiveMessageWhenClientIdleAndServerIdle() { - var stateMachine = makeServerStateMachine() + var stateMachine = makeServerStateMachine(targetState: .clientIdleServerIdle) XCTAssertThrowsError( ofType: RPCError.self, @@ -1370,10 +1245,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testReceiveMessageWhenClientOpenAndServerIdle() { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) + var stateMachine = makeServerStateMachine(targetState: .clientOpenServerIdle) // Receive messages successfully: the second one should close client. XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: false)) @@ -1390,13 +1262,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testReceiveMessageWhenClientOpenAndServerOpen() throws { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - - // Open server - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + var stateMachine = makeServerStateMachine(targetState: .clientOpenServerOpen) // Receive messages successfully: the second one should close client. XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: false)) @@ -1413,29 +1279,14 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testReceiveMessageWhenClientOpenAndServerClosed() { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - - // Open server - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - - // Close server - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + var stateMachine = makeServerStateMachine(targetState: .clientOpenServerClosed) // Client is not done sending request, don't fail. XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: false)) } func testReceiveMessageWhenClientClosedAndServerIdle() { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - - // Close client - XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + var stateMachine = makeServerStateMachine(targetState: .clientClosedServerIdle) XCTAssertThrowsError( ofType: RPCError.self, @@ -1447,16 +1298,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testReceiveMessageWhenClientClosedAndServerOpen() { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - - // Open server - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - - // Close client - XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + var stateMachine = makeServerStateMachine(targetState: .clientClosedServerOpen) XCTAssertThrowsError( ofType: RPCError.self, @@ -1468,19 +1310,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testReceiveMessageWhenClientClosedAndServerClosed() { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - - // Open server - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - - // Close client - XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) - - // Close server - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + var stateMachine = makeServerStateMachine(targetState: .clientClosedServerClosed) XCTAssertThrowsError( ofType: RPCError.self, @@ -1494,7 +1324,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // - MARK: Next outbound message func testNextOutboundMessageWhenClientIdleAndServerIdle() { - var stateMachine = makeServerStateMachine() + var stateMachine = makeServerStateMachine(targetState: .clientIdleServerIdle) XCTAssertThrowsError( ofType: RPCError.self, @@ -1506,10 +1336,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testNextOutboundMessageWhenClientOpenAndServerIdle() throws { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) + var stateMachine = makeServerStateMachine(targetState: .clientOpenServerIdle) XCTAssertThrowsError( ofType: RPCError.self, @@ -1521,10 +1348,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testNextOutboundMessageWhenClientOpenAndServerIdle_WithCompression() throws { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) + var stateMachine = makeServerStateMachine(targetState: .clientOpenServerIdle) XCTAssertThrowsError( ofType: RPCError.self, @@ -1536,13 +1360,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testNextOutboundMessageWhenClientOpenAndServerOpen() throws { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - - // Open server - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + var stateMachine = makeServerStateMachine(targetState: .clientOpenServerOpen) XCTAssertNil(try stateMachine.nextOutboundMessage()) @@ -1561,15 +1379,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testNextOutboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { - var stateMachine = makeServerStateMachineWithCompression() - - // Open client - XCTAssertNoThrow( - try stateMachine.receive(metadata: .receivedHeadersWithDeflateCompression, endStream: false) - ) - - // Open server - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + var stateMachine = makeServerStateMachine(targetState: .clientOpenServerOpen, compressionEnabled: true) XCTAssertNil(try stateMachine.nextOutboundMessage()) @@ -1588,13 +1398,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testNextOutboundMessageWhenClientOpenAndServerClosed() throws { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - - // Open server - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + var stateMachine = makeServerStateMachine(targetState: .clientOpenServerOpen) // Send message and close server XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: true)) @@ -1612,13 +1416,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testNextOutboundMessageWhenClientClosedAndServerIdle() throws { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - - // Close client - XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + var stateMachine = makeServerStateMachine(targetState: .clientClosedServerIdle) XCTAssertThrowsError( ofType: RPCError.self, @@ -1630,13 +1428,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testNextOutboundMessageWhenClientClosedAndServerOpen() throws { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - - // Open server - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + var stateMachine = makeServerStateMachine(targetState: .clientOpenServerOpen) // Send a message XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) @@ -1666,16 +1458,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testNextOutboundMessageWhenClientClosedAndServerClosed() throws { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - - // Open server - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - - // Close client - XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + var stateMachine = makeServerStateMachine(targetState: .clientClosedServerOpen) // Send a message and close server XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: true)) @@ -1698,27 +1481,18 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // - MARK: Next inbound message func testNextInboundMessageWhenClientIdleAndServerIdle() { - var stateMachine = makeServerStateMachine() + var stateMachine = makeServerStateMachine(targetState: .clientIdleServerIdle) XCTAssertNil(stateMachine.nextInboundMessage()) } func testNextInboundMessageWhenClientOpenAndServerIdle() { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) + var stateMachine = makeServerStateMachine(targetState: .clientOpenServerIdle) XCTAssertNil(stateMachine.nextInboundMessage()) } func testNextInboundMessageWhenClientOpenAndServerOpen() throws { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - - // Open server - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + var stateMachine = makeServerStateMachine(targetState: .clientOpenServerOpen) let receivedBytes = ByteBuffer(bytes: [ 0, // compression flag: unset @@ -1734,15 +1508,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testNextInboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { - var stateMachine = makeServerStateMachineWithCompression() - - // Open client - XCTAssertNoThrow( - try stateMachine.receive(metadata: .receivedHeadersWithDeflateCompression, endStream: false) - ) - - // Open server - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + var stateMachine = makeServerStateMachine(targetState: .clientOpenServerOpen, compressionEnabled: true) let originalMessage = [UInt8]([42, 42, 43, 43]) var framer = GRPCMessageFramer() @@ -1760,13 +1526,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testNextInboundMessageWhenClientOpenAndServerClosed() throws { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - - // Open server - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + var stateMachine = makeServerStateMachine(targetState: .clientOpenServerOpen) let receivedBytes = ByteBuffer(bytes: [ 0, // compression flag: unset @@ -1785,25 +1545,13 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testNextInboundMessageWhenClientClosedAndServerIdle() { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - - // Close client - XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + var stateMachine = makeServerStateMachine(targetState: .clientClosedServerIdle) XCTAssertNil(stateMachine.nextInboundMessage()) } func testNextInboundMessageWhenClientClosedAndServerOpen() throws { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - - // Open server - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + var stateMachine = makeServerStateMachine(targetState: .clientOpenServerOpen) let receivedBytes = ByteBuffer(bytes: [ 0, // compression flag: unset @@ -1824,13 +1572,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testNextInboundMessageWhenClientClosedAndServerClosed() throws { - var stateMachine = makeServerStateMachine() - - // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: .receivedHeaders, endStream: false)) - - // Open server - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + var stateMachine = makeServerStateMachine(targetState: .clientOpenServerOpen) let receivedBytes = ByteBuffer(bytes: [ 0, // compression flag: unset From d17103d726e9ac2e5460dc292afb7a0992e57a2f Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Tue, 20 Feb 2024 16:06:00 +0000 Subject: [PATCH 21/51] Add OnNextOutboundMessage --- .../GRPCStreamStateMachine.swift | 43 +++++++++++++------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index a07bd57a5..a48ad880c 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -219,7 +219,7 @@ enum GRPCStreamStateMachineState { let deframer: NIOSingleStepByteToMessageProcessor var decompressor: Zlib.Decompressor? - + var inboundMessageBuffer: OneOrManyQueue<[UInt8]> init(previousState: ClientOpenServerOpenState) { @@ -264,6 +264,7 @@ enum GRPCStreamStateMachineState { // the client. var framer: GRPCMessageFramer var compressor: Zlib.Compressor? + // These are already deframed, so we don't need the deframer anymore. var inboundMessageBuffer: OneOrManyQueue<[UInt8]> @@ -357,7 +358,18 @@ struct GRPCStreamStateMachine { } } - mutating func nextOutboundMessage() throws -> ByteBuffer? { + /// The result of requesting the next outbound message. + enum OnNextOutboundMessage { + /// Either the receiving party is closed, so we shouldn't send any more messages; or the sender is done + /// writing messages (i.e. we are now closed). + case noMoreMessages + /// There isn't a message ready to be sent, but we could still receive more, so keep trying. + case awaitMoreMessages + /// A message is ready to be sent. + case sendMessage(ByteBuffer) + } + + mutating func nextOutboundMessage() throws -> OnNextOutboundMessage { switch self.configuration { case .client: return try self.clientNextOutboundMessage() @@ -474,31 +486,36 @@ extension GRPCStreamStateMachine { ) } } + /// Returns the client's next request to the server. /// - Returns: The request to be made to the server. - private mutating func clientNextOutboundMessage() throws -> ByteBuffer? { + private mutating func clientNextOutboundMessage() throws -> OnNextOutboundMessage { switch self.state { case .clientIdleServerIdle: throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client is not open yet.") case .clientOpenServerIdle(var state): let request = try state.framer.next(compressor: state.compressor) self.state = .clientOpenServerIdle(state) - return request + return request != nil ? .sendMessage(request!) : .awaitMoreMessages case .clientOpenServerOpen(var state): let request = try state.framer.next(compressor: state.compressor) self.state = .clientOpenServerOpen(state) - return request + return request != nil ? .sendMessage(request!) : .awaitMoreMessages case .clientClosedServerIdle(var state): let request = try state.framer.next(compressor: state.compressor) self.state = .clientClosedServerIdle(state) - return request + // If the client is closed and there is no message to be sent, then we + // are done sending messages, as we cannot call send(message:) anymore. + return request != nil ? .sendMessage(request!) : .noMoreMessages case .clientClosedServerOpen(var state): let request = try state.framer.next(compressor: state.compressor) self.state = .clientClosedServerOpen(state) - return request + // If the client is closed and there is no message to be sent, then we + // are done sending messages, as we cannot call send(message:) anymore. + return request != nil ? .sendMessage(request!) : .noMoreMessages case .clientOpenServerClosed, .clientClosedServerClosed: // Nothing to do if server is closed. - return nil + return .noMoreMessages } } @@ -1066,26 +1083,26 @@ extension GRPCStreamStateMachine { } } - private mutating func serverNextOutboundMessage() throws -> ByteBuffer? { + private mutating func serverNextOutboundMessage() throws -> OnNextOutboundMessage { switch self.state { case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server is not open yet.") case .clientOpenServerOpen(var state): let response = try state.framer.next(compressor: state.compressor) self.state = .clientOpenServerOpen(state) - return response + return response != nil ? .sendMessage(response!) : .awaitMoreMessages case .clientClosedServerOpen(var state): let response = try state.framer.next(compressor: state.compressor) self.state = .clientClosedServerOpen(state) - return response + return response != nil ? .sendMessage(response!) : .awaitMoreMessages case .clientOpenServerClosed(var state): let response = try state.framer.next(compressor: state.compressor) self.state = .clientOpenServerClosed(state) - return response + return response != nil ? .sendMessage(response!) : .noMoreMessages case .clientClosedServerClosed(var state): let response = try state.framer.next(compressor: state.compressor) self.state = .clientClosedServerClosed(state) - return response + return response != nil ? .sendMessage(response!) : .noMoreMessages } } From 469806a84cdc14fd087cecebbbda24061fdf46b3 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Tue, 20 Feb 2024 16:46:57 +0000 Subject: [PATCH 22/51] End (de)compressor at the right time --- .../GRPCStreamStateMachine.swift | 58 ++++++++++++++----- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index a48ad880c..c34ee9b36 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -272,18 +272,21 @@ enum GRPCStreamStateMachineState { self.framer = previousState.framer self.compressor = previousState.compressor self.inboundMessageBuffer = previousState.inboundMessageBuffer + previousState.decompressor?.end() } init(previousState: ClientClosedServerIdleState) { self.framer = previousState.framer self.compressor = previousState.compressor self.inboundMessageBuffer = previousState.inboundMessageBuffer + previousState.decompressor?.end() } init(previousState: ClientOpenServerClosedState) { self.framer = previousState.framer self.compressor = previousState.compressor self.inboundMessageBuffer = previousState.inboundMessageBuffer + previousState.decompressor?.end() } } } @@ -504,17 +507,36 @@ extension GRPCStreamStateMachine { case .clientClosedServerIdle(var state): let request = try state.framer.next(compressor: state.compressor) self.state = .clientClosedServerIdle(state) - // If the client is closed and there is no message to be sent, then we - // are done sending messages, as we cannot call send(message:) anymore. - return request != nil ? .sendMessage(request!) : .noMoreMessages + if let request { + return .sendMessage(request) + } else { + // If the client is closed and there is no message to be sent, then we + // are done sending messages, as we cannot call send(message:) anymore. + // There are no more messages to be sent, so we can end the compressor. + state.compressor?.end() + return .noMoreMessages + } case .clientClosedServerOpen(var state): let request = try state.framer.next(compressor: state.compressor) self.state = .clientClosedServerOpen(state) - // If the client is closed and there is no message to be sent, then we - // are done sending messages, as we cannot call send(message:) anymore. - return request != nil ? .sendMessage(request!) : .noMoreMessages - case .clientOpenServerClosed, .clientClosedServerClosed: - // Nothing to do if server is closed. + if let request { + return .sendMessage(request) + } else { + // If the client is closed and there is no message to be sent, then we + // are done sending messages, as we cannot call send(message:) anymore. + // There are no more messages to be sent, so we can end the compressor. + state.compressor?.end() + return .noMoreMessages + } + case .clientOpenServerClosed(let state): + // No point in sending any more requests if the server is closed: + // we end the compressor. + state.compressor?.end() + return .noMoreMessages + case .clientClosedServerClosed(let state): + // No point in sending any more requests if the server is closed: + // we end the compressor. + state.compressor?.end() return .noMoreMessages } } @@ -660,8 +682,6 @@ extension GRPCStreamStateMachine { return .receivedMetadata(Metadata(headers: metadata)) case .clientClosedServerOpen(let state): if endStream { - state.compressor?.end() - state.decompressor?.end() self.state = .clientClosedServerClosed(.init(previousState: state)) } else { // This state is valid: server can send trailing metadata without grpc-status @@ -913,8 +933,6 @@ extension GRPCStreamStateMachine { self.state = .clientOpenServerClosed(.init(previousState: state)) return self.makeTrailers(status: status, customMetadata: metadata, trailersOnly: trailersOnly) case .clientClosedServerOpen(let state): - state.compressor?.end() - state.decompressor?.end() self.state = .clientClosedServerClosed(.init(previousState: state)) return self.makeTrailers(status: status, customMetadata: metadata, trailersOnly: trailersOnly) case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: @@ -1098,11 +1116,23 @@ extension GRPCStreamStateMachine { case .clientOpenServerClosed(var state): let response = try state.framer.next(compressor: state.compressor) self.state = .clientOpenServerClosed(state) - return response != nil ? .sendMessage(response!) : .noMoreMessages + if let response { + return .sendMessage(response) + } else { + // There are no more messages to be sent, so we can end the compressor. + state.compressor?.end() + return .noMoreMessages + } case .clientClosedServerClosed(var state): let response = try state.framer.next(compressor: state.compressor) self.state = .clientClosedServerClosed(state) - return response != nil ? .sendMessage(response!) : .noMoreMessages + if let response { + return .sendMessage(response) + } else { + // There are no more messages to be sent, so we can end the compressor. + state.compressor?.end() + return .noMoreMessages + } } } From d256376d5332a150fa9ee0507c99c6779ae99003 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Tue, 20 Feb 2024 17:45:18 +0000 Subject: [PATCH 23/51] Fix tests --- .../GRPCStreamStateMachineTests.swift | 171 ++++++++++++++---- 1 file changed, 140 insertions(+), 31 deletions(-) diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index 3326c8002..3d508f4c9 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -458,10 +458,19 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { for targetState in [TargetStateMachineState.clientOpenServerIdle, .clientOpenServerOpen] { var stateMachine = self.makeClientStateMachine(targetState: targetState) - XCTAssertNil(try stateMachine.nextOutboundMessage()) + var action = try stateMachine.nextOutboundMessage() + guard case .awaitMoreMessages = action else { + XCTFail("Expected action to be sendMessage but was \(action)") + return + } XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) - let request = try XCTUnwrap(stateMachine.nextOutboundMessage()) + + action = try stateMachine.nextOutboundMessage() + guard case .sendMessage(let request) = action else { + XCTFail("Expected action to be sendMessage but was \(action)") + return + } let expectedBytes: [UInt8] = [ 0, // compression flag: unset @@ -471,7 +480,11 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertEqual(Array(buffer: request), expectedBytes) // And then make sure that nothing else is returned anymore - XCTAssertNil(try stateMachine.nextOutboundMessage()) + action = try stateMachine.nextOutboundMessage() + guard case .awaitMoreMessages = action else { + XCTFail("Expected action to be sendMessage but was \(action)") + return + } } } @@ -481,11 +494,20 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { compressionEnabled: true ) - XCTAssertNil(try stateMachine.nextOutboundMessage()) + var action = try stateMachine.nextOutboundMessage() + guard case .awaitMoreMessages = action else { + XCTFail("Expected action to be awaitMoreMessages but was \(action)") + return + } let originalMessage = [UInt8]([42, 42, 43, 43]) XCTAssertNoThrow(try stateMachine.send(message: originalMessage, endStream: false)) - let request = try XCTUnwrap(stateMachine.nextOutboundMessage()) + + action = try stateMachine.nextOutboundMessage() + guard case .sendMessage(let request) = action else { + XCTFail("Expected action to be sendMessage but was \(action)") + return + } var framer = GRPCMessageFramer() let compressor = Zlib.Compressor(method: .deflate) @@ -503,11 +525,20 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { compressionEnabled: true ) - XCTAssertNil(try stateMachine.nextOutboundMessage()) + var action = try stateMachine.nextOutboundMessage() + guard case .awaitMoreMessages = action else { + XCTFail("Expected action to be awaitMoreMessages but was \(action)") + return + } let originalMessage = [UInt8]([42, 42, 43, 43]) XCTAssertNoThrow(try stateMachine.send(message: originalMessage, endStream: false)) - let request = try XCTUnwrap(stateMachine.nextOutboundMessage()) + + action = try stateMachine.nextOutboundMessage() + guard case .sendMessage(let request) = action else { + XCTFail("Expected action to be sendMessage but was \(action)") + return + } var framer = GRPCMessageFramer() let compressor = Zlib.Compressor(method: .deflate) @@ -522,13 +553,21 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { func testNextOutboundMessageWhenClientOpenAndServerClosed() throws { var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerClosed) - // No messages to send, so make sure nil is returned - XCTAssertNil(try stateMachine.nextOutboundMessage()) + // No more messages to send + var action = try stateMachine.nextOutboundMessage() + guard case .noMoreMessages = action else { + XCTFail("Expected action to be .noMoreMessages but was \(action)") + return + } - // Queue a message, but assert the next outbound message is nil nevertheless, + // Queue a message, but assert the action is .noMoreMessages nevertheless, // because the server is closed. XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) - XCTAssertNil(try stateMachine.nextOutboundMessage()) + action = try stateMachine.nextOutboundMessage() + guard case .noMoreMessages = action else { + XCTFail("Expected action to be .noMoreMessages but was \(action)") + return + } } func testNextOutboundMessageWhenClientClosedAndServerIdle() throws { @@ -539,7 +578,12 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Make sure that getting the next outbound message _does_ return the message // we have enqueued. - let request = try XCTUnwrap(stateMachine.nextOutboundMessage()) + var action = try stateMachine.nextOutboundMessage() + guard case .sendMessage(let request) = action else { + XCTFail("Expected action to be sendMessage but was \(action)") + return + } + let expectedBytes: [UInt8] = [ 0, // compression flag: unset 0, 0, 0, 2, // message length: 2 bytes @@ -548,7 +592,11 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertEqual(Array(buffer: request), expectedBytes) // And then make sure that nothing else is returned anymore - XCTAssertNil(try stateMachine.nextOutboundMessage()) + action = try stateMachine.nextOutboundMessage() + guard case .noMoreMessages = action else { + XCTFail("Expected action to be noMoreMessages but was \(action)") + return + } } func testNextOutboundMessageWhenClientClosedAndServerOpen() throws { @@ -559,7 +607,12 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Make sure that getting the next outbound message _does_ return the message // we have enqueued. - let request = try XCTUnwrap(stateMachine.nextOutboundMessage()) + var action = try stateMachine.nextOutboundMessage() + guard case .sendMessage(let request) = action else { + XCTFail("Expected action to be sendMessage but was \(action)") + return + } + let expectedBytes: [UInt8] = [ 0, // compression flag: unset 0, 0, 0, 2, // message length: 2 bytes @@ -568,10 +621,14 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertEqual(Array(buffer: request), expectedBytes) // And then make sure that nothing else is returned anymore - XCTAssertNil(try stateMachine.nextOutboundMessage()) + action = try stateMachine.nextOutboundMessage() + guard case .noMoreMessages = action else { + XCTFail("Expected action to be noMoreMessages but was \(action)") + return + } } - func testNextOutboundMessageWhenClientClosedAndServerClosed() { + func testNextOutboundMessageWhenClientClosedAndServerClosed() throws { var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerOpen) // Send a message XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) @@ -584,7 +641,11 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Even though we have enqueued a message, don't send it, because the server // is closed. - XCTAssertNil(try stateMachine.nextOutboundMessage()) + var action = try stateMachine.nextOutboundMessage() + guard case .noMoreMessages = action else { + XCTFail("Expected action to be noMoreMessages but was \(action)") + return + } } // - MARK: Next inbound message @@ -1362,30 +1423,52 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { func testNextOutboundMessageWhenClientOpenAndServerOpen() throws { var stateMachine = makeServerStateMachine(targetState: .clientOpenServerOpen) - XCTAssertNil(try stateMachine.nextOutboundMessage()) + var action = try stateMachine.nextOutboundMessage() + guard case .awaitMoreMessages = action else { + XCTFail("Expected action to be awaitMoreMessages but was \(action)") + return + } XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) - let request = try XCTUnwrap(stateMachine.nextOutboundMessage()) + + action = try stateMachine.nextOutboundMessage() + guard case .sendMessage(let response) = action else { + XCTFail("Expected action to be sendMessage but was \(action)") + return + } let expectedBytes: [UInt8] = [ 0, // compression flag: unset 0, 0, 0, 2, // message length: 2 bytes 42, 42, // original message ] - XCTAssertEqual(Array(buffer: request), expectedBytes) + XCTAssertEqual(Array(buffer: response), expectedBytes) // And then make sure that nothing else is returned anymore - XCTAssertNil(try stateMachine.nextOutboundMessage()) + action = try stateMachine.nextOutboundMessage() + guard case .awaitMoreMessages = action else { + XCTFail("Expected action to be awaitMoreMessages but was \(action)") + return + } } func testNextOutboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { var stateMachine = makeServerStateMachine(targetState: .clientOpenServerOpen, compressionEnabled: true) - XCTAssertNil(try stateMachine.nextOutboundMessage()) + var action = try stateMachine.nextOutboundMessage() + guard case .awaitMoreMessages = action else { + XCTFail("Expected action to be awaitMoreMessages but was \(action)") + return + } let originalMessage = [UInt8]([42, 42, 43, 43]) XCTAssertNoThrow(try stateMachine.send(message: originalMessage, endStream: false)) - let request = try XCTUnwrap(stateMachine.nextOutboundMessage()) + + action = try stateMachine.nextOutboundMessage() + guard case .sendMessage(let response) = action else { + XCTFail("Expected action to be sendMessage but was \(action)") + return + } var framer = GRPCMessageFramer() let compressor = Zlib.Compressor(method: .deflate) @@ -1394,7 +1477,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { let framedMessage = try XCTUnwrap(framer.next(compressor: compressor)) let expectedBytes = Array(buffer: framedMessage) - XCTAssertEqual(Array(buffer: request), expectedBytes) + XCTAssertEqual(Array(buffer: response), expectedBytes) } func testNextOutboundMessageWhenClientOpenAndServerClosed() throws { @@ -1402,7 +1485,12 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Send message and close server XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: true)) - let response = try XCTUnwrap(stateMachine.nextOutboundMessage()) + + var action = try stateMachine.nextOutboundMessage() + guard case .sendMessage(let response) = action else { + XCTFail("Expected action to be sendMessage but was \(action)") + return + } let expectedBytes: [UInt8] = [ 0, // compression flag: unset @@ -1412,7 +1500,11 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertEqual(Array(buffer: response), expectedBytes) // And then make sure that nothing else is returned anymore - XCTAssertNil(try stateMachine.nextOutboundMessage()) + action = try stateMachine.nextOutboundMessage() + guard case .noMoreMessages = action else { + XCTFail("Expected action to be noMoreMessages but was \(action)") + return + } } func testNextOutboundMessageWhenClientClosedAndServerIdle() throws { @@ -1441,7 +1533,12 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Make sure that getting the next outbound message _does_ return the message // we have enqueued. - let request = try XCTUnwrap(stateMachine.nextOutboundMessage()) + var action = try stateMachine.nextOutboundMessage() + guard case .sendMessage(let response) = action else { + XCTFail("Expected action to be sendMessage but was \(action)") + return + } + let expectedBytes: [UInt8] = [ 0, // compression flag: unset 0, 0, 0, 2, // message length: 2 bytes @@ -1451,10 +1548,14 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { 0, 0, 0, 2, // message length: 2 bytes 43, 43, // original message ] - XCTAssertEqual(Array(buffer: request), expectedBytes) + XCTAssertEqual(Array(buffer: response), expectedBytes) // And then make sure that nothing else is returned anymore - XCTAssertNil(try stateMachine.nextOutboundMessage()) + action = try stateMachine.nextOutboundMessage() + guard case .awaitMoreMessages = action else { + XCTFail("Expected action to be awaitMoreMessages but was \(action)") + return + } } func testNextOutboundMessageWhenClientClosedAndServerClosed() throws { @@ -1465,7 +1566,11 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // We have enqueued a message, make sure we return it even though server is closed, // because we haven't yet drained all of the pending messages. - let response = try XCTUnwrap(stateMachine.nextOutboundMessage()) + var action = try stateMachine.nextOutboundMessage() + guard case .sendMessage(let response) = action else { + XCTFail("Expected action to be sendMessage but was \(action)") + return + } let expectedBytes: [UInt8] = [ 0, // compression flag: unset @@ -1475,7 +1580,11 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertEqual(Array(buffer: response), expectedBytes) // And then make sure that nothing else is returned anymore - XCTAssertNil(try stateMachine.nextOutboundMessage()) + action = try stateMachine.nextOutboundMessage() + guard case .noMoreMessages = action else { + XCTFail("Expected action to be noMoreMessages but was \(action)") + return + } } // - MARK: Next inbound message From 4c91296afea3aba93444c8973742f675f2b7b096 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Wed, 21 Feb 2024 10:24:43 +0000 Subject: [PATCH 24/51] Small test refactor --- .../GRPCStreamStateMachine.swift | 25 +- .../GRPCStreamStateMachineTests.swift | 269 +++++------------- 2 files changed, 82 insertions(+), 212 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index c34ee9b36..163ec3544 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -19,18 +19,6 @@ import NIOCore import NIOHPACK import NIOHTTP1 -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -enum OnMetadataReceived { - case receivedMetadata(Metadata) - - // Client-specific actions - case failedRequest(Status) - case doNothing - - // Server-specific actions - case rejectRPC(trailers: HPACKHeaders) -} - enum Scheme: String { case http case https @@ -339,6 +327,17 @@ struct GRPCStreamStateMachine { } } + enum OnMetadataReceived: Equatable { + case receivedMetadata(Metadata) + + // Client-specific actions + case failedRequest(Status) + case doNothing + + // Server-specific actions + case rejectRPC(trailers: HPACKHeaders) + } + mutating func receive(metadata: HPACKHeaders, endStream: Bool) throws -> OnMetadataReceived { switch self.configuration { case .client: @@ -362,7 +361,7 @@ struct GRPCStreamStateMachine { } /// The result of requesting the next outbound message. - enum OnNextOutboundMessage { + enum OnNextOutboundMessage: Equatable { /// Either the receiving party is closed, so we shouldn't send any more messages; or the sender is done /// writing messages (i.e. we are now closed). case noMoreMessages diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index 3d508f4c9..c0fc5c97a 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -73,6 +73,14 @@ extension HPACKHeaders { ] } +fileprivate func assertRejectedRPC(_ action: GRPCStreamStateMachine.OnMetadataReceived, expression: (HPACKHeaders) -> Void) { + guard case .rejectRPC(let trailers) = action else { + XCTFail("RPC should have been rejected.") + return + } + expression(trailers) +} + final class GRPCStreamClientStateMachineTests: XCTestCase { private func makeClientStateMachine( targetState: TargetStateMachineState, @@ -273,10 +281,6 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { ], endStream: false ) - guard case .receivedMetadata(let customMetadata) = action else { - XCTFail("Expected action to be receivedMetadata but was \(action)") - return - } var expectedMetadata: Metadata = [ ":status": "200", @@ -285,7 +289,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { "custom": "123", ] expectedMetadata.addBinary([42, 43, 44], forKey: "custom-bin") - XCTAssertEqual(customMetadata, expectedMetadata) + XCTAssertEqual(action, .receivedMetadata(expectedMetadata)) } } @@ -339,10 +343,6 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { ], endStream: true ) - guard case .receivedMetadata(let customMetadata) = action else { - XCTFail("Expected action to be receivedMetadata but was \(action)") - return - } let expectedMetadata: Metadata = [ ":status": "200", @@ -350,7 +350,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { "grpc-encoding": "deflate", "custom": "123", ] - XCTAssertEqual(customMetadata, expectedMetadata) + XCTAssertEqual(action, .receivedMetadata(expectedMetadata)) } } @@ -458,33 +458,22 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { for targetState in [TargetStateMachineState.clientOpenServerIdle, .clientOpenServerOpen] { var stateMachine = self.makeClientStateMachine(targetState: targetState) - var action = try stateMachine.nextOutboundMessage() - guard case .awaitMoreMessages = action else { - XCTFail("Expected action to be sendMessage but was \(action)") - return - } + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) - action = try stateMachine.nextOutboundMessage() - guard case .sendMessage(let request) = action else { - XCTFail("Expected action to be sendMessage but was \(action)") - return - } - let expectedBytes: [UInt8] = [ 0, // compression flag: unset 0, 0, 0, 2, // message length: 2 bytes 42, 42, // original message ] - XCTAssertEqual(Array(buffer: request), expectedBytes) + XCTAssertEqual( + try stateMachine.nextOutboundMessage(), + .sendMessage(ByteBuffer(bytes: expectedBytes)) + ) // And then make sure that nothing else is returned anymore - action = try stateMachine.nextOutboundMessage() - guard case .awaitMoreMessages = action else { - XCTFail("Expected action to be sendMessage but was \(action)") - return - } + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) } } @@ -494,29 +483,19 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { compressionEnabled: true ) - var action = try stateMachine.nextOutboundMessage() - guard case .awaitMoreMessages = action else { - XCTFail("Expected action to be awaitMoreMessages but was \(action)") - return - } + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) let originalMessage = [UInt8]([42, 42, 43, 43]) XCTAssertNoThrow(try stateMachine.send(message: originalMessage, endStream: false)) - action = try stateMachine.nextOutboundMessage() - guard case .sendMessage(let request) = action else { - XCTFail("Expected action to be sendMessage but was \(action)") - return - } - + let request = try stateMachine.nextOutboundMessage() var framer = GRPCMessageFramer() let compressor = Zlib.Compressor(method: .deflate) defer { compressor.end() } framer.append(originalMessage) let framedMessage = try XCTUnwrap(framer.next(compressor: compressor)) - let expectedBytes = Array(buffer: framedMessage) - XCTAssertEqual(Array(buffer: request), expectedBytes) + XCTAssertEqual(request, .sendMessage(framedMessage)) } func testNextOutboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { @@ -525,49 +504,31 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { compressionEnabled: true ) - var action = try stateMachine.nextOutboundMessage() - guard case .awaitMoreMessages = action else { - XCTFail("Expected action to be awaitMoreMessages but was \(action)") - return - } + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) let originalMessage = [UInt8]([42, 42, 43, 43]) XCTAssertNoThrow(try stateMachine.send(message: originalMessage, endStream: false)) - action = try stateMachine.nextOutboundMessage() - guard case .sendMessage(let request) = action else { - XCTFail("Expected action to be sendMessage but was \(action)") - return - } - + let request = try stateMachine.nextOutboundMessage() var framer = GRPCMessageFramer() let compressor = Zlib.Compressor(method: .deflate) defer { compressor.end() } framer.append(originalMessage) let framedMessage = try XCTUnwrap(framer.next(compressor: compressor)) - let expectedBytes = Array(buffer: framedMessage) - XCTAssertEqual(Array(buffer: request), expectedBytes) + XCTAssertEqual(request, .sendMessage(framedMessage)) } func testNextOutboundMessageWhenClientOpenAndServerClosed() throws { var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerClosed) // No more messages to send - var action = try stateMachine.nextOutboundMessage() - guard case .noMoreMessages = action else { - XCTFail("Expected action to be .noMoreMessages but was \(action)") - return - } + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) // Queue a message, but assert the action is .noMoreMessages nevertheless, // because the server is closed. XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) - action = try stateMachine.nextOutboundMessage() - guard case .noMoreMessages = action else { - XCTFail("Expected action to be .noMoreMessages but was \(action)") - return - } + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) } func testNextOutboundMessageWhenClientClosedAndServerIdle() throws { @@ -578,25 +539,16 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Make sure that getting the next outbound message _does_ return the message // we have enqueued. - var action = try stateMachine.nextOutboundMessage() - guard case .sendMessage(let request) = action else { - XCTFail("Expected action to be sendMessage but was \(action)") - return - } - + let request = try stateMachine.nextOutboundMessage() let expectedBytes: [UInt8] = [ 0, // compression flag: unset 0, 0, 0, 2, // message length: 2 bytes 42, 42, // original message ] - XCTAssertEqual(Array(buffer: request), expectedBytes) + XCTAssertEqual(request, .sendMessage(ByteBuffer(bytes: expectedBytes))) // And then make sure that nothing else is returned anymore - action = try stateMachine.nextOutboundMessage() - guard case .noMoreMessages = action else { - XCTFail("Expected action to be noMoreMessages but was \(action)") - return - } + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) } func testNextOutboundMessageWhenClientClosedAndServerOpen() throws { @@ -607,25 +559,16 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Make sure that getting the next outbound message _does_ return the message // we have enqueued. - var action = try stateMachine.nextOutboundMessage() - guard case .sendMessage(let request) = action else { - XCTFail("Expected action to be sendMessage but was \(action)") - return - } - + let request = try stateMachine.nextOutboundMessage() let expectedBytes: [UInt8] = [ 0, // compression flag: unset 0, 0, 0, 2, // message length: 2 bytes 42, 42, // original message ] - XCTAssertEqual(Array(buffer: request), expectedBytes) + XCTAssertEqual(request, .sendMessage(ByteBuffer(bytes: expectedBytes))) // And then make sure that nothing else is returned anymore - action = try stateMachine.nextOutboundMessage() - guard case .noMoreMessages = action else { - XCTFail("Expected action to be noMoreMessages but was \(action)") - return - } + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) } func testNextOutboundMessageWhenClientClosedAndServerClosed() throws { @@ -641,11 +584,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Even though we have enqueued a message, don't send it, because the server // is closed. - var action = try stateMachine.nextOutboundMessage() - guard case .noMoreMessages = action else { - XCTFail("Expected action to be noMoreMessages but was \(action)") - return - } + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) } // - MARK: Next inbound message @@ -1108,12 +1047,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine(targetState: .clientIdleServerIdle) let action = try stateMachine.receive(metadata: .clientInitialMetadata, endStream: false) - guard case .receivedMetadata(let metadata) = action else { - XCTFail("Expected action to be doNothing") - return - } - - XCTAssertEqual(metadata, [":path": "test/test", "content-type": "application/grpc"]) + XCTAssertEqual(action, .receivedMetadata([":path": "test/test", "content-type": "application/grpc"])) } func testReceiveMetadataWhenClientIdleAndServerIdle_WithEndStream() { @@ -1144,14 +1078,10 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { metadata: .receivedHeadersWithoutContentType, endStream: false ) - - guard case .rejectRPC(let trailers) = action else { - XCTFail("RPC should have been rejected.") - return + assertRejectedRPC(action) { trailers in + XCTAssertEqual(trailers.count, 1) + XCTAssertEqual(trailers.status, "415") } - - XCTAssertEqual(trailers.count, 1) - XCTAssertEqual(trailers.status, "415") } func testReceiveMetadataWhenClientIdleAndServerIdle_InvalidContentType() throws { @@ -1161,14 +1091,10 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { metadata: .receivedHeadersWithInvalidContentType, endStream: false ) - - guard case .rejectRPC(let trailers) = action else { - XCTFail("RPC should have been rejected.") - return + assertRejectedRPC(action) { trailers in + XCTAssertEqual(trailers.count, 1) + XCTAssertEqual(trailers.status, "415") } - - XCTAssertEqual(trailers.count, 1) - XCTAssertEqual(trailers.status, "415") } func testReceiveMetadataWhenClientIdleAndServerIdle_MissingPath() throws { @@ -1179,14 +1105,11 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { endStream: false ) - guard case .rejectRPC(let trailers) = action else { - XCTFail("RPC should have been rejected.") - return + assertRejectedRPC(action) { trailers in + XCTAssertEqual(trailers.count, 2) + XCTAssertEqual(trailers.grpcStatus, .unimplemented) + XCTAssertEqual(trailers.grpcStatusMessage, "No :path header has been set.") } - - XCTAssertEqual(trailers.count, 2) - XCTAssertEqual(trailers.grpcStatus, .unimplemented) - XCTAssertEqual(trailers.grpcStatusMessage, "No :path header has been set.") } func testReceiveMetadataWhenClientIdleAndServerIdle_ServerUnsupportedEncoding() throws { @@ -1199,21 +1122,18 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { endStream: false ) - guard case .rejectRPC(let trailers) = action else { - XCTFail("RPC should have been rejected.") - return + assertRejectedRPC(action) { trailers in + XCTAssertEqual(trailers.count, 3) + XCTAssertEqual(trailers.grpcStatus, .unimplemented) + XCTAssertEqual( + trailers.grpcStatusMessage, + """ + gzip compression is not supported; \ + supported algorithms are listed in grpc-accept-encoding + """ + ) + XCTAssertEqual(trailers.acceptedEncodings, [.deflate]) } - - XCTAssertEqual(trailers.count, 3) - XCTAssertEqual(trailers.grpcStatus, .unimplemented) - XCTAssertEqual( - trailers.grpcStatusMessage, - """ - gzip compression is not supported; \ - supported algorithms are listed in grpc-accept-encoding - """ - ) - XCTAssertEqual(trailers.acceptedEncodings, [.deflate]) } //TODO: add more encoding-related validation tests (for both client and server) @@ -1423,53 +1343,31 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { func testNextOutboundMessageWhenClientOpenAndServerOpen() throws { var stateMachine = makeServerStateMachine(targetState: .clientOpenServerOpen) - var action = try stateMachine.nextOutboundMessage() - guard case .awaitMoreMessages = action else { - XCTFail("Expected action to be awaitMoreMessages but was \(action)") - return - } + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) - action = try stateMachine.nextOutboundMessage() - guard case .sendMessage(let response) = action else { - XCTFail("Expected action to be sendMessage but was \(action)") - return - } - + let response = try stateMachine.nextOutboundMessage() let expectedBytes: [UInt8] = [ 0, // compression flag: unset 0, 0, 0, 2, // message length: 2 bytes 42, 42, // original message ] - XCTAssertEqual(Array(buffer: response), expectedBytes) + XCTAssertEqual(response, .sendMessage(ByteBuffer(bytes: expectedBytes))) - // And then make sure that nothing else is returned anymore - action = try stateMachine.nextOutboundMessage() - guard case .awaitMoreMessages = action else { - XCTFail("Expected action to be awaitMoreMessages but was \(action)") - return - } + // And then make sure that nothing else is returned + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) } func testNextOutboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { var stateMachine = makeServerStateMachine(targetState: .clientOpenServerOpen, compressionEnabled: true) - var action = try stateMachine.nextOutboundMessage() - guard case .awaitMoreMessages = action else { - XCTFail("Expected action to be awaitMoreMessages but was \(action)") - return - } + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) let originalMessage = [UInt8]([42, 42, 43, 43]) XCTAssertNoThrow(try stateMachine.send(message: originalMessage, endStream: false)) - action = try stateMachine.nextOutboundMessage() - guard case .sendMessage(let response) = action else { - XCTFail("Expected action to be sendMessage but was \(action)") - return - } - + let response = try stateMachine.nextOutboundMessage() var framer = GRPCMessageFramer() let compressor = Zlib.Compressor(method: .deflate) defer { compressor.end() } @@ -1477,7 +1375,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { let framedMessage = try XCTUnwrap(framer.next(compressor: compressor)) let expectedBytes = Array(buffer: framedMessage) - XCTAssertEqual(Array(buffer: response), expectedBytes) + XCTAssertEqual(response, .sendMessage(ByteBuffer(bytes: expectedBytes))) } func testNextOutboundMessageWhenClientOpenAndServerClosed() throws { @@ -1486,25 +1384,16 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Send message and close server XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: true)) - var action = try stateMachine.nextOutboundMessage() - guard case .sendMessage(let response) = action else { - XCTFail("Expected action to be sendMessage but was \(action)") - return - } - + let response = try stateMachine.nextOutboundMessage() let expectedBytes: [UInt8] = [ 0, // compression flag: unset 0, 0, 0, 2, // message length: 2 bytes 42, 42, // original message ] - XCTAssertEqual(Array(buffer: response), expectedBytes) + XCTAssertEqual(response, .sendMessage(ByteBuffer(bytes: expectedBytes))) // And then make sure that nothing else is returned anymore - action = try stateMachine.nextOutboundMessage() - guard case .noMoreMessages = action else { - XCTFail("Expected action to be noMoreMessages but was \(action)") - return - } + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) } func testNextOutboundMessageWhenClientClosedAndServerIdle() throws { @@ -1533,12 +1422,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Make sure that getting the next outbound message _does_ return the message // we have enqueued. - var action = try stateMachine.nextOutboundMessage() - guard case .sendMessage(let response) = action else { - XCTFail("Expected action to be sendMessage but was \(action)") - return - } - + let response = try stateMachine.nextOutboundMessage() let expectedBytes: [UInt8] = [ 0, // compression flag: unset 0, 0, 0, 2, // message length: 2 bytes @@ -1548,14 +1432,10 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { 0, 0, 0, 2, // message length: 2 bytes 43, 43, // original message ] - XCTAssertEqual(Array(buffer: response), expectedBytes) + XCTAssertEqual(response, .sendMessage(ByteBuffer(bytes: expectedBytes))) // And then make sure that nothing else is returned anymore - action = try stateMachine.nextOutboundMessage() - guard case .awaitMoreMessages = action else { - XCTFail("Expected action to be awaitMoreMessages but was \(action)") - return - } + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) } func testNextOutboundMessageWhenClientClosedAndServerClosed() throws { @@ -1566,25 +1446,16 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // We have enqueued a message, make sure we return it even though server is closed, // because we haven't yet drained all of the pending messages. - var action = try stateMachine.nextOutboundMessage() - guard case .sendMessage(let response) = action else { - XCTFail("Expected action to be sendMessage but was \(action)") - return - } - + let response = try stateMachine.nextOutboundMessage() let expectedBytes: [UInt8] = [ 0, // compression flag: unset 0, 0, 0, 2, // message length: 2 bytes 42, 42, // original message ] - XCTAssertEqual(Array(buffer: response), expectedBytes) + XCTAssertEqual(response, .sendMessage(ByteBuffer(bytes: expectedBytes))) // And then make sure that nothing else is returned anymore - action = try stateMachine.nextOutboundMessage() - guard case .noMoreMessages = action else { - XCTFail("Expected action to be noMoreMessages but was \(action)") - return - } + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) } // - MARK: Next inbound message From 389b6d51adb5eabaeb3a9bf2b9f882650ada9210 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Wed, 21 Feb 2024 10:49:08 +0000 Subject: [PATCH 25/51] Encode grpc status message in trailers --- .../GRPCStreamStateMachine.swift | 25 ++- .../GRPCStatusMessageMarshaller.swift | 206 ++++++++++++++++++ 2 files changed, 219 insertions(+), 12 deletions(-) create mode 100644 Sources/GRPCHTTP2Core/Internal/GRPCStatusMessageMarshaller.swift diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 163ec3544..20154164b 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -613,10 +613,6 @@ extension GRPCStreamStateMachine { // This state is valid: server can send trailing metadata without grpc-status // or END_STREAM set, and follow it with an empty message frame where they are set. () - // TODO: I believe we should set some flag in the state to signal that - // we're expecting an empty data frame with END_STREAM set; otherwise, - // we could get an infinite number of metadata frames from the server - - // not sure this should be allowed. } return .receivedMetadata(Metadata(headers: metadata)) case .clientClosedServerIdle(let state): @@ -686,10 +682,6 @@ extension GRPCStreamStateMachine { // This state is valid: server can send trailing metadata without grpc-status // or END_STREAM set, and follow it with an empty message frame where they are set. () - // TODO: I believe we should set some flag in the state to signal that - // we're expecting an empty data frame with END_STREAM set; otherwise, - // we could get an infinite number of metadata frames from the server - - // not sure this should be allowed. } return .receivedMetadata(Metadata(headers: metadata)) case .clientClosedServerClosed: @@ -910,7 +902,6 @@ extension GRPCStreamStateMachine { headers.grpcStatus = status.code if !status.message.isEmpty { - // TODO: this message has to be percent-encoded headers.grpcStatusMessage = status.message } @@ -1314,11 +1305,16 @@ extension HPACKHeaders { var grpcStatusMessage: String? { get { - self.firstString(forKey: .grpcStatusMessage) + if let message = self.firstString(forKey: .grpcStatusMessage) { + return GRPCStatusMessageMarshaller.unmarshall(message) + } + return nil } set { if let newValue { - self.add(newValue, forKey: .grpcStatusMessage) + if let percentEncodedMessage = GRPCStatusMessageMarshaller.marshall(newValue) { + self.add(percentEncodedMessage, forKey: .grpcStatusMessage) + } } else { self.removeAllValues(forKey: .grpcStatusMessage) } @@ -1371,7 +1367,12 @@ extension Metadata { metadata.addString(header.value, forKey: header.name) } } else { - metadata.addString(header.value, forKey: header.name) + if header.name == GRPCHTTP2Keys.grpcStatusMessage.rawValue, + let decodedStatusMessage = headers.grpcStatusMessage { + metadata.addString(decodedStatusMessage, forKey: header.name) + } else { + metadata.addString(header.value, forKey: header.name) + } } } self = metadata diff --git a/Sources/GRPCHTTP2Core/Internal/GRPCStatusMessageMarshaller.swift b/Sources/GRPCHTTP2Core/Internal/GRPCStatusMessageMarshaller.swift new file mode 100644 index 000000000..a9d5d63ce --- /dev/null +++ b/Sources/GRPCHTTP2Core/Internal/GRPCStatusMessageMarshaller.swift @@ -0,0 +1,206 @@ +/* + * 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. + */ + +// swiftformat:disable:next enumNamespaces +public struct GRPCStatusMessageMarshaller { + /// Adds percent encoding to the given message. + /// + /// - Parameter message: Message to percent encode. + /// - Returns: Percent encoded string, or `nil` if it could not be encoded. + public static func marshall(_ message: String) -> String? { + return percentEncode(message) + } + + /// Removes percent encoding from the given message. + /// + /// - Parameter message: Message to remove encoding from. + /// - Returns: The string with percent encoding removed, or the input string if the encoding + /// could not be removed. + public static func unmarshall(_ message: String) -> String { + return removePercentEncoding(message) + } +} + +extension GRPCStatusMessageMarshaller { + /// Adds percent encoding to the given message. + /// + /// gRPC uses percent encoding as defined in RFC 3986 § 2.1 but with a different set of restricted + /// characters. The allowed characters are all visible printing characters except for (`%`, + /// `0x25`). That is: `0x20`-`0x24`, `0x26`-`0x7E`. + /// + /// - Parameter message: The message to encode. + /// - Returns: Percent encoded string, or `nil` if it could not be encoded. + private static func percentEncode(_ message: String) -> String? { + let utf8 = message.utf8 + + let encodedLength = self.percentEncodedLength(for: utf8) + // Fast-path: all characters are valid, nothing to encode. + if encodedLength == utf8.count { + return message + } + + var bytes: [UInt8] = [] + bytes.reserveCapacity(encodedLength) + + for char in message.utf8 { + switch char { + // See: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#responses + case 0x20 ... 0x24, + 0x26 ... 0x7E: + bytes.append(char) + + default: + bytes.append(UInt8(ascii: "%")) + bytes.append(self.toHex(char >> 4)) + bytes.append(self.toHex(char & 0xF)) + } + } + + return String(decoding: bytes, as: UTF8.self) + } + + /// Returns the percent encoded length of the given `UTF8View`. + private static func percentEncodedLength(for view: String.UTF8View) -> Int { + var count = view.count + for byte in view { + switch byte { + case 0x20 ... 0x24, + 0x26 ... 0x7E: + () + + default: + count += 2 + } + } + return count + } + + /// Encode the given byte as hexadecimal. + /// + /// - Precondition: Only the four least significant bits may be set. + /// - Parameter nibble: The nibble to convert to hexadecimal. + private static func toHex(_ nibble: UInt8) -> UInt8 { + assert(nibble & 0xF == nibble) + + switch nibble { + case 0 ... 9: + return nibble &+ UInt8(ascii: "0") + default: + return nibble &+ (UInt8(ascii: "A") &- 10) + } + } + + /// Remove gRPC percent encoding from `message`. If any portion of the string could not be decoded + /// then the encoded message will be returned. + /// + /// - Parameter message: The message to remove percent encoding from. + /// - Returns: The decoded message. + private static func removePercentEncoding(_ message: String) -> String { + let utf8 = message.utf8 + + let decodedLength = self.percentDecodedLength(for: utf8) + // Fast-path: no decoding to do! Note that we may also have detected that the encoding is + // invalid, in which case we will return the encoded message: this is fine. + if decodedLength == utf8.count { + return message + } + + var chars: [UInt8] = [] + // We can't decode more characters than are already encoded. + chars.reserveCapacity(decodedLength) + + var currentIndex = utf8.startIndex + let endIndex = utf8.endIndex + + while currentIndex < endIndex { + let byte = utf8[currentIndex] + + switch byte { + case UInt8(ascii: "%"): + guard let (nextIndex, nextNextIndex) = utf8.nextTwoIndices(after: currentIndex), + let nextHex = fromHex(utf8[nextIndex]), + let nextNextHex = fromHex(utf8[nextNextIndex]) + else { + // If we can't decode the message, aborting and returning the encoded message is fine + // according to the spec. + return message + } + chars.append((nextHex << 4) | nextNextHex) + currentIndex = nextNextIndex + + default: + chars.append(byte) + } + + currentIndex = utf8.index(after: currentIndex) + } + + return String(decoding: chars, as: Unicode.UTF8.self) + } + + /// Returns the expected length of the decoded `UTF8View`. + private static func percentDecodedLength(for view: String.UTF8View) -> Int { + var encoded = 0 + + for byte in view { + switch byte { + case UInt8(ascii: "%"): + // This can't overflow since it can't be larger than view.count. + encoded &+= 1 + + default: + () + } + } + + let notEncoded = view.count - (encoded * 3) + + guard notEncoded >= 0 else { + // We've received gibberish: more '%' than expected. gRPC allows for the status message to + // be left encoded should it be incorrectly encoded. We'll do exactly that by returning + // the number of bytes in the view which will causes us to take the fast-path exit. + return view.count + } + + return notEncoded + encoded + } + + private static func fromHex(_ byte: UInt8) -> UInt8? { + switch byte { + case UInt8(ascii: "0") ... UInt8(ascii: "9"): + return byte &- UInt8(ascii: "0") + case UInt8(ascii: "A") ... UInt8(ascii: "Z"): + return byte &- (UInt8(ascii: "A") &- 10) + case UInt8(ascii: "a") ... UInt8(ascii: "z"): + return byte &- (UInt8(ascii: "a") &- 10) + default: + return nil + } + } +} + +extension String.UTF8View { + /// Return the next two valid indices after the given index. The indices are considered valid if + /// they less than `endIndex`. + fileprivate func nextTwoIndices(after index: Index) -> (Index, Index)? { + let secondIndex = self.index(index, offsetBy: 2) + guard secondIndex < self.endIndex else { + return nil + } + + return (self.index(after: index), secondIndex) + } +} From 507018c2c22ab569d81f53be04cc29a822a0a629 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Wed, 21 Feb 2024 11:02:18 +0000 Subject: [PATCH 26/51] Draft new common paths tests --- .../GRPCStreamStateMachineTests.swift | 61 +++++++++++++++---- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index c0fc5c97a..ada26d4f0 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -73,14 +73,6 @@ extension HPACKHeaders { ] } -fileprivate func assertRejectedRPC(_ action: GRPCStreamStateMachine.OnMetadataReceived, expression: (HPACKHeaders) -> Void) { - guard case .rejectRPC(let trailers) = action else { - XCTFail("RPC should have been rejected.") - return - } - expression(trailers) -} - final class GRPCStreamClientStateMachineTests: XCTestCase { private func makeClientStateMachine( targetState: TargetStateMachineState, @@ -778,6 +770,24 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { "Client cannot be idle if server is sending initial metadata: it must have opened." ) } + + // - MARK: Common paths + + func testNormalFlow() { + // TODO: implement + } + + func testClientClosesBeforeServerOpens() { + // TODO: implement + } + + func testClientClosesBeforeServerResponds() { + // TODO: implement + } + + func testServerRejectsRPC() { + // TODO: implement + } } func testSendMetadataWhenClientOpenAndServerIdle() throws { @@ -1078,7 +1088,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { metadata: .receivedHeadersWithoutContentType, endStream: false ) - assertRejectedRPC(action) { trailers in + self.assertRejectedRPC(action) { trailers in XCTAssertEqual(trailers.count, 1) XCTAssertEqual(trailers.status, "415") } @@ -1091,7 +1101,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { metadata: .receivedHeadersWithInvalidContentType, endStream: false ) - assertRejectedRPC(action) { trailers in + self.assertRejectedRPC(action) { trailers in XCTAssertEqual(trailers.count, 1) XCTAssertEqual(trailers.status, "415") } @@ -1105,7 +1115,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { endStream: false ) - assertRejectedRPC(action) { trailers in + self.assertRejectedRPC(action) { trailers in XCTAssertEqual(trailers.count, 2) XCTAssertEqual(trailers.grpcStatus, .unimplemented) XCTAssertEqual(trailers.grpcStatusMessage, "No :path header has been set.") @@ -1122,7 +1132,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { endStream: false ) - assertRejectedRPC(action) { trailers in + self.assertRejectedRPC(action) { trailers in XCTAssertEqual(trailers.count, 3) XCTAssertEqual(trailers.grpcStatus, .unimplemented) XCTAssertEqual( @@ -1574,5 +1584,32 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertNil(stateMachine.nextInboundMessage()) } + + // - MARK: Common paths + + func testNormalFlow() { + // TODO: implement + } + + func testClientClosesBeforeServerOpens() { + // TODO: implement + } + + func testClientClosesBeforeServerResponds() { + // TODO: implement + } + + func testServerRejectsRPC() { + // TODO: implement + } +} +extension XCTestCase { + func assertRejectedRPC(_ action: GRPCStreamStateMachine.OnMetadataReceived, expression: (HPACKHeaders) -> Void) { + guard case .rejectRPC(let trailers) = action else { + XCTFail("RPC should have been rejected.") + return + } + expression(trailers) + } } From 3219dc0e5e0f378c311195d2fb38ac1a4e999c82 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Wed, 21 Feb 2024 13:35:53 +0000 Subject: [PATCH 27/51] Formatting --- .../GRPCStreamStateMachine.swift | 11 ++-- .../GRPCStreamStateMachineTests.swift | 58 +++++++++++-------- 2 files changed, 41 insertions(+), 28 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 20154164b..541bff76e 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -207,7 +207,7 @@ enum GRPCStreamStateMachineState { let deframer: NIOSingleStepByteToMessageProcessor var decompressor: Zlib.Decompressor? - + var inboundMessageBuffer: OneOrManyQueue<[UInt8]> init(previousState: ClientOpenServerOpenState) { @@ -337,7 +337,7 @@ struct GRPCStreamStateMachine { // Server-specific actions case rejectRPC(trailers: HPACKHeaders) } - + mutating func receive(metadata: HPACKHeaders, endStream: Bool) throws -> OnMetadataReceived { switch self.configuration { case .client: @@ -370,7 +370,7 @@ struct GRPCStreamStateMachine { /// A message is ready to be sent. case sendMessage(ByteBuffer) } - + mutating func nextOutboundMessage() throws -> OnNextOutboundMessage { switch self.configuration { case .client: @@ -488,7 +488,7 @@ extension GRPCStreamStateMachine { ) } } - + /// Returns the client's next request to the server. /// - Returns: The request to be made to the server. private mutating func clientNextOutboundMessage() throws -> OnNextOutboundMessage { @@ -1368,7 +1368,8 @@ extension Metadata { } } else { if header.name == GRPCHTTP2Keys.grpcStatusMessage.rawValue, - let decodedStatusMessage = headers.grpcStatusMessage { + let decodedStatusMessage = headers.grpcStatusMessage + { metadata.addString(decodedStatusMessage, forKey: header.name) } else { metadata.addString(header.value, forKey: header.name) diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index ada26d4f0..66c60f8dc 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -54,12 +54,12 @@ extension HPACKHeaders { ] static let receivedHeadersWithInvalidContentType: Self = [ GRPCHTTP2Keys.path.rawValue: "test/test", - GRPCHTTP2Keys.contentType.rawValue: "invalid/invalid" + GRPCHTTP2Keys.contentType.rawValue: "invalid/invalid", ] static let receivedHeadersWithoutEndpoint: Self = [ GRPCHTTP2Keys.contentType.rawValue: "application/grpc" ] - + // Server static let serverInitialMetadata: Self = [ GRPCHTTP2Keys.status.rawValue: "200", @@ -453,7 +453,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) - + let expectedBytes: [UInt8] = [ 0, // compression flag: unset 0, 0, 0, 2, // message length: 2 bytes @@ -479,7 +479,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { let originalMessage = [UInt8]([42, 42, 43, 43]) XCTAssertNoThrow(try stateMachine.send(message: originalMessage, endStream: false)) - + let request = try stateMachine.nextOutboundMessage() var framer = GRPCMessageFramer() let compressor = Zlib.Compressor(method: .deflate) @@ -500,7 +500,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { let originalMessage = [UInt8]([42, 42, 43, 43]) XCTAssertNoThrow(try stateMachine.send(message: originalMessage, endStream: false)) - + let request = try stateMachine.nextOutboundMessage() var framer = GRPCMessageFramer() let compressor = Zlib.Compressor(method: .deflate) @@ -697,7 +697,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { targetState: TargetStateMachineState, compressionEnabled: Bool = false ) -> GRPCStreamStateMachine { - + var stateMachine = GRPCStreamStateMachine( configuration: .server( .init( @@ -770,21 +770,21 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { "Client cannot be idle if server is sending initial metadata: it must have opened." ) } - + // - MARK: Common paths func testNormalFlow() { // TODO: implement } - + func testClientClosesBeforeServerOpens() { // TODO: implement } - + func testClientClosesBeforeServerResponds() { // TODO: implement } - + func testServerRejectsRPC() { // TODO: implement } @@ -1057,7 +1057,10 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = makeServerStateMachine(targetState: .clientIdleServerIdle) let action = try stateMachine.receive(metadata: .clientInitialMetadata, endStream: false) - XCTAssertEqual(action, .receivedMetadata([":path": "test/test", "content-type": "application/grpc"])) + XCTAssertEqual( + action, + .receivedMetadata([":path": "test/test", "content-type": "application/grpc"]) + ) } func testReceiveMetadataWhenClientIdleAndServerIdle_WithEndStream() { @@ -1131,7 +1134,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { metadata: .clientInitialMetadataWithGzipCompression, endStream: false ) - + self.assertRejectedRPC(action) { trailers in XCTAssertEqual(trailers.count, 3) XCTAssertEqual(trailers.grpcStatus, .unimplemented) @@ -1145,7 +1148,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertEqual(trailers.acceptedEncodings, [.deflate]) } } - + //TODO: add more encoding-related validation tests (for both client and server) func testReceiveMetadataWhenClientOpenAndServerIdle() throws { @@ -1356,7 +1359,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) - + let response = try stateMachine.nextOutboundMessage() let expectedBytes: [UInt8] = [ 0, // compression flag: unset @@ -1370,13 +1373,16 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testNextOutboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { - var stateMachine = makeServerStateMachine(targetState: .clientOpenServerOpen, compressionEnabled: true) + var stateMachine = makeServerStateMachine( + targetState: .clientOpenServerOpen, + compressionEnabled: true + ) XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) let originalMessage = [UInt8]([42, 42, 43, 43]) XCTAssertNoThrow(try stateMachine.send(message: originalMessage, endStream: false)) - + let response = try stateMachine.nextOutboundMessage() var framer = GRPCMessageFramer() let compressor = Zlib.Compressor(method: .deflate) @@ -1393,7 +1399,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Send message and close server XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: true)) - + let response = try stateMachine.nextOutboundMessage() let expectedBytes: [UInt8] = [ 0, // compression flag: unset @@ -1498,7 +1504,10 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testNextInboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { - var stateMachine = makeServerStateMachine(targetState: .clientOpenServerOpen, compressionEnabled: true) + var stateMachine = makeServerStateMachine( + targetState: .clientOpenServerOpen, + compressionEnabled: true + ) let originalMessage = [UInt8]([42, 42, 43, 43]) var framer = GRPCMessageFramer() @@ -1584,28 +1593,31 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertNil(stateMachine.nextInboundMessage()) } - + // - MARK: Common paths func testNormalFlow() { // TODO: implement } - + func testClientClosesBeforeServerOpens() { // TODO: implement } - + func testClientClosesBeforeServerResponds() { // TODO: implement } - + func testServerRejectsRPC() { // TODO: implement } } extension XCTestCase { - func assertRejectedRPC(_ action: GRPCStreamStateMachine.OnMetadataReceived, expression: (HPACKHeaders) -> Void) { + func assertRejectedRPC( + _ action: GRPCStreamStateMachine.OnMetadataReceived, + expression: (HPACKHeaders) -> Void + ) { guard case .rejectRPC(let trailers) = action else { XCTFail("RPC should have been rejected.") return From 2bff8ab420e09455364c1fd5a43527e047d4dbb6 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Thu, 22 Feb 2024 12:03:36 +0000 Subject: [PATCH 28/51] Don't allow server to end stream by sending a message --- .../GRPCStreamStateMachine.swift | 119 +-- .../GRPCStreamStateMachineTests.swift | 704 ++++++++++++++---- 2 files changed, 631 insertions(+), 192 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 541bff76e..9180bde76 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -311,7 +311,11 @@ struct GRPCStreamStateMachine { case .client: try self.clientSend(message: message, endStream: endStream) case .server: - try self.serverSend(message: message, endStream: endStream) + assert( + !endStream, + "Can't end response stream by sending a message - send(status:metadata:trailersOnly:) must be called" + ) + try self.serverSend(message: message) } } @@ -323,7 +327,11 @@ struct GRPCStreamStateMachine { "Client cannot send status and trailer." ) case .server: - return try self.serverSend(status: status, metadata: metadata, trailersOnly: trailersOnly) + return try self.serverSend( + status: status, + customMetadata: metadata, + trailersOnly: trailersOnly + ) } } @@ -335,7 +343,7 @@ struct GRPCStreamStateMachine { case doNothing // Server-specific actions - case rejectRPC(trailers: HPACKHeaders) + case rejectRPC(status: Status?, trailers: HPACKHeaders?) } mutating func receive(metadata: HPACKHeaders, endStream: Bool) throws -> OnMetadataReceived { @@ -849,7 +857,7 @@ extension GRPCStreamStateMachine { } } - private mutating func serverSend(message: [UInt8], endStream: Bool) throws { + private mutating func serverSend(message: [UInt8]) throws { switch self.state { case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: throw self.assertionFailureAndCreateRPCErrorOnInternalError( @@ -857,18 +865,10 @@ extension GRPCStreamStateMachine { ) case .clientOpenServerOpen(var state): state.framer.append(message) - if endStream { - self.state = .clientOpenServerClosed(.init(previousState: state)) - } else { - self.state = .clientOpenServerOpen(state) - } + self.state = .clientOpenServerOpen(state) case .clientClosedServerOpen(var state): state.framer.append(message) - if endStream { - self.state = .clientClosedServerClosed(.init(previousState: state)) - } else { - self.state = .clientClosedServerOpen(state) - } + self.state = .clientClosedServerOpen(state) case .clientOpenServerClosed, .clientClosedServerClosed: throw self.assertionFailureAndCreateRPCErrorOnInternalError( "Server can't send a message if it's closed." @@ -877,26 +877,39 @@ extension GRPCStreamStateMachine { } private func makeTrailers( - status: Status, - customMetadata: Metadata, + status: Status?, + customMetadata: Metadata?, trailersOnly: Bool ) -> HPACKHeaders { + // If status isn't present, it means we're returning a non-200 HTTP :status + // header. This should already be included in the custom metadata: assert + // this and simply return those headers. + guard let status else { + assert(customMetadata != nil, "Something is very wrong") + var headers = HPACKHeaders() + for metadataPair in customMetadata! { + headers.add(name: metadataPair.key, value: metadataPair.value.encoded()) + } + assert(headers.status != nil && headers.status != "200") + return headers + } + // Trailers always contain the grpc-status header, and optionally, // grpc-status-message, and custom metadata. // If it's a trailers-only response, they will also contain :status and // content-type. var headers = HPACKHeaders() - + let customMetadataCount = customMetadata?.count ?? 0 if trailersOnly { // Reserve 4 for capacity: 3 for the required headers, and 1 for the // optional status message. - headers.reserveCapacity(4 + customMetadata.count) + headers.reserveCapacity(4 + customMetadataCount) headers.status = "200" headers.contentType = .protobuf } else { // Reserve 2 for capacity: one for the required grpc-status, and // one for the optional message. - headers.reserveCapacity(2 + customMetadata.count) + headers.reserveCapacity(2 + customMetadataCount) } headers.grpcStatus = status.code @@ -905,26 +918,36 @@ extension GRPCStreamStateMachine { headers.grpcStatusMessage = status.message } - for metadataPair in customMetadata { - headers.add(name: metadataPair.key, value: metadataPair.value.encoded()) + if let customMetadata { + for metadataPair in customMetadata { + headers.add(name: metadataPair.key, value: metadataPair.value.encoded()) + } } return headers } private mutating func serverSend( - status: Status, - metadata: Metadata, + status: Status?, + customMetadata: Metadata?, trailersOnly: Bool ) throws -> HPACKHeaders { // Close the server. switch self.state { case .clientOpenServerOpen(let state): self.state = .clientOpenServerClosed(.init(previousState: state)) - return self.makeTrailers(status: status, customMetadata: metadata, trailersOnly: trailersOnly) + return self.makeTrailers( + status: status, + customMetadata: customMetadata, + trailersOnly: trailersOnly + ) case .clientClosedServerOpen(let state): self.state = .clientClosedServerClosed(.init(previousState: state)) - return self.makeTrailers(status: status, customMetadata: metadata, trailersOnly: trailersOnly) + return self.makeTrailers( + status: status, + customMetadata: customMetadata, + trailersOnly: trailersOnly + ) case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: throw self.assertionFailureAndCreateRPCErrorOnInternalError( "Server can't send status if idle." @@ -956,15 +979,15 @@ extension GRPCStreamStateMachine { // Respond with HTTP-level Unsupported Media Type status code. var trailers = HPACKHeaders() trailers.status = "415" - return .rejectRPC(trailers: trailers) + return .rejectRPC(status: nil, trailers: trailers) } guard metadata.path != nil else { - var trailers = HPACKHeaders() - trailers.reserveCapacity(2) - trailers.grpcStatus = .unimplemented - trailers.grpcStatusMessage = "No \(GRPCHTTP2Keys.path.rawValue) header has been set." - return .rejectRPC(trailers: trailers) + let status = Status( + code: .unimplemented, + message: "No \(GRPCHTTP2Keys.path.rawValue) header has been set." + ) + return .rejectRPC(status: status, trailers: nil) } func isIdentityOrCompatibleEncoding(_ clientEncoding: CompressionAlgorithm) -> Bool { @@ -981,33 +1004,31 @@ extension GRPCStreamStateMachine { var encodingValuesIterator = encodingValues.makeIterator() if let rawEncoding = encodingValuesIterator.next() { guard encodingValuesIterator.next() == nil else { - var trailers = HPACKHeaders() - trailers.reserveCapacity(2) - trailers.grpcStatus = .internalError - trailers.grpcStatusMessage = - "\(GRPCHTTP2Keys.encoding) must contain no more than one value." - return .rejectRPC(trailers: trailers) + let status = Status( + code: .internalError, + message: "\(GRPCHTTP2Keys.encoding) must contain no more than one value." + ) + return .rejectRPC(status: status, trailers: nil) } guard let clientEncoding = CompressionAlgorithm(rawValue: String(rawEncoding)), isIdentityOrCompatibleEncoding(clientEncoding) else { if configuration.acceptedEncodings.isEmpty { - var trailers = HPACKHeaders() - trailers.reserveCapacity(2) - trailers.grpcStatus = .unimplemented - trailers.grpcStatusMessage = "Compression is not supported" - return .rejectRPC(trailers: trailers) + let status = Status(code: .unimplemented, message: "Compression is not supported") + return .rejectRPC(status: status, trailers: nil) } else { var trailers = HPACKHeaders() - trailers.reserveCapacity(3) - trailers.grpcStatus = .unimplemented - trailers.grpcStatusMessage = """ - \(rawEncoding) compression is not supported; \ - supported algorithms are listed in grpc-accept-encoding - """ + trailers.reserveCapacity(1) + let status = Status( + code: .unimplemented, + message: """ + \(rawEncoding) compression is not supported; \ + supported algorithms are listed in grpc-accept-encoding + """ + ) trailers.acceptedEncodings = configuration.acceptedEncodings - return .rejectRPC(trailers: trailers) + return .rejectRPC(status: status, trailers: trailers) } } diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index 66c60f8dc..e056d18ec 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -35,19 +35,28 @@ extension HPACKHeaders { // Client static let clientInitialMetadata: Self = [ GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.scheme.rawValue: "http", + GRPCHTTP2Keys.method.rawValue: "POST", GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.te.rawValue: "trailers", ] static let clientInitialMetadataWithDeflateCompression: Self = [ GRPCHTTP2Keys.path.rawValue: "test/test", GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.encoding.rawValue: "deflate", + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.scheme.rawValue: "https", + GRPCHTTP2Keys.te.rawValue: "te", GRPCHTTP2Keys.acceptEncoding.rawValue: "deflate", + GRPCHTTP2Keys.encoding.rawValue: "deflate", ] static let clientInitialMetadataWithGzipCompression: Self = [ GRPCHTTP2Keys.path.rawValue: "test/test", GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.encoding.rawValue: "gzip", + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.scheme.rawValue: "https", + GRPCHTTP2Keys.te.rawValue: "te", GRPCHTTP2Keys.acceptEncoding.rawValue: "gzip", + GRPCHTTP2Keys.encoding.rawValue: "gzip", ] static let receivedHeadersWithoutContentType: Self = [ GRPCHTTP2Keys.path.rawValue: "test/test" @@ -64,6 +73,7 @@ extension HPACKHeaders { static let serverInitialMetadata: Self = [ GRPCHTTP2Keys.status.rawValue: "200", GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, + GRPCHTTP2Keys.acceptEncoding.rawValue: "deflate", ] static let serverInitialMetadataWithDeflateCompression: Self = [ GRPCHTTP2Keys.status.rawValue: "200", @@ -71,6 +81,11 @@ extension HPACKHeaders { GRPCHTTP2Keys.encoding.rawValue: "deflate", GRPCHTTP2Keys.acceptEncoding.rawValue: "deflate", ] + static let serverTrailers: Self = [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, + GRPCHTTP2Keys.grpcStatus.rawValue: "0", + ] } final class GRPCStreamClientStateMachineTests: XCTestCase { @@ -481,12 +496,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertNoThrow(try stateMachine.send(message: originalMessage, endStream: false)) let request = try stateMachine.nextOutboundMessage() - var framer = GRPCMessageFramer() - let compressor = Zlib.Compressor(method: .deflate) - defer { compressor.end() } - framer.append(originalMessage) - - let framedMessage = try XCTUnwrap(framer.next(compressor: compressor)) + let framedMessage = try self.frameMessage(originalMessage, compress: true) XCTAssertEqual(request, .sendMessage(framedMessage)) } @@ -502,12 +512,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertNoThrow(try stateMachine.send(message: originalMessage, endStream: false)) let request = try stateMachine.nextOutboundMessage() - var framer = GRPCMessageFramer() - let compressor = Zlib.Compressor(method: .deflate) - defer { compressor.end() } - framer.append(originalMessage) - - let framedMessage = try XCTUnwrap(framer.next(compressor: compressor)) + let framedMessage = try self.frameMessage(originalMessage, compress: true) XCTAssertEqual(request, .sendMessage(framedMessage)) } @@ -613,12 +618,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { ) let originalMessage = [UInt8]([42, 42, 43, 43]) - var framer = GRPCMessageFramer() - let compressor = Zlib.Compressor(method: .deflate) - defer { compressor.end() } - framer.append(originalMessage) - let receivedBytes = try framer.next(compressor: compressor)! - + let receivedBytes = try self.frameMessage(originalMessage, compress: true) try stateMachine.receive(message: receivedBytes, endStream: false) let receivedMessage = try XCTUnwrap(stateMachine.nextInboundMessage()) @@ -690,6 +690,212 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertNil(stateMachine.nextInboundMessage()) } + + // - MARK: Common paths + + func testNormalFlow() throws { + var stateMachine = self.makeClientStateMachine(targetState: .clientIdleServerIdle) + + // Client sends metadata + let clientInitialMetadata = try stateMachine.send(metadata: .init()) + XCTAssertEqual( + clientInitialMetadata, + [ + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.scheme.rawValue: "http", + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.te.rawValue: "trailers", + GRPCHTTP2Keys.acceptEncoding.rawValue: "deflate", + ] + ) + + // Server sends initial metadata + let serverInitialHeadersAction = try stateMachine.receive( + metadata: .serverInitialMetadata, + endStream: false + ) + XCTAssertEqual( + serverInitialHeadersAction, + .receivedMetadata([ + ":status": "200", + "content-type": "application/grpc", + "grpc-accept-encoding": "deflate", + ]) + ) + + // Client sends messages + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) + + let message = [UInt8]([1, 2, 3, 4]) + let framedMessage = try self.frameMessage(message, compress: false) + try stateMachine.send(message: message, endStream: false) + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .sendMessage(framedMessage)) + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) + + // Server sends response + XCTAssertNil(stateMachine.nextInboundMessage()) + + let firstResponseBytes = [UInt8]([5, 6, 7]) + let firstResponse = try self.frameMessage(firstResponseBytes, compress: false) + let secondResponseBytes = [UInt8]([8, 9, 10]) + let secondResponse = try self.frameMessage(secondResponseBytes, compress: false) + try stateMachine.receive(message: firstResponse, endStream: false) + try stateMachine.receive(message: secondResponse, endStream: false) + + // Make sure messages have arrived + XCTAssertEqual(stateMachine.nextInboundMessage(), firstResponseBytes) + XCTAssertEqual(stateMachine.nextInboundMessage(), secondResponseBytes) + XCTAssertNil(stateMachine.nextInboundMessage()) + + // Client sends end + try stateMachine.send(message: [], endStream: true) + + // Server ends + let metadataReceivedAction = try stateMachine.receive( + metadata: .serverTrailers, + endStream: true + ) + XCTAssertEqual(metadataReceivedAction, .receivedMetadata(Metadata(headers: .serverTrailers))) + + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) + XCTAssertNil(stateMachine.nextInboundMessage()) + } + + func testClientClosesBeforeServerOpens() throws { + var stateMachine = self.makeClientStateMachine(targetState: .clientIdleServerIdle) + + // Client sends metadata + let clientInitialMetadata = try stateMachine.send(metadata: .init()) + XCTAssertEqual( + clientInitialMetadata, + [ + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.scheme.rawValue: "http", + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.te.rawValue: "trailers", + GRPCHTTP2Keys.acceptEncoding.rawValue: "deflate", + ] + ) + + // Client sends messages and ends + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) + + let message = [UInt8]([1, 2, 3, 4]) + let framedMessage = try self.frameMessage(message, compress: false) + try stateMachine.send(message: message, endStream: true) + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .sendMessage(framedMessage)) + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) + + // Server sends initial metadata + let serverInitialHeadersAction = try stateMachine.receive( + metadata: .serverInitialMetadata, + endStream: false + ) + XCTAssertEqual( + serverInitialHeadersAction, + .receivedMetadata([ + ":status": "200", + "content-type": "application/grpc", + "grpc-accept-encoding": "deflate", + ]) + ) + + // Server sends response + XCTAssertNil(stateMachine.nextInboundMessage()) + + let firstResponseBytes = [UInt8]([5, 6, 7]) + let firstResponse = try self.frameMessage(firstResponseBytes, compress: false) + let secondResponseBytes = [UInt8]([8, 9, 10]) + let secondResponse = try self.frameMessage(secondResponseBytes, compress: false) + try stateMachine.receive(message: firstResponse, endStream: false) + try stateMachine.receive(message: secondResponse, endStream: false) + + // Make sure messages have arrived + XCTAssertEqual(stateMachine.nextInboundMessage(), firstResponseBytes) + XCTAssertEqual(stateMachine.nextInboundMessage(), secondResponseBytes) + XCTAssertNil(stateMachine.nextInboundMessage()) + + // Server ends + let metadataReceivedAction = try stateMachine.receive( + metadata: .serverTrailers, + endStream: true + ) + XCTAssertEqual(metadataReceivedAction, .receivedMetadata(Metadata(headers: .serverTrailers))) + + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) + XCTAssertNil(stateMachine.nextInboundMessage()) + } + + func testClientClosesBeforeServerResponds() throws { + var stateMachine = self.makeClientStateMachine(targetState: .clientIdleServerIdle) + + // Client sends metadata + let clientInitialMetadata = try stateMachine.send(metadata: .init()) + XCTAssertEqual( + clientInitialMetadata, + [ + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.scheme.rawValue: "http", + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.te.rawValue: "trailers", + GRPCHTTP2Keys.acceptEncoding.rawValue: "deflate", + ] + ) + + // Client sends messages + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) + + let message = [UInt8]([1, 2, 3, 4]) + let framedMessage = try self.frameMessage(message, compress: false) + try stateMachine.send(message: message, endStream: false) + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .sendMessage(framedMessage)) + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) + + // Server sends initial metadata + let serverInitialHeadersAction = try stateMachine.receive( + metadata: .serverInitialMetadata, + endStream: false + ) + XCTAssertEqual( + serverInitialHeadersAction, + .receivedMetadata([ + ":status": "200", + "content-type": "application/grpc", + "grpc-accept-encoding": "deflate", + ]) + ) + + // Client sends end + try stateMachine.send(message: [], endStream: true) + + // Server sends response + XCTAssertNil(stateMachine.nextInboundMessage()) + + let firstResponseBytes = [UInt8]([5, 6, 7]) + let firstResponse = try self.frameMessage(firstResponseBytes, compress: false) + let secondResponseBytes = [UInt8]([8, 9, 10]) + let secondResponse = try self.frameMessage(secondResponseBytes, compress: false) + try stateMachine.receive(message: firstResponse, endStream: false) + try stateMachine.receive(message: secondResponse, endStream: false) + + // Make sure messages have arrived + XCTAssertEqual(stateMachine.nextInboundMessage(), firstResponseBytes) + XCTAssertEqual(stateMachine.nextInboundMessage(), secondResponseBytes) + XCTAssertNil(stateMachine.nextInboundMessage()) + + // Server ends + let metadataReceivedAction = try stateMachine.receive( + metadata: .serverTrailers, + endStream: true + ) + XCTAssertEqual(metadataReceivedAction, .receivedMetadata(Metadata(headers: .serverTrailers))) + + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) + XCTAssertNil(stateMachine.nextInboundMessage()) + } } final class GRPCStreamServerStateMachineTests: XCTestCase { @@ -728,7 +934,13 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Open server XCTAssertNoThrow(try stateMachine.send(metadata: Metadata(headers: .serverInitialMetadata))) // Close server - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + XCTAssertNoThrow( + try stateMachine.send( + status: .init(code: .ok, message: ""), + metadata: [], + trailersOnly: false + ) + ) case .clientClosedServerIdle: // Open client XCTAssertNoThrow(try stateMachine.receive(metadata: clientMetadata, endStream: false)) @@ -749,7 +961,13 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Close client XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) // Close server - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + XCTAssertNoThrow( + try stateMachine.send( + status: .init(code: .ok, message: ""), + metadata: [], + trailersOnly: false + ) + ) } return stateMachine @@ -758,7 +976,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // - MARK: Send Metadata func testSendMetadataWhenClientIdleAndServerIdle() throws { - var stateMachine = makeServerStateMachine(targetState: .clientIdleServerIdle) + var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) XCTAssertThrowsError( ofType: RPCError.self, @@ -770,33 +988,15 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { "Client cannot be idle if server is sending initial metadata: it must have opened." ) } - - // - MARK: Common paths - - func testNormalFlow() { - // TODO: implement - } - - func testClientClosesBeforeServerOpens() { - // TODO: implement - } - - func testClientClosesBeforeServerResponds() { - // TODO: implement - } - - func testServerRejectsRPC() { - // TODO: implement - } } func testSendMetadataWhenClientOpenAndServerIdle() throws { - var stateMachine = makeServerStateMachine(targetState: .clientOpenServerIdle) + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerIdle) XCTAssertNoThrow(try stateMachine.send(metadata: .init())) } func testSendMetadataWhenClientOpenAndServerOpen() throws { - var stateMachine = makeServerStateMachine(targetState: .clientOpenServerOpen) + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) // Try sending metadata again: should throw XCTAssertThrowsError( @@ -809,7 +1009,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testSendMetadataWhenClientOpenAndServerClosed() throws { - var stateMachine = makeServerStateMachine(targetState: .clientOpenServerClosed) + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerClosed) // Try sending metadata again: should throw XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in @@ -819,7 +1019,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testSendMetadataWhenClientClosedAndServerIdle() throws { - var stateMachine = makeServerStateMachine(targetState: .clientClosedServerIdle) + var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerIdle) // We should be allowed to send initial metadata if client is closed: // client may be finished sending request but may still be awaiting response. @@ -827,7 +1027,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testSendMetadataWhenClientClosedAndServerOpen() throws { - var stateMachine = makeServerStateMachine(targetState: .clientClosedServerOpen) + var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerOpen) // Try sending metadata again: should throw XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in @@ -837,7 +1037,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testSendMetadataWhenClientClosedAndServerClosed() throws { - var stateMachine = makeServerStateMachine(targetState: .clientClosedServerClosed) + var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerClosed) // Try sending metadata again: should throw XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in @@ -849,7 +1049,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // - MARK: Send Message func testSendMessageWhenClientIdleAndServerIdle() { - var stateMachine = makeServerStateMachine(targetState: .clientIdleServerIdle) + var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) XCTAssertThrowsError( ofType: RPCError.self, @@ -864,7 +1064,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testSendMessageWhenClientOpenAndServerIdle() { - var stateMachine = makeServerStateMachine(targetState: .clientOpenServerIdle) + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerIdle) // Now send a message XCTAssertThrowsError( @@ -880,14 +1080,14 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testSendMessageWhenClientOpenAndServerOpen() { - var stateMachine = makeServerStateMachine(targetState: .clientOpenServerOpen) + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) // Now send a message XCTAssertNoThrow(try stateMachine.send(message: [], endStream: false)) } func testSendMessageWhenClientOpenAndServerClosed() { - var stateMachine = makeServerStateMachine(targetState: .clientOpenServerClosed) + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerClosed) // Try sending another message: it should fail XCTAssertThrowsError( @@ -900,7 +1100,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testSendMessageWhenClientClosedAndServerIdle() { - var stateMachine = makeServerStateMachine(targetState: .clientClosedServerIdle) + var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerIdle) XCTAssertThrowsError( ofType: RPCError.self, @@ -915,7 +1115,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testSendMessageWhenClientClosedAndServerOpen() { - var stateMachine = makeServerStateMachine(targetState: .clientClosedServerOpen) + var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerOpen) // Try sending a message: even though client is closed, we should send it // because it may be expecting a response. @@ -923,7 +1123,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testSendMessageWhenClientClosedAndServerClosed() { - var stateMachine = makeServerStateMachine(targetState: .clientClosedServerClosed) + var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerClosed) // Try sending another message: it should fail XCTAssertThrowsError( @@ -938,7 +1138,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // - MARK: Send Status and Trailers func testSendStatusAndTrailersWhenClientIdleAndServerIdle() { - var stateMachine = makeServerStateMachine(targetState: .clientIdleServerIdle) + var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) XCTAssertThrowsError( ofType: RPCError.self, @@ -954,7 +1154,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testSendStatusAndTrailersWhenClientOpenAndServerIdle() { - var stateMachine = makeServerStateMachine(targetState: .clientOpenServerIdle) + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerIdle) XCTAssertThrowsError( ofType: RPCError.self, @@ -970,7 +1170,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testSendStatusAndTrailersWhenClientOpenAndServerOpen() { - var stateMachine = makeServerStateMachine(targetState: .clientOpenServerOpen) + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) XCTAssertNoThrow( try stateMachine.send( @@ -991,7 +1191,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testSendStatusAndTrailersWhenClientOpenAndServerClosed() { - var stateMachine = makeServerStateMachine(targetState: .clientOpenServerClosed) + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerClosed) XCTAssertThrowsError( ofType: RPCError.self, @@ -1007,7 +1207,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testSendStatusAndTrailersWhenClientClosedAndServerIdle() { - var stateMachine = makeServerStateMachine(targetState: .clientClosedServerIdle) + var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerIdle) XCTAssertThrowsError( ofType: RPCError.self, @@ -1023,7 +1223,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testSendStatusAndTrailersWhenClientClosedAndServerOpen() { - var stateMachine = makeServerStateMachine(targetState: .clientClosedServerOpen) + var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerOpen) // Client is closed but may still be awaiting response, so we should be able to send it. XCTAssertNoThrow( @@ -1036,7 +1236,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testSendStatusAndTrailersWhenClientClosedAndServerClosed() { - var stateMachine = makeServerStateMachine(targetState: .clientClosedServerClosed) + var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerClosed) XCTAssertThrowsError( ofType: RPCError.self, @@ -1054,17 +1254,17 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // - MARK: Receive metadata func testReceiveMetadataWhenClientIdleAndServerIdle() throws { - var stateMachine = makeServerStateMachine(targetState: .clientIdleServerIdle) + var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) let action = try stateMachine.receive(metadata: .clientInitialMetadata, endStream: false) XCTAssertEqual( action, - .receivedMetadata([":path": "test/test", "content-type": "application/grpc"]) + .receivedMetadata(Metadata(headers: .clientInitialMetadata)) ) } func testReceiveMetadataWhenClientIdleAndServerIdle_WithEndStream() { - var stateMachine = makeServerStateMachine(targetState: .clientIdleServerIdle) + var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) // If endStream is set, we should fail, because the client can only close by // sending a message with endStream set. If they send metadata it has to be @@ -1085,48 +1285,53 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testReceiveMetadataWhenClientIdleAndServerIdle_MissingContentType() throws { - var stateMachine = makeServerStateMachine(targetState: .clientIdleServerIdle) + var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) let action = try stateMachine.receive( metadata: .receivedHeadersWithoutContentType, endStream: false ) - self.assertRejectedRPC(action) { trailers in - XCTAssertEqual(trailers.count, 1) - XCTAssertEqual(trailers.status, "415") + + try self.assertRejectedRPC(action) { grpcStatus, trailers in + let unwrappedTrailers = try XCTUnwrap(trailers) + XCTAssertEqual(unwrappedTrailers.count, 1) + XCTAssertEqual(unwrappedTrailers.status, "415") } } func testReceiveMetadataWhenClientIdleAndServerIdle_InvalidContentType() throws { - var stateMachine = makeServerStateMachine(targetState: .clientIdleServerIdle) + var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) let action = try stateMachine.receive( metadata: .receivedHeadersWithInvalidContentType, endStream: false ) - self.assertRejectedRPC(action) { trailers in - XCTAssertEqual(trailers.count, 1) - XCTAssertEqual(trailers.status, "415") + + try self.assertRejectedRPC(action) { grpcStatus, trailers in + let unwrappedTrailers = try XCTUnwrap(trailers) + XCTAssertEqual(unwrappedTrailers.count, 1) + XCTAssertEqual(unwrappedTrailers.status, "415") } } func testReceiveMetadataWhenClientIdleAndServerIdle_MissingPath() throws { - var stateMachine = makeServerStateMachine(targetState: .clientIdleServerIdle) + var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) let action = try stateMachine.receive( metadata: .receivedHeadersWithoutEndpoint, endStream: false ) - self.assertRejectedRPC(action) { trailers in - XCTAssertEqual(trailers.count, 2) - XCTAssertEqual(trailers.grpcStatus, .unimplemented) - XCTAssertEqual(trailers.grpcStatusMessage, "No :path header has been set.") + try self.assertRejectedRPC(action) { grpcStatus, trailers in + XCTAssertNil(trailers) + let unwrappedStatus = try XCTUnwrap(grpcStatus) + XCTAssertEqual(unwrappedStatus.code, .unimplemented) + XCTAssertEqual(unwrappedStatus.message, "No :path header has been set.") } } func testReceiveMetadataWhenClientIdleAndServerIdle_ServerUnsupportedEncoding() throws { - var stateMachine = makeServerStateMachine(targetState: .clientIdleServerIdle) + var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) // Try opening client with a compression algorithm that is not accepted // by the server. @@ -1135,24 +1340,28 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { endStream: false ) - self.assertRejectedRPC(action) { trailers in - XCTAssertEqual(trailers.count, 3) - XCTAssertEqual(trailers.grpcStatus, .unimplemented) + try self.assertRejectedRPC(action) { grpcStatus, trailers in + let unwrappedTrailers = try XCTUnwrap(trailers) + XCTAssertEqual(unwrappedTrailers.count, 1) + XCTAssertEqual(unwrappedTrailers.acceptedEncodings, [.deflate]) + + let unwrappedStatus = try XCTUnwrap(grpcStatus) + XCTAssertEqual(unwrappedStatus.code, .unimplemented) XCTAssertEqual( - trailers.grpcStatusMessage, + unwrappedStatus.message, """ gzip compression is not supported; \ supported algorithms are listed in grpc-accept-encoding """ ) - XCTAssertEqual(trailers.acceptedEncodings, [.deflate]) } } //TODO: add more encoding-related validation tests (for both client and server) + // and message encoding tests func testReceiveMetadataWhenClientOpenAndServerIdle() throws { - var stateMachine = makeServerStateMachine(targetState: .clientOpenServerIdle) + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerIdle) // Try receiving initial metadata again - should fail XCTAssertThrowsError( @@ -1165,7 +1374,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testReceiveMetadataWhenClientOpenAndServerOpen() throws { - var stateMachine = makeServerStateMachine(targetState: .clientOpenServerOpen) + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) XCTAssertThrowsError( ofType: RPCError.self, @@ -1177,7 +1386,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testReceiveMetadataWhenClientOpenAndServerClosed() { - var stateMachine = makeServerStateMachine(targetState: .clientOpenServerClosed) + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerClosed) XCTAssertThrowsError( ofType: RPCError.self, @@ -1189,7 +1398,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testReceiveMetadataWhenClientClosedAndServerIdle() { - var stateMachine = makeServerStateMachine(targetState: .clientClosedServerIdle) + var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerIdle) XCTAssertThrowsError( ofType: RPCError.self, @@ -1201,7 +1410,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testReceiveMetadataWhenClientClosedAndServerOpen() { - var stateMachine = makeServerStateMachine(targetState: .clientClosedServerOpen) + var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerOpen) XCTAssertThrowsError( ofType: RPCError.self, @@ -1213,7 +1422,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testReceiveMetadataWhenClientClosedAndServerClosed() { - var stateMachine = makeServerStateMachine(targetState: .clientClosedServerClosed) + var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerClosed) XCTAssertThrowsError( ofType: RPCError.self, @@ -1227,7 +1436,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // - MARK: Receive message func testReceiveMessageWhenClientIdleAndServerIdle() { - var stateMachine = makeServerStateMachine(targetState: .clientIdleServerIdle) + var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) XCTAssertThrowsError( ofType: RPCError.self, @@ -1239,7 +1448,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testReceiveMessageWhenClientOpenAndServerIdle() { - var stateMachine = makeServerStateMachine(targetState: .clientOpenServerIdle) + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerIdle) // Receive messages successfully: the second one should close client. XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: false)) @@ -1256,7 +1465,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testReceiveMessageWhenClientOpenAndServerOpen() throws { - var stateMachine = makeServerStateMachine(targetState: .clientOpenServerOpen) + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) // Receive messages successfully: the second one should close client. XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: false)) @@ -1273,14 +1482,14 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testReceiveMessageWhenClientOpenAndServerClosed() { - var stateMachine = makeServerStateMachine(targetState: .clientOpenServerClosed) + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerClosed) // Client is not done sending request, don't fail. XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: false)) } func testReceiveMessageWhenClientClosedAndServerIdle() { - var stateMachine = makeServerStateMachine(targetState: .clientClosedServerIdle) + var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerIdle) XCTAssertThrowsError( ofType: RPCError.self, @@ -1292,7 +1501,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testReceiveMessageWhenClientClosedAndServerOpen() { - var stateMachine = makeServerStateMachine(targetState: .clientClosedServerOpen) + var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerOpen) XCTAssertThrowsError( ofType: RPCError.self, @@ -1304,7 +1513,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testReceiveMessageWhenClientClosedAndServerClosed() { - var stateMachine = makeServerStateMachine(targetState: .clientClosedServerClosed) + var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerClosed) XCTAssertThrowsError( ofType: RPCError.self, @@ -1318,7 +1527,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // - MARK: Next outbound message func testNextOutboundMessageWhenClientIdleAndServerIdle() { - var stateMachine = makeServerStateMachine(targetState: .clientIdleServerIdle) + var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) XCTAssertThrowsError( ofType: RPCError.self, @@ -1330,7 +1539,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testNextOutboundMessageWhenClientOpenAndServerIdle() throws { - var stateMachine = makeServerStateMachine(targetState: .clientOpenServerIdle) + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerIdle) XCTAssertThrowsError( ofType: RPCError.self, @@ -1342,7 +1551,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testNextOutboundMessageWhenClientOpenAndServerIdle_WithCompression() throws { - var stateMachine = makeServerStateMachine(targetState: .clientOpenServerIdle) + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerIdle) XCTAssertThrowsError( ofType: RPCError.self, @@ -1354,7 +1563,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testNextOutboundMessageWhenClientOpenAndServerOpen() throws { - var stateMachine = makeServerStateMachine(targetState: .clientOpenServerOpen) + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) @@ -1373,7 +1582,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testNextOutboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { - var stateMachine = makeServerStateMachine( + var stateMachine = self.makeServerStateMachine( targetState: .clientOpenServerOpen, compressionEnabled: true ) @@ -1384,21 +1593,22 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertNoThrow(try stateMachine.send(message: originalMessage, endStream: false)) let response = try stateMachine.nextOutboundMessage() - var framer = GRPCMessageFramer() - let compressor = Zlib.Compressor(method: .deflate) - defer { compressor.end() } - framer.append(originalMessage) - - let framedMessage = try XCTUnwrap(framer.next(compressor: compressor)) - let expectedBytes = Array(buffer: framedMessage) - XCTAssertEqual(response, .sendMessage(ByteBuffer(bytes: expectedBytes))) + let framedMessage = try self.frameMessage(originalMessage, compress: true) + XCTAssertEqual(response, .sendMessage(framedMessage)) } func testNextOutboundMessageWhenClientOpenAndServerClosed() throws { - var stateMachine = makeServerStateMachine(targetState: .clientOpenServerOpen) + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) // Send message and close server - XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: true)) + XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) + XCTAssertNoThrow( + try stateMachine.send( + status: .init(code: .ok, message: ""), + metadata: [], + trailersOnly: false + ) + ) let response = try stateMachine.nextOutboundMessage() let expectedBytes: [UInt8] = [ @@ -1413,7 +1623,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testNextOutboundMessageWhenClientClosedAndServerIdle() throws { - var stateMachine = makeServerStateMachine(targetState: .clientClosedServerIdle) + var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerIdle) XCTAssertThrowsError( ofType: RPCError.self, @@ -1425,7 +1635,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testNextOutboundMessageWhenClientClosedAndServerOpen() throws { - var stateMachine = makeServerStateMachine(targetState: .clientOpenServerOpen) + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) // Send a message XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) @@ -1455,10 +1665,17 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testNextOutboundMessageWhenClientClosedAndServerClosed() throws { - var stateMachine = makeServerStateMachine(targetState: .clientClosedServerOpen) + var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerOpen) // Send a message and close server - XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: true)) + XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) + XCTAssertNoThrow( + try stateMachine.send( + status: .init(code: .ok, message: ""), + metadata: [], + trailersOnly: false + ) + ) // We have enqueued a message, make sure we return it even though server is closed, // because we haven't yet drained all of the pending messages. @@ -1477,18 +1694,18 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // - MARK: Next inbound message func testNextInboundMessageWhenClientIdleAndServerIdle() { - var stateMachine = makeServerStateMachine(targetState: .clientIdleServerIdle) + var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) XCTAssertNil(stateMachine.nextInboundMessage()) } func testNextInboundMessageWhenClientOpenAndServerIdle() { - var stateMachine = makeServerStateMachine(targetState: .clientOpenServerIdle) + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerIdle) XCTAssertNil(stateMachine.nextInboundMessage()) } func testNextInboundMessageWhenClientOpenAndServerOpen() throws { - var stateMachine = makeServerStateMachine(targetState: .clientOpenServerOpen) + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) let receivedBytes = ByteBuffer(bytes: [ 0, // compression flag: unset @@ -1504,17 +1721,13 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testNextInboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { - var stateMachine = makeServerStateMachine( + var stateMachine = self.makeServerStateMachine( targetState: .clientOpenServerOpen, compressionEnabled: true ) let originalMessage = [UInt8]([42, 42, 43, 43]) - var framer = GRPCMessageFramer() - let compressor = Zlib.Compressor(method: .deflate) - defer { compressor.end() } - framer.append(originalMessage) - let receivedBytes = try framer.next(compressor: compressor)! + let receivedBytes = try self.frameMessage(originalMessage, compress: true) try stateMachine.receive(message: receivedBytes, endStream: false) @@ -1525,7 +1738,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testNextInboundMessageWhenClientOpenAndServerClosed() throws { - var stateMachine = makeServerStateMachine(targetState: .clientOpenServerOpen) + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) let receivedBytes = ByteBuffer(bytes: [ 0, // compression flag: unset @@ -1535,7 +1748,13 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { try stateMachine.receive(message: receivedBytes, endStream: false) // Close server - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + XCTAssertNoThrow( + try stateMachine.send( + status: .init(code: .ok, message: ""), + metadata: [], + trailersOnly: false + ) + ) let receivedMessage = try XCTUnwrap(stateMachine.nextInboundMessage()) XCTAssertEqual(receivedMessage, [42, 42]) @@ -1544,13 +1763,13 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testNextInboundMessageWhenClientClosedAndServerIdle() { - var stateMachine = makeServerStateMachine(targetState: .clientClosedServerIdle) + var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerIdle) XCTAssertNil(stateMachine.nextInboundMessage()) } func testNextInboundMessageWhenClientClosedAndServerOpen() throws { - var stateMachine = makeServerStateMachine(targetState: .clientOpenServerOpen) + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) let receivedBytes = ByteBuffer(bytes: [ 0, // compression flag: unset @@ -1571,7 +1790,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testNextInboundMessageWhenClientClosedAndServerClosed() throws { - var stateMachine = makeServerStateMachine(targetState: .clientOpenServerOpen) + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) let receivedBytes = ByteBuffer(bytes: [ 0, // compression flag: unset @@ -1581,7 +1800,13 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { try stateMachine.receive(message: receivedBytes, endStream: false) // Close server - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + XCTAssertNoThrow( + try stateMachine.send( + status: .init(code: .ok, message: ""), + metadata: [], + trailersOnly: false + ) + ) // Close client XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) @@ -1596,32 +1821,225 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // - MARK: Common paths - func testNormalFlow() { - // TODO: implement - } + func testNormalFlow() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) + + // Client sends metadata + let receiveMetadataAction = try stateMachine.receive( + metadata: .clientInitialMetadata, + endStream: false + ) + XCTAssertEqual( + receiveMetadataAction, + .receivedMetadata(Metadata(headers: .clientInitialMetadata)) + ) + + // Server sends initial metadata + let sentInitialHeaders = try stateMachine.send(metadata: Metadata(headers: ["custom": "value"])) + XCTAssertEqual( + sentInitialHeaders, + [ + ":status": "200", + "content-type": "application/grpc", + "grpc-accept-encoding": "deflate", + "custom": "value", + ] + ) + + // Client sends messages + let deframedMessage = [UInt8]([1, 2, 3, 4]) + let completeMessage = try self.frameMessage(deframedMessage, compress: false) + // Split message into two parts to make sure the stitching together of the frames works well + let firstMessage = completeMessage.getSlice(at: 0, length: 4)! + let secondMessage = completeMessage.getSlice(at: 4, length: completeMessage.readableBytes - 4)! + + try stateMachine.receive(message: firstMessage, endStream: false) + XCTAssertNil(stateMachine.nextInboundMessage()) + try stateMachine.receive(message: secondMessage, endStream: false) + XCTAssertEqual(stateMachine.nextInboundMessage(), deframedMessage) - func testClientClosesBeforeServerOpens() { - // TODO: implement + // Server sends response + let firstResponse = [UInt8]([5, 6, 7]) + let secondResponse = [UInt8]([8, 9, 10]) + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) + try stateMachine.send(message: firstResponse, endStream: false) + try stateMachine.send(message: secondResponse, endStream: false) + + // Make sure messages are outbound + let framedMessages = try self.frameMessages([firstResponse, secondResponse], compress: false) + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .sendMessage(framedMessages)) + + // Client sends end + try stateMachine.receive(message: ByteBuffer(), endStream: true) + + // Server ends + let response = try stateMachine.send( + status: .init(code: .ok, message: ""), + metadata: [], + trailersOnly: false + ) + XCTAssertEqual(response, ["grpc-status": "0"]) + + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) + XCTAssertNil(stateMachine.nextInboundMessage()) } - func testClientClosesBeforeServerResponds() { - // TODO: implement + func testClientClosesBeforeServerOpens() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) + + // Client sends metadata + let receiveMetadataAction = try stateMachine.receive( + metadata: .clientInitialMetadata, + endStream: false + ) + XCTAssertEqual( + receiveMetadataAction, + .receivedMetadata(Metadata(headers: .clientInitialMetadata)) + ) + + // Client sends messages + let deframedMessage = [UInt8]([1, 2, 3, 4]) + let completeMessage = try self.frameMessage(deframedMessage, compress: false) + // Split message into two parts to make sure the stitching together of the frames works well + let firstMessage = completeMessage.getSlice(at: 0, length: 4)! + let secondMessage = completeMessage.getSlice(at: 4, length: completeMessage.readableBytes - 4)! + + try stateMachine.receive(message: firstMessage, endStream: false) + XCTAssertNil(stateMachine.nextInboundMessage()) + try stateMachine.receive(message: secondMessage, endStream: false) + XCTAssertEqual(stateMachine.nextInboundMessage(), deframedMessage) + + // Client sends end + try stateMachine.receive(message: ByteBuffer(), endStream: true) + + // Server sends initial metadata + let sentInitialHeaders = try stateMachine.send(metadata: Metadata(headers: ["custom": "value"])) + XCTAssertEqual( + sentInitialHeaders, + [ + "custom": "value", + ":status": "200", + "content-type": "application/grpc", + "grpc-accept-encoding": "deflate", + ] + ) + + // Server sends response + let firstResponse = [UInt8]([5, 6, 7]) + let secondResponse = [UInt8]([8, 9, 10]) + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) + try stateMachine.send(message: firstResponse, endStream: false) + try stateMachine.send(message: secondResponse, endStream: false) + + // Make sure messages are outbound + let framedMessages = try self.frameMessages([firstResponse, secondResponse], compress: false) + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .sendMessage(framedMessages)) + + // Server ends + let response = try stateMachine.send( + status: .init(code: .ok, message: ""), + metadata: [], + trailersOnly: false + ) + XCTAssertEqual(response, ["grpc-status": "0"]) + + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) + XCTAssertNil(stateMachine.nextInboundMessage()) } - func testServerRejectsRPC() { - // TODO: implement + func testClientClosesBeforeServerResponds() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) + + // Client sends metadata + let receiveMetadataAction = try stateMachine.receive( + metadata: .clientInitialMetadata, + endStream: false + ) + XCTAssertEqual( + receiveMetadataAction, + .receivedMetadata(Metadata(headers: .clientInitialMetadata)) + ) + + // Client sends messages + let deframedMessage = [UInt8]([1, 2, 3, 4]) + let completeMessage = try self.frameMessage(deframedMessage, compress: false) + // Split message into two parts to make sure the stitching together of the frames works well + let firstMessage = completeMessage.getSlice(at: 0, length: 4)! + let secondMessage = completeMessage.getSlice(at: 4, length: completeMessage.readableBytes - 4)! + + try stateMachine.receive(message: firstMessage, endStream: false) + XCTAssertNil(stateMachine.nextInboundMessage()) + try stateMachine.receive(message: secondMessage, endStream: false) + XCTAssertEqual(stateMachine.nextInboundMessage(), deframedMessage) + + // Server sends initial metadata + let sentInitialHeaders = try stateMachine.send(metadata: Metadata(headers: ["custom": "value"])) + XCTAssertEqual( + sentInitialHeaders, + [ + "custom": "value", + ":status": "200", + "content-type": "application/grpc", + "grpc-accept-encoding": "deflate", + ] + ) + + // Client sends end + try stateMachine.receive(message: ByteBuffer(), endStream: true) + + // Server sends response + let firstResponse = [UInt8]([5, 6, 7]) + let secondResponse = [UInt8]([8, 9, 10]) + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) + try stateMachine.send(message: firstResponse, endStream: false) + try stateMachine.send(message: secondResponse, endStream: false) + + // Make sure messages are outbound + let framedMessages = try self.frameMessages([firstResponse, secondResponse], compress: false) + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .sendMessage(framedMessages)) + + // Server ends + let response = try stateMachine.send( + status: .init(code: .ok, message: ""), + metadata: [], + trailersOnly: false + ) + XCTAssertEqual(response, ["grpc-status": "0"]) + + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) + XCTAssertNil(stateMachine.nextInboundMessage()) } } extension XCTestCase { func assertRejectedRPC( _ action: GRPCStreamStateMachine.OnMetadataReceived, - expression: (HPACKHeaders) -> Void - ) { - guard case .rejectRPC(let trailers) = action else { + expression: (Status?, HPACKHeaders?) throws -> Void + ) rethrows { + guard case .rejectRPC(let status, let trailers) = action else { XCTFail("RPC should have been rejected.") return } - expression(trailers) + try expression(status, trailers) + } + + func frameMessage(_ message: [UInt8], compress: Bool) throws -> ByteBuffer { + try frameMessages([message], compress: compress) + } + + func frameMessages(_ messages: any Sequence<[UInt8]>, compress: Bool) throws -> ByteBuffer { + var framer = GRPCMessageFramer() + let compressor: Zlib.Compressor? = { + if compress { + return Zlib.Compressor(method: .deflate) + } else { + return nil + } + }() + defer { compressor?.end() } + for message in messages { + framer.append(message) + } + return try XCTUnwrap(framer.next(compressor: compressor)) } } From 7396271b9c2c4a47f3cdf3baf1d6fb55208714ee Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Tue, 5 Mar 2024 11:22:41 +0000 Subject: [PATCH 29/51] Small PR change --- Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 9180bde76..6406e8593 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -311,10 +311,11 @@ struct GRPCStreamStateMachine { case .client: try self.clientSend(message: message, endStream: endStream) case .server: - assert( - !endStream, - "Can't end response stream by sending a message - send(status:metadata:trailersOnly:) must be called" - ) + guard !endStream else { + throw self.assertionFailureAndCreateRPCErrorOnInternalError( + "Can't end response stream by sending a message - send(status:metadata:trailersOnly:) must be called" + ) + } try self.serverSend(message: message) } } From 6ee3bd0b912c9f9285cfe0c95f9a72d9fc1b9698 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Tue, 5 Mar 2024 22:52:01 +0000 Subject: [PATCH 30/51] PR changes --- .../GRPCStreamStateMachine.swift | 768 ++++++++---------- .../GRPCHTTP2Core/Internal/ContentType.swift | 6 +- .../GRPCStatusMessageMarshaller.swift | 6 +- .../GRPCStreamStateMachineTests.swift | 207 ++--- .../GRPCStatusMessageMarshallerTests.swift | 44 + 5 files changed, 481 insertions(+), 550 deletions(-) create mode 100644 Tests/GRPCHTTP2CoreTests/Internal/GRPCStatusMessageMarshallerTests.swift diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 6406e8593..bbff10062 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -31,7 +31,7 @@ enum GRPCStreamStateMachineConfiguration { struct ClientConfiguration { var methodDescriptor: MethodDescriptor var scheme: Scheme - var outboundEncoding: CompressionAlgorithm? + var outboundEncoding: CompressionAlgorithm var acceptedEncodings: [CompressionAlgorithm] } @@ -54,9 +54,9 @@ enum GRPCStreamStateMachineState { let maximumPayloadSize: Int } - enum DecompressionConfiguration { + enum DecompressionState { case decompressionNotYetKnown - case decompression(CompressionAlgorithm?) + case decompression(CompressionAlgorithm) } struct ClientOpenServerIdleState { @@ -76,8 +76,8 @@ enum GRPCStreamStateMachineState { init( previousState: ClientIdleServerIdleState, - compressionAlgorithm: CompressionAlgorithm?, - decompressionConfiguration: DecompressionConfiguration + compressionAlgorithm: CompressionAlgorithm, + decompressionState: DecompressionState ) { self.maximumPayloadSize = previousState.maximumPayloadSize @@ -92,7 +92,10 @@ enum GRPCStreamStateMachineState { // sent it when starting the request. // In the case of the client, it will need to wait until the server responds // with its initial metadata. - if case .decompression(let decompressionAlgorithm) = decompressionConfiguration { + switch decompressionState { + case .decompressionNotYetKnown: + self.deframer = nil + case .decompression(let decompressionAlgorithm): if let zlibMethod = Zlib.Method(encoding: decompressionAlgorithm) { self.decompressor = Zlib.Decompressor(method: zlibMethod) } @@ -101,8 +104,6 @@ enum GRPCStreamStateMachineState { decompressor: self.decompressor ) self.deframer = NIOSingleStepByteToMessageProcessor(decoder) - } else { - self.deframer = nil } self.inboundMessageBuffer = .init() @@ -118,30 +119,38 @@ enum GRPCStreamStateMachineState { var inboundMessageBuffer: OneOrManyQueue<[UInt8]> + /// This should be called from the server path, as the deframer will already be configured in this scenario. + init(previousState: ClientOpenServerIdleState) { + self.framer = previousState.framer + self.compressor = previousState.compressor + + // In the case of the server, it will already have a deframer set up, + // because it already knows what encoding the client is using: + // it's okay to force-unwrap. + self.deframer = previousState.deframer! + self.decompressor = previousState.decompressor + + self.inboundMessageBuffer = previousState.inboundMessageBuffer + } + + /// This should only be called from the client path, as the deframer has not yet been set up. init( previousState: ClientOpenServerIdleState, - decompressionAlgorithm: CompressionAlgorithm? = nil + decompressionAlgorithm: CompressionAlgorithm ) { self.framer = previousState.framer self.compressor = previousState.compressor - // In the case of the server, it will already have a deframer set up, - // because it already knows what encoding the client is using. - // In the case of the client, it will only be able to set it up + // In the case of the client, it will only be able to set up the deframer // after it receives the chosen encoding from the server. - if let previousDeframer = previousState.deframer { - self.deframer = previousDeframer - self.decompressor = previousState.decompressor - } else { - if let zlibMethod = Zlib.Method(encoding: decompressionAlgorithm) { - self.decompressor = Zlib.Decompressor(method: zlibMethod) - } - let decoder = GRPCMessageDeframer( - maximumPayloadSize: previousState.maximumPayloadSize, - decompressor: self.decompressor - ) - self.deframer = NIOSingleStepByteToMessageProcessor(decoder) + if let zlibMethod = Zlib.Method(encoding: decompressionAlgorithm) { + self.decompressor = Zlib.Decompressor(method: zlibMethod) } + let decoder = GRPCMessageDeframer( + maximumPayloadSize: previousState.maximumPayloadSize, + decompressor: self.decompressor + ) + self.deframer = NIOSingleStepByteToMessageProcessor(decoder) self.inboundMessageBuffer = previousState.inboundMessageBuffer } @@ -190,6 +199,26 @@ enum GRPCStreamStateMachineState { var inboundMessageBuffer: OneOrManyQueue<[UInt8]> + /// We are closing the client as soon as it opens (i.e., endStream was set when receiving the client's + /// initial metadata). We don't need to know a decompression algorithm, since we won't receive + /// any more messages from the client anyways, as it's closed. + init( + previousState: ClientIdleServerIdleState, + compressionAlgorithm: CompressionAlgorithm + ) { + self.maximumPayloadSize = previousState.maximumPayloadSize + + if let zlibMethod = Zlib.Method(encoding: compressionAlgorithm) { + self.compressor = Zlib.Compressor(method: zlibMethod) + } + self.framer = GRPCMessageFramer() + self.outboundCompression = compressionAlgorithm + // We don't need a deframer since we won't receive any messages from the + // client: it's closed. + self.deframer = nil + self.inboundMessageBuffer = .init() + } + init(previousState: ClientOpenServerIdleState) { self.maximumPayloadSize = previousState.maximumPayloadSize self.framer = previousState.framer @@ -218,31 +247,40 @@ enum GRPCStreamStateMachineState { self.inboundMessageBuffer = previousState.inboundMessageBuffer } + /// This should be called from the server path, as the deframer will already be configured in this scenario. + init(previousState: ClientClosedServerIdleState) { + self.framer = previousState.framer + self.compressor = previousState.compressor + + // In the case of the server, it will already have a deframer set up, + // because it already knows what encoding the client is using: + // it's okay to force-unwrap. + self.deframer = previousState.deframer! + self.decompressor = previousState.decompressor + + self.inboundMessageBuffer = previousState.inboundMessageBuffer + } + + /// This should only be called from the client path, as the deframer has not yet been set up. init( previousState: ClientClosedServerIdleState, - decompressionAlgorithm: CompressionAlgorithm? = nil + decompressionAlgorithm: CompressionAlgorithm ) { self.framer = previousState.framer self.compressor = previousState.compressor - self.inboundMessageBuffer = previousState.inboundMessageBuffer - // In the case of the server, it will already have a deframer set up, - // because it already knows what encoding the client is using. - // In the case of the client, it will only be able to set it up + // In the case of the client, it will only be able to set up the deframer // after it receives the chosen encoding from the server. - if let previousDeframer = previousState.deframer { - self.deframer = previousDeframer - self.decompressor = previousState.decompressor - } else { - if let zlibMethod = Zlib.Method(encoding: decompressionAlgorithm) { - self.decompressor = Zlib.Decompressor(method: zlibMethod) - } - let decoder = GRPCMessageDeframer( - maximumPayloadSize: previousState.maximumPayloadSize, - decompressor: self.decompressor - ) - self.deframer = NIOSingleStepByteToMessageProcessor(decoder) + if let zlibMethod = Zlib.Method(encoding: decompressionAlgorithm) { + self.decompressor = Zlib.Decompressor(method: zlibMethod) } + let decoder = GRPCMessageDeframer( + maximumPayloadSize: previousState.maximumPayloadSize, + decompressor: self.decompressor + ) + self.deframer = NIOSingleStepByteToMessageProcessor(decoder) + + self.inboundMessageBuffer = previousState.inboundMessageBuffer } } @@ -312,7 +350,7 @@ struct GRPCStreamStateMachine { try self.clientSend(message: message, endStream: endStream) case .server: guard !endStream else { - throw self.assertionFailureAndCreateRPCErrorOnInternalError( + try self.invalidState( "Can't end response stream by sending a message - send(status:metadata:trailersOnly:) must be called" ) } @@ -324,7 +362,7 @@ struct GRPCStreamStateMachine { { switch self.configuration { case .client: - throw self.assertionFailureAndCreateRPCErrorOnInternalError( + try self.invalidState( "Client cannot send status and trailer." ) case .server: @@ -340,11 +378,11 @@ struct GRPCStreamStateMachine { case receivedMetadata(Metadata) // Client-specific actions - case failedRequest(Status) + case failedRequest(status: Status, metadata: Metadata) case doNothing // Server-specific actions - case rejectRPC(status: Status?, trailers: HPACKHeaders?) + case rejectRPC(trailers: HPACKHeaders) } mutating func receive(metadata: HPACKHeaders, endStream: Bool) throws -> OnMetadataReceived { @@ -389,7 +427,17 @@ struct GRPCStreamStateMachine { } } - mutating func nextInboundMessage() -> [UInt8]? { + /// The result of requesting the next inbound message. + enum OnNextInboundMessage: Equatable { + /// The sender is done writing messages and there are no more messages to be received. + case noMoreMessages + /// There isn't a message ready to be sent, but we could still receive more, so keep trying. + case awaitMoreMessages + /// A message has been received. + case receiveMessage([UInt8]) + } + + mutating func nextInboundMessage() -> OnNextInboundMessage { switch self.configuration { case .client: return self.clientNextInboundMessage() @@ -413,18 +461,18 @@ extension GRPCStreamStateMachine { // Add required headers // See https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests - headers.path = methodDescriptor - headers.scheme = scheme - headers.method = "POST" - headers.contentType = .protobuf - headers.te = "trailers" // Used to detect incompatible proxies - - if let encoding = outboundEncoding { - headers.encoding = encoding + headers.add(methodDescriptor.fullyQualifiedMethod, forKey: .path) + headers.add(scheme.rawValue, forKey: .scheme) + headers.add("POST", forKey: .method) + headers.add(ContentType.grpc.canonicalValue, forKey: .contentType) + headers.add("trailers", forKey: .te) // Used to detect incompatible proxies + + if let encoding = outboundEncoding, encoding != .identity { + headers.add(encoding.name, forKey: .encoding) } - if !acceptedEncodings.isEmpty { - headers.acceptedEncodings = acceptedEncodings + for acceptedEncoding in acceptedEncodings { + headers.add(acceptedEncoding.name, forKey: .acceptEncoding) } for metadataPair in customMetadata { @@ -445,7 +493,7 @@ extension GRPCStreamStateMachine { .init( previousState: state, compressionAlgorithm: configuration.outboundEncoding, - decompressionConfiguration: .decompressionNotYetKnown + decompressionState: .decompressionNotYetKnown ) ) return self.makeClientHeaders( @@ -456,11 +504,11 @@ extension GRPCStreamStateMachine { customMetadata: metadata ) case .clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed: - throw self.assertionFailureAndCreateRPCErrorOnInternalError( + try self.invalidState( "Client is already open: shouldn't be sending metadata." ) case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - throw self.assertionFailureAndCreateRPCErrorOnInternalError( + try self.invalidState( "Client is closed: can't send metadata." ) } @@ -470,7 +518,7 @@ extension GRPCStreamStateMachine { // Client sends message. switch self.state { case .clientIdleServerIdle: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client not yet open.") + try self.invalidState("Client not yet open.") case .clientOpenServerIdle(var state): state.framer.append(message) if endStream { @@ -492,7 +540,7 @@ extension GRPCStreamStateMachine { self.state = .clientClosedServerClosed(.init(previousState: state)) } case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - throw self.assertionFailureAndCreateRPCErrorOnInternalError( + try self.invalidState( "Client is closed, cannot send a message." ) } @@ -503,37 +551,39 @@ extension GRPCStreamStateMachine { private mutating func clientNextOutboundMessage() throws -> OnNextOutboundMessage { switch self.state { case .clientIdleServerIdle: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Client is not open yet.") + try self.invalidState("Client is not open yet.") case .clientOpenServerIdle(var state): let request = try state.framer.next(compressor: state.compressor) self.state = .clientOpenServerIdle(state) - return request != nil ? .sendMessage(request!) : .awaitMoreMessages + return request.map { .sendMessage($0) } ?? .awaitMoreMessages case .clientOpenServerOpen(var state): let request = try state.framer.next(compressor: state.compressor) self.state = .clientOpenServerOpen(state) - return request != nil ? .sendMessage(request!) : .awaitMoreMessages + return request.map { .sendMessage($0) } ?? .awaitMoreMessages case .clientClosedServerIdle(var state): let request = try state.framer.next(compressor: state.compressor) - self.state = .clientClosedServerIdle(state) if let request { + self.state = .clientClosedServerIdle(state) return .sendMessage(request) } else { // If the client is closed and there is no message to be sent, then we // are done sending messages, as we cannot call send(message:) anymore. // There are no more messages to be sent, so we can end the compressor. state.compressor?.end() + self.state = .clientClosedServerIdle(state) return .noMoreMessages } case .clientClosedServerOpen(var state): let request = try state.framer.next(compressor: state.compressor) - self.state = .clientClosedServerOpen(state) if let request { + self.state = .clientClosedServerOpen(state) return .sendMessage(request) } else { // If the client is closed and there is no message to be sent, then we // are done sending messages, as we cannot call send(message:) anymore. // There are no more messages to be sent, so we can end the compressor. state.compressor?.end() + self.state = .clientClosedServerOpen(state) return .noMoreMessages } case .clientOpenServerClosed(let state): @@ -548,6 +598,83 @@ extension GRPCStreamStateMachine { return .noMoreMessages } } + + private mutating func validateHeaders(_ metadata: HPACKHeaders) -> OnMetadataReceived? { + let grpcStatus = metadata.firstString(forKey: .grpcStatus) + .flatMap { Int($0) } + .flatMap { Status.Code(rawValue: $0) } + let httpStatus = metadata.firstString(forKey: .status) + guard grpcStatus != nil || httpStatus == "200" else { + let httpStatusCode = + httpStatus + .flatMap { Int($0) } + .map { HTTPResponseStatus(statusCode: $0) } + + guard let httpStatusCode else { + return .failedRequest( + status: .init(code: .unknown, message: "Unexpected non-200 HTTP Status Code."), + metadata: Metadata(headers: metadata) + ) + } + + if (100 ... 199).contains(httpStatusCode.code) { + // For 1xx status codes, the entire header should be skipped and a + // subsequent header should be read. + // See https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md + return .doNothing + } + + // Close the client if open, and forward the mapped status code. + if case .clientOpenServerIdle(let state) = self.state { + self.state = .clientClosedServerIdle(.init(previousState: state)) + } + return .failedRequest( + status: .init( + code: Status.Code(httpStatusCode: httpStatusCode), + message: "Unexpected non-200 HTTP Status Code." + ), + metadata: Metadata(headers: metadata) + ) + } + + let contentTypeHeader = metadata.first(name: GRPCHTTP2Keys.contentType.rawValue) + guard contentTypeHeader.flatMap(ContentType.init) != nil else { + return .failedRequest( + status: .init( + code: .internalError, + message: "Missing \(GRPCHTTP2Keys.contentType) header" + ), + metadata: Metadata(headers: metadata) + ) + } + + return nil + } + + private enum ProcessInboundEncodingResult { + case error(OnMetadataReceived) + case success(CompressionAlgorithm) + } + + private func processInboundEncoding(_ metadata: HPACKHeaders) -> ProcessInboundEncodingResult { + let inboundEncoding: CompressionAlgorithm + if let serverEncoding = metadata.first(name: GRPCHTTP2Keys.encoding.rawValue) { + guard let parsedEncoding = CompressionAlgorithm(rawValue: serverEncoding) else { + return .error(.failedRequest( + status: .init( + code: .internalError, + message: + "The server picked a compression algorithm ('\(serverEncoding)') the client does not know about." + ), + metadata: Metadata(headers: metadata) + )) + } + inboundEncoding = parsedEncoding + } else { + inboundEncoding = .identity + } + return .success(inboundEncoding) + } private mutating func clientReceive( metadata: HPACKHeaders, @@ -555,66 +682,28 @@ extension GRPCStreamStateMachine { ) throws -> OnMetadataReceived { switch self.state { case .clientOpenServerIdle(let state): - guard metadata.grpcStatus != nil || metadata.status == "200" else { - let httpStatusCode = metadata.status - .flatMap { Int($0) } - .map { HTTPResponseStatus(statusCode: $0) } - - guard let httpStatusCode else { - return .failedRequest( - .init(code: .unknown, message: "Unexpected non-200 HTTP Status Code.") - ) - } - - if (100 ... 199).contains(httpStatusCode.code) { - // For 1xx status codes, the entire header should be skipped and a - // subsequent header should be read. - // See https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md - return .doNothing - } - - // Close the client and forward the mapped status code. - self.state = .clientClosedServerIdle(.init(previousState: state)) - return .failedRequest( - .init( - code: Status.Code(httpStatusCode: httpStatusCode), - message: "Unexpected non-200 HTTP Status Code." - ) - ) - } - - let contentTypeHeader = metadata.first(name: GRPCHTTP2Keys.contentType.rawValue) - guard contentTypeHeader.flatMap(ContentType.init) != nil else { - return .failedRequest( - .init(code: .internalError, message: "Missing \(GRPCHTTP2Keys.contentType) header") - ) + if let failedValidation = self.validateHeaders(metadata) { + return failedValidation } if endStream { // This is a trailers-only response: close server. self.state = .clientOpenServerClosed(.init(previousState: state)) } else { - var inboundEncoding: CompressionAlgorithm? - if let serverEncoding = metadata.first(name: GRPCHTTP2Keys.encoding.rawValue) { - guard let parsedEncoding = CompressionAlgorithm(rawValue: serverEncoding) else { - return .failedRequest( - .init( - code: .internalError, - message: "The server picked a compression algorithm the client does not know about." - ) + switch self.processInboundEncoding(metadata) { + case .error(let failure): + return failure + case .success(let inboundEncoding): + self.state = .clientOpenServerOpen( + .init( + previousState: state, + decompressionAlgorithm: inboundEncoding ) - } - inboundEncoding = parsedEncoding - } - - self.state = .clientOpenServerOpen( - .init( - previousState: state, - decompressionAlgorithm: inboundEncoding ) - ) + } } return .receivedMetadata(Metadata(headers: metadata)) + case .clientOpenServerOpen(let state): if endStream { self.state = .clientOpenServerClosed(.init(previousState: state)) @@ -624,66 +713,30 @@ extension GRPCStreamStateMachine { () } return .receivedMetadata(Metadata(headers: metadata)) - case .clientClosedServerIdle(let state): - guard metadata.grpcStatus != nil || metadata.status == "200" else { - let httpStatusCode = metadata.status - .flatMap { Int($0) } - .map { HTTPResponseStatus(statusCode: $0) } - - guard let httpStatusCode else { - return .failedRequest( - .init(code: .unknown, message: "Unexpected non-200 HTTP Status Code.") - ) - } - if (100 ... 199).contains(httpStatusCode.code) { - // For 1xx status codes, the entire header should be skipped and a - // subsequent header should be read. - // See https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md - return .doNothing - } - - // Forward the mapped status code. - return .failedRequest( - .init( - code: Status.Code(httpStatusCode: httpStatusCode), - message: "Unexpected non-200 HTTP Status Code." - ) - ) - } - - let contentTypeHeader = metadata.first(name: GRPCHTTP2Keys.contentType.rawValue) - guard contentTypeHeader.flatMap(ContentType.init) != nil else { - return .failedRequest( - .init(code: .internalError, message: "Missing \(GRPCHTTP2Keys.contentType) header") - ) + case .clientClosedServerIdle(let state): + if let failedValidation = self.validateHeaders(metadata) { + return failedValidation } if endStream { - // This is a trailers-only response. + // This is a trailers-only response: close server. self.state = .clientClosedServerClosed(.init(previousState: state)) } else { - var inboundEncoding: CompressionAlgorithm? - if let serverEncoding = metadata.first(name: GRPCHTTP2Keys.encoding.rawValue) { - guard let parsedEncoding = CompressionAlgorithm(rawValue: serverEncoding) else { - return .failedRequest( - .init( - code: .internalError, - message: "The server picked a compression algorithm the client does not know about." - ) + switch self.processInboundEncoding(metadata) { + case .error(let failure): + return failure + case .success(let inboundEncoding): + self.state = .clientClosedServerOpen( + .init( + previousState: state, + decompressionAlgorithm: inboundEncoding ) - } - inboundEncoding = parsedEncoding - } - - self.state = .clientClosedServerOpen( - .init( - previousState: state, - decompressionAlgorithm: inboundEncoding ) - ) + } } return .receivedMetadata(Metadata(headers: metadata)) + case .clientClosedServerOpen(let state): if endStream { self.state = .clientClosedServerClosed(.init(previousState: state)) @@ -693,6 +746,7 @@ extension GRPCStreamStateMachine { () } return .receivedMetadata(Metadata(headers: metadata)) + case .clientClosedServerClosed: // We could end up here if we received a grpc-status header in a previous // frame (which would have already close the server) and then we receive @@ -701,17 +755,17 @@ extension GRPCStreamStateMachine { // Note that we don't want to ignore it if EOS is not set here though, as // then it would be an invalid payload. if !endStream || metadata.count > 0 { - throw self.assertionFailureAndCreateRPCErrorOnInternalError( + try self.invalidState( "Server is closed, nothing could have been sent." ) } - return .receivedMetadata([]) + return .doNothing case .clientIdleServerIdle: - throw self.assertionFailureAndCreateRPCErrorOnInternalError( + try self.invalidState( "Server cannot have sent metadata if the client is idle." ) case .clientOpenServerClosed: - throw self.assertionFailureAndCreateRPCErrorOnInternalError( + try self.invalidState( "Server is closed, nothing could have been sent." ) } @@ -721,11 +775,11 @@ extension GRPCStreamStateMachine { // This is a message received by the client, from the server. switch self.state { case .clientIdleServerIdle: - throw self.assertionFailureAndCreateRPCErrorOnInternalError( + try self.invalidState( "Cannot have received anything from server if client is not yet open." ) case .clientOpenServerIdle, .clientClosedServerIdle: - throw self.assertionFailureAndCreateRPCErrorOnInternalError( + try self.invalidState( "Server cannot have sent a message before sending the initial metadata." ) case .clientOpenServerOpen(var state): @@ -749,45 +803,42 @@ extension GRPCStreamStateMachine { self.state = .clientClosedServerOpen(state) } case .clientOpenServerClosed, .clientClosedServerClosed: - throw self.assertionFailureAndCreateRPCErrorOnInternalError( + try self.invalidState( "Cannot have received anything from a closed server." ) } } - private mutating func clientNextInboundMessage() -> [UInt8]? { + private mutating func clientNextInboundMessage() -> OnNextInboundMessage { switch self.state { case .clientOpenServerOpen(var state): let message = state.inboundMessageBuffer.pop() self.state = .clientOpenServerOpen(state) - return message + return message.map { .receiveMessage($0) } ?? .awaitMoreMessages case .clientOpenServerClosed(var state): let message = state.inboundMessageBuffer.pop() self.state = .clientOpenServerClosed(state) - return message + return message.map { .receiveMessage($0) } ?? .noMoreMessages case .clientClosedServerOpen(var state): let message = state.inboundMessageBuffer.pop() self.state = .clientClosedServerOpen(state) - return message + return message.map { .receiveMessage($0) } ?? .awaitMoreMessages case .clientClosedServerClosed(var state): let message = state.inboundMessageBuffer.pop() self.state = .clientClosedServerClosed(state) - return message + return message.map { .receiveMessage($0) } ?? .noMoreMessages case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: - return nil + return .awaitMoreMessages } } - private func assertionFailureAndCreateRPCErrorOnInternalError( - _ message: String, - line: UInt = #line - ) -> RPCError { + private func invalidState(_ message: String, line: UInt = #line) throws -> Never { if !self.skipAssertions { assertionFailure(message, line: line) } - return RPCError(code: .internalError, message: message) + throw RPCError(code: .internalError, message: message) } } @@ -805,15 +856,15 @@ extension GRPCStreamStateMachine { var headers = HPACKHeaders() headers.reserveCapacity(4 + customMetadata.count) - headers.status = "200" - headers.contentType = .protobuf + headers.add("200", forKey: .status) + headers.add(ContentType.grpc.canonicalValue, forKey: .contentType) - if let outboundEncoding { - headers.encoding = outboundEncoding + if let outboundEncoding, outboundEncoding != .identity { + headers.add(outboundEncoding.name, forKey: .encoding) } - if !configuration.acceptedEncodings.isEmpty { - headers.acceptedEncodings = configuration.acceptedEncodings + for acceptedEncoding in configuration.acceptedEncodings { + headers.add(acceptedEncoding.name, forKey: .acceptEncoding) } for metadataPair in customMetadata { @@ -844,15 +895,15 @@ extension GRPCStreamStateMachine { customMetadata: metadata ) case .clientIdleServerIdle: - throw self.assertionFailureAndCreateRPCErrorOnInternalError( + try self.invalidState( "Client cannot be idle if server is sending initial metadata: it must have opened." ) case .clientOpenServerClosed, .clientClosedServerClosed: - throw self.assertionFailureAndCreateRPCErrorOnInternalError( + try self.invalidState( "Server cannot send metadata if closed." ) case .clientOpenServerOpen, .clientClosedServerOpen: - throw self.assertionFailureAndCreateRPCErrorOnInternalError( + try self.invalidState( "Server has already sent initial metadata." ) } @@ -861,7 +912,7 @@ extension GRPCStreamStateMachine { private mutating func serverSend(message: [UInt8]) throws { switch self.state { case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: - throw self.assertionFailureAndCreateRPCErrorOnInternalError( + try self.invalidState( "Server must have sent initial metadata before sending a message." ) case .clientOpenServerOpen(var state): @@ -871,12 +922,23 @@ extension GRPCStreamStateMachine { state.framer.append(message) self.state = .clientClosedServerOpen(state) case .clientOpenServerClosed, .clientClosedServerClosed: - throw self.assertionFailureAndCreateRPCErrorOnInternalError( + try self.invalidState( "Server can't send a message if it's closed." ) } } + private func makeNon200StatusTrailers(_ customMetadata: Metadata?) -> HPACKHeaders { + assert(customMetadata != nil, "Something is very wrong") + var headers = HPACKHeaders() + for metadataPair in customMetadata! { + headers.add(name: metadataPair.key, value: metadataPair.value.encoded()) + } + let httpStatus = headers.firstString(forKey: .status) + assert(httpStatus != nil && httpStatus != "200") + return headers + } + private func makeTrailers( status: Status?, customMetadata: Metadata?, @@ -886,13 +948,7 @@ extension GRPCStreamStateMachine { // header. This should already be included in the custom metadata: assert // this and simply return those headers. guard let status else { - assert(customMetadata != nil, "Something is very wrong") - var headers = HPACKHeaders() - for metadataPair in customMetadata! { - headers.add(name: metadataPair.key, value: metadataPair.value.encoded()) - } - assert(headers.status != nil && headers.status != "200") - return headers + return makeNon200StatusTrailers(customMetadata) } // Trailers always contain the grpc-status header, and optionally, @@ -905,18 +961,20 @@ extension GRPCStreamStateMachine { // Reserve 4 for capacity: 3 for the required headers, and 1 for the // optional status message. headers.reserveCapacity(4 + customMetadataCount) - headers.status = "200" - headers.contentType = .protobuf + headers.add("200", forKey: .status) + headers.add(ContentType.grpc.canonicalValue, forKey: .contentType) } else { // Reserve 2 for capacity: one for the required grpc-status, and // one for the optional message. headers.reserveCapacity(2 + customMetadataCount) } - headers.grpcStatus = status.code + headers.add(String(status.code.rawValue), forKey: .grpcStatus) if !status.message.isEmpty { - headers.grpcStatusMessage = status.message + if let percentEncodedMessage = GRPCStatusMessageMarshaller.marshall(status.message) { + headers.add(percentEncodedMessage, forKey: .grpcStatusMessage) + } } if let customMetadata { @@ -950,11 +1008,11 @@ extension GRPCStreamStateMachine { trailersOnly: trailersOnly ) case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: - throw self.assertionFailureAndCreateRPCErrorOnInternalError( + try self.invalidState( "Server can't send status if idle." ) case .clientOpenServerClosed, .clientClosedServerClosed: - throw self.assertionFailureAndCreateRPCErrorOnInternalError( + try self.invalidState( "Server can't send anything if closed." ) } @@ -967,28 +1025,24 @@ extension GRPCStreamStateMachine { ) throws -> OnMetadataReceived { switch self.state { case .clientIdleServerIdle(let state): - if endStream { - throw self.assertionFailureAndCreateRPCErrorOnInternalError( - """ - Client should have opened before ending the stream: \ - stream shouldn't have been closed when sending initial metadata. - """ - ) - } - - guard metadata.contentType != nil else { + let contentType = metadata.firstString(forKey: .contentType) + .flatMap { ContentType(value: $0) } + guard contentType != nil else { // Respond with HTTP-level Unsupported Media Type status code. var trailers = HPACKHeaders() - trailers.status = "415" - return .rejectRPC(status: nil, trailers: trailers) + trailers.add("415", forKey: .status) + return .rejectRPC(trailers: trailers) } - guard metadata.path != nil else { + let path = metadata.firstString(forKey: .path) + .flatMap { MethodDescriptor(fullyQualifiedMethod: $0) } + guard path != nil else { let status = Status( code: .unimplemented, message: "No \(GRPCHTTP2Keys.path.rawValue) header has been set." ) - return .rejectRPC(status: status, trailers: nil) + let trailers = self.makeTrailers(status: status, customMetadata: nil, trailersOnly: true) + return .rejectRPC(trailers: trailers) } func isIdentityOrCompatibleEncoding(_ clientEncoding: CompressionAlgorithm) -> Bool { @@ -997,7 +1051,7 @@ extension GRPCStreamStateMachine { // Firstly, find out if we support the client's chosen encoding, and reject // the RPC if we don't. - var inboundEncoding: CompressionAlgorithm? = nil + let inboundEncoding: CompressionAlgorithm let encodingValues = metadata.values( forHeader: GRPCHTTP2Keys.encoding.rawValue, canonicalForm: true @@ -1009,7 +1063,8 @@ extension GRPCStreamStateMachine { code: .internalError, message: "\(GRPCHTTP2Keys.encoding) must contain no more than one value." ) - return .rejectRPC(status: status, trailers: nil) + let trailers = self.makeTrailers(status: status, customMetadata: nil, trailersOnly: true) + return .rejectRPC(trailers: trailers) } guard let clientEncoding = CompressionAlgorithm(rawValue: String(rawEncoding)), @@ -1017,10 +1072,9 @@ extension GRPCStreamStateMachine { else { if configuration.acceptedEncodings.isEmpty { let status = Status(code: .unimplemented, message: "Compression is not supported") - return .rejectRPC(status: status, trailers: nil) + let trailers = self.makeTrailers(status: status, customMetadata: nil, trailersOnly: true) + return .rejectRPC(trailers: trailers) } else { - var trailers = HPACKHeaders() - trailers.reserveCapacity(1) let status = Status( code: .unimplemented, message: """ @@ -1028,46 +1082,64 @@ extension GRPCStreamStateMachine { supported algorithms are listed in grpc-accept-encoding """ ) - trailers.acceptedEncodings = configuration.acceptedEncodings - return .rejectRPC(status: status, trailers: trailers) + var customTrailers = Metadata() + for acceptedEncoding in configuration.acceptedEncodings { + customTrailers.addString(acceptedEncoding.name, forKey: GRPCHTTP2Keys.acceptEncoding.rawValue) + } + let trailers = self.makeTrailers(status: status, customMetadata: customTrailers, trailersOnly: true) + return .rejectRPC(trailers: trailers) } } // Server supports client's encoding. - // If it's identity, just skip it altogether. - if clientEncoding != .identity { - inboundEncoding = clientEncoding - } + inboundEncoding = clientEncoding + } else { + inboundEncoding = .identity } // Secondly, find a compatible encoding the server can use to compress outbound messages, // based on the encodings the client has advertised. - var outboundEncoding: CompressionAlgorithm? = nil - if let clientAdvertisedEncodings = metadata.acceptedEncodings { + let outboundEncoding: CompressionAlgorithm + let clientAdvertisedEncodings = metadata.values( + forHeader: GRPCHTTP2Keys.acceptEncoding.rawValue, + canonicalForm: true + ) + .compactMap { CompressionAlgorithm(rawValue: String($0)) } + if !clientAdvertisedEncodings.isEmpty { // Find the preferred encoding and use it to compress responses. // If it's identity, just skip it altogether, since we won't be // compressing. outboundEncoding = clientAdvertisedEncodings - .first { isIdentityOrCompatibleEncoding($0) } - .flatMap { $0 == .identity ? nil : $0 } + .first { isIdentityOrCompatibleEncoding($0) } ?? .identity + } else { + outboundEncoding = .identity } - self.state = .clientOpenServerIdle( - .init( - previousState: state, - compressionAlgorithm: outboundEncoding, - decompressionConfiguration: .decompression(inboundEncoding) + if endStream { + self.state = .clientClosedServerIdle( + .init( + previousState: state, + compressionAlgorithm: outboundEncoding + ) ) - ) + } else { + self.state = .clientOpenServerIdle( + .init( + previousState: state, + compressionAlgorithm: outboundEncoding, + decompressionState: .decompression(inboundEncoding) + ) + ) + } return .receivedMetadata(Metadata(headers: metadata)) case .clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed: - throw self.assertionFailureAndCreateRPCErrorOnInternalError( + try self.invalidState( "Client shouldn't have sent metadata twice." ) case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - throw self.assertionFailureAndCreateRPCErrorOnInternalError( + try self.invalidState( "Client can't have sent metadata if closed." ) } @@ -1076,13 +1148,12 @@ extension GRPCStreamStateMachine { private mutating func serverReceive(bytes: ByteBuffer, endStream: Bool) throws { switch self.state { case .clientIdleServerIdle: - throw self.assertionFailureAndCreateRPCErrorOnInternalError( + try self.invalidState( "Can't have received a message if client is idle." ) case .clientOpenServerIdle(var state): // Deframer must be present on the server side, as we know the decompression // algorithm from the moment the client opens. - assert(state.deframer != nil) try state.deframer!.process(buffer: bytes) { deframedMessage in state.inboundMessageBuffer.append(deframedMessage) } @@ -1102,12 +1173,15 @@ extension GRPCStreamStateMachine { } else { self.state = .clientOpenServerOpen(state) } - case .clientOpenServerClosed: + case .clientOpenServerClosed(let state): // Client is not done sending request, but server has already closed. - // Ignore the rest of the request: do nothing. - () + // Ignore the rest of the request: do nothing, unless endStream is set, + // in which case close the client. + if endStream { + self.state = .clientClosedServerClosed(.init(previousState: state)) + } case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - throw self.assertionFailureAndCreateRPCErrorOnInternalError( + try self.invalidState( "Client can't send a message if closed." ) } @@ -1116,62 +1190,66 @@ extension GRPCStreamStateMachine { private mutating func serverNextOutboundMessage() throws -> OnNextOutboundMessage { switch self.state { case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: - throw self.assertionFailureAndCreateRPCErrorOnInternalError("Server is not open yet.") + try self.invalidState("Server is not open yet.") case .clientOpenServerOpen(var state): let response = try state.framer.next(compressor: state.compressor) self.state = .clientOpenServerOpen(state) - return response != nil ? .sendMessage(response!) : .awaitMoreMessages + return response.map { .sendMessage($0) } ?? .awaitMoreMessages case .clientClosedServerOpen(var state): let response = try state.framer.next(compressor: state.compressor) self.state = .clientClosedServerOpen(state) - return response != nil ? .sendMessage(response!) : .awaitMoreMessages + return response.map { .sendMessage($0) } ?? .awaitMoreMessages case .clientOpenServerClosed(var state): let response = try state.framer.next(compressor: state.compressor) - self.state = .clientOpenServerClosed(state) if let response { + self.state = .clientOpenServerClosed(state) return .sendMessage(response) } else { // There are no more messages to be sent, so we can end the compressor. state.compressor?.end() + self.state = .clientOpenServerClosed(state) return .noMoreMessages } case .clientClosedServerClosed(var state): let response = try state.framer.next(compressor: state.compressor) - self.state = .clientClosedServerClosed(state) if let response { + self.state = .clientClosedServerClosed(state) return .sendMessage(response) } else { // There are no more messages to be sent, so we can end the compressor. state.compressor?.end() + self.state = .clientClosedServerClosed(state) return .noMoreMessages } } } - private mutating func serverNextInboundMessage() -> [UInt8]? { + private mutating func serverNextInboundMessage() -> OnNextInboundMessage { switch self.state { case .clientOpenServerIdle(var state): let request = state.inboundMessageBuffer.pop() self.state = .clientOpenServerIdle(state) - return request + return request.map { .receiveMessage($0) } ?? .awaitMoreMessages case .clientOpenServerOpen(var state): let request = state.inboundMessageBuffer.pop() self.state = .clientOpenServerOpen(state) - return request + return request.map { .receiveMessage($0) } ?? .awaitMoreMessages case .clientOpenServerClosed(var state): let request = state.inboundMessageBuffer.pop() self.state = .clientOpenServerClosed(state) - return request + return request.map { .receiveMessage($0) } ?? .awaitMoreMessages case .clientClosedServerOpen(var state): let request = state.inboundMessageBuffer.pop() self.state = .clientClosedServerOpen(state) - return request + return request.map { .receiveMessage($0) } ?? .noMoreMessages case .clientClosedServerClosed(var state): let request = state.inboundMessageBuffer.pop() self.state = .clientClosedServerClosed(state) - return request - case .clientClosedServerIdle, .clientIdleServerIdle: - return nil + return request.map { .receiveMessage($0) } ?? .noMoreMessages + case .clientClosedServerIdle: + return .noMoreMessages + case .clientIdleServerIdle: + return .awaitMoreMessages } } } @@ -1200,170 +1278,19 @@ internal enum GRPCHTTP2Keys: String { } extension HPACKHeaders { - var path: MethodDescriptor? { - get { - self.firstString(forKey: .path) - .flatMap { MethodDescriptor(fullyQualifiedMethod: $0) } - } - set { - if let newValue { - self.add(newValue.fullyQualifiedMethod, forKey: .path) - } else { - self.removeAllValues(forKey: .path) - } - } - } - - var contentType: ContentType? { - get { - self.firstString(forKey: .contentType) - .flatMap { ContentType(value: $0) } - } - set { - if let newValue { - self.add(newValue.canonicalValue, forKey: .contentType) - } else { - self.removeAllValues(forKey: .contentType) - } - } - } - - var encoding: CompressionAlgorithm? { - get { - self.firstString(forKey: .encoding) - .flatMap { CompressionAlgorithm(rawValue: $0) } - } - set { - if let newValue { - self.add(newValue.name, forKey: .encoding) - } else { - self.removeAllValues(forKey: .encoding) - } - } - } - - var acceptedEncodings: [CompressionAlgorithm]? { - get { - self.values(forHeader: GRPCHTTP2Keys.acceptEncoding.rawValue, canonicalForm: true) - .compactMap { CompressionAlgorithm(rawValue: String($0)) } - } - set { - if let newValue { - for value in newValue { - self.add(value.name, forKey: .acceptEncoding) - } - } else { - self.removeAllValues(forKey: .acceptEncoding) - } - } - } - - var scheme: Scheme? { - get { - self.firstString(forKey: .scheme).flatMap { Scheme(rawValue: $0) } - } - set { - if let newValue { - self.add(newValue.rawValue, forKey: .scheme) - } else { - self.removeAllValues(forKey: .scheme) - } - } - } - - var method: String? { - get { - self.firstString(forKey: .method) - } - set { - if let newValue { - self.add(newValue, forKey: .method) - } else { - self.removeAllValues(forKey: .method) - } - } - } - - var te: String? { - get { - self.firstString(forKey: .te) - } - set { - if let newValue { - self.add(newValue, forKey: .te) - } else { - self.removeAllValues(forKey: .te) - } - } - } - - var status: String? { - get { - self.firstString(forKey: .status) - } - set { - if let newValue { - self.add(newValue, forKey: .status) - } else { - self.removeAllValues(forKey: .status) - } - } - } - - var grpcStatus: Status.Code? { - get { - self.firstString(forKey: .grpcStatus) - .flatMap { Int($0) } - .flatMap { Status.Code(rawValue: $0) } - } - set { - if let newValue { - self.add(String(newValue.rawValue), forKey: .grpcStatus) - } else { - self.removeAllValues(forKey: .grpcStatus) - } - } - } - - var grpcStatusMessage: String? { - get { - if let message = self.firstString(forKey: .grpcStatusMessage) { - return GRPCStatusMessageMarshaller.unmarshall(message) - } - return nil - } - set { - if let newValue { - if let percentEncodedMessage = GRPCStatusMessageMarshaller.marshall(newValue) { - self.add(percentEncodedMessage, forKey: .grpcStatusMessage) - } - } else { - self.removeAllValues(forKey: .grpcStatusMessage) - } - } - } - - private func firstString(forKey key: GRPCHTTP2Keys) -> String? { + internal func firstString(forKey key: GRPCHTTP2Keys) -> String? { self.values(forHeader: key.rawValue, canonicalForm: true).first(where: { _ in true }).map { String($0) } } - private mutating func add(_ value: String, forKey key: GRPCHTTP2Keys) { + internal mutating func add(_ value: String, forKey key: GRPCHTTP2Keys) { self.add(name: key.rawValue, value: value) } - - private mutating func removeAllValues(forKey key: GRPCHTTP2Keys) { - self.remove(name: key.rawValue) - } } extension Zlib.Method { - init?(encoding: CompressionAlgorithm?) { - guard let encoding else { - return nil - } - + init?(encoding: CompressionAlgorithm) { switch encoding { case .identity: return nil @@ -1389,9 +1316,8 @@ extension Metadata { metadata.addString(header.value, forKey: header.name) } } else { - if header.name == GRPCHTTP2Keys.grpcStatusMessage.rawValue, - let decodedStatusMessage = headers.grpcStatusMessage - { + if header.name == GRPCHTTP2Keys.grpcStatusMessage.rawValue { + let decodedStatusMessage = GRPCStatusMessageMarshaller.unmarshall(header.value) metadata.addString(decodedStatusMessage, forKey: header.name) } else { metadata.addString(header.value, forKey: header.name) diff --git a/Sources/GRPCHTTP2Core/Internal/ContentType.swift b/Sources/GRPCHTTP2Core/Internal/ContentType.swift index f3377c133..2e098d39f 100644 --- a/Sources/GRPCHTTP2Core/Internal/ContentType.swift +++ b/Sources/GRPCHTTP2Core/Internal/ContentType.swift @@ -17,13 +17,13 @@ // See: // - https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md enum ContentType { - case protobuf + case grpc init?(value: String) { switch value { case "application/grpc", "application/grpc+proto": - self = .protobuf + self = .grpc default: return nil @@ -32,7 +32,7 @@ enum ContentType { var canonicalValue: String { switch self { - case .protobuf: + case .grpc: // This is more widely supported than "application/grpc+proto" return "application/grpc" } diff --git a/Sources/GRPCHTTP2Core/Internal/GRPCStatusMessageMarshaller.swift b/Sources/GRPCHTTP2Core/Internal/GRPCStatusMessageMarshaller.swift index a9d5d63ce..3db35b5d2 100644 --- a/Sources/GRPCHTTP2Core/Internal/GRPCStatusMessageMarshaller.swift +++ b/Sources/GRPCHTTP2Core/Internal/GRPCStatusMessageMarshaller.swift @@ -15,12 +15,12 @@ */ // swiftformat:disable:next enumNamespaces -public struct GRPCStatusMessageMarshaller { +enum GRPCStatusMessageMarshaller { /// Adds percent encoding to the given message. /// /// - Parameter message: Message to percent encode. /// - Returns: Percent encoded string, or `nil` if it could not be encoded. - public static func marshall(_ message: String) -> String? { + static func marshall(_ message: String) -> String? { return percentEncode(message) } @@ -29,7 +29,7 @@ public struct GRPCStatusMessageMarshaller { /// - Parameter message: Message to remove encoding from. /// - Returns: The string with percent encoding removed, or the input string if the encoding /// could not be removed. - public static func unmarshall(_ message: String) -> String { + static func unmarshall(_ message: String) -> String { return removePercentEncoding(message) } } diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index e056d18ec..3ae6bb0ca 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -72,18 +72,18 @@ extension HPACKHeaders { // Server static let serverInitialMetadata: Self = [ GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, + GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, GRPCHTTP2Keys.acceptEncoding.rawValue: "deflate", ] static let serverInitialMetadataWithDeflateCompression: Self = [ GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, + GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, GRPCHTTP2Keys.encoding.rawValue: "deflate", GRPCHTTP2Keys.acceptEncoding.rawValue: "deflate", ] static let serverTrailers: Self = [ GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, + GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, GRPCHTTP2Keys.grpcStatus.rawValue: "0", ] } @@ -98,7 +98,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { .init( methodDescriptor: .init(service: "test", method: "test"), scheme: .http, - outboundEncoding: compressionEnabled ? .deflate : nil, + outboundEncoding: compressionEnabled ? .deflate : .identity, acceptedEncodings: [.deflate] ) ), @@ -281,7 +281,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { let action = try stateMachine.receive( metadata: [ GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, + GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, GRPCHTTP2Keys.encoding.rawValue: "deflate", "custom": "123", "custom-bin": String(base64Encoding: [42, 43, 44]), @@ -344,7 +344,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { let action = try stateMachine.receive( metadata: [ GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: ContentType.protobuf.canonicalValue, + GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, GRPCHTTP2Keys.encoding.rawValue: "deflate", "custom": "123", ], @@ -591,7 +591,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { TargetStateMachineState.clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle, ] { var stateMachine = self.makeClientStateMachine(targetState: targetState) - XCTAssertNil(stateMachine.nextInboundMessage()) + XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) } } @@ -605,10 +605,8 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { ]) try stateMachine.receive(message: receivedBytes, endStream: false) - let receivedMessage = try XCTUnwrap(stateMachine.nextInboundMessage()) - XCTAssertEqual(receivedMessage, [42, 42]) - - XCTAssertNil(stateMachine.nextInboundMessage()) + XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage([42, 42])) + XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) } func testNextInboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { @@ -621,10 +619,8 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { let receivedBytes = try self.frameMessage(originalMessage, compress: true) try stateMachine.receive(message: receivedBytes, endStream: false) - let receivedMessage = try XCTUnwrap(stateMachine.nextInboundMessage()) - XCTAssertEqual(receivedMessage, originalMessage) - - XCTAssertNil(stateMachine.nextInboundMessage()) + XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(originalMessage)) + XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) } func testNextInboundMessageWhenClientOpenAndServerClosed() throws { @@ -640,10 +636,8 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Close server XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) - let receivedMessage = try XCTUnwrap(stateMachine.nextInboundMessage()) - XCTAssertEqual(receivedMessage, [42, 42]) - - XCTAssertNil(stateMachine.nextInboundMessage()) + XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage([42, 42])) + XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) } func testNextInboundMessageWhenClientClosedAndServerOpen() throws { @@ -661,10 +655,8 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Even though the client is closed, because it received a message while open, // we must get the message now. - let receivedMessage = try XCTUnwrap(stateMachine.nextInboundMessage()) - XCTAssertEqual(receivedMessage, [42, 42]) - - XCTAssertNil(stateMachine.nextInboundMessage()) + XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage([42, 42])) + XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) } func testNextInboundMessageWhenClientClosedAndServerClosed() throws { @@ -685,10 +677,8 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Even though the client is closed, because it received a message while open, // we must get the message now. - let receivedMessage = try XCTUnwrap(stateMachine.nextInboundMessage()) - XCTAssertEqual(receivedMessage, [42, 42]) - - XCTAssertNil(stateMachine.nextInboundMessage()) + XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage([42, 42])) + XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) } // - MARK: Common paths @@ -734,7 +724,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) // Server sends response - XCTAssertNil(stateMachine.nextInboundMessage()) + XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) let firstResponseBytes = [UInt8]([5, 6, 7]) let firstResponse = try self.frameMessage(firstResponseBytes, compress: false) @@ -744,9 +734,9 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { try stateMachine.receive(message: secondResponse, endStream: false) // Make sure messages have arrived - XCTAssertEqual(stateMachine.nextInboundMessage(), firstResponseBytes) - XCTAssertEqual(stateMachine.nextInboundMessage(), secondResponseBytes) - XCTAssertNil(stateMachine.nextInboundMessage()) + XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(firstResponseBytes)) + XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(secondResponseBytes)) + XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) // Client sends end try stateMachine.send(message: [], endStream: true) @@ -759,7 +749,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertEqual(metadataReceivedAction, .receivedMetadata(Metadata(headers: .serverTrailers))) XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) - XCTAssertNil(stateMachine.nextInboundMessage()) + XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) } func testClientClosesBeforeServerOpens() throws { @@ -803,7 +793,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { ) // Server sends response - XCTAssertNil(stateMachine.nextInboundMessage()) + XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) let firstResponseBytes = [UInt8]([5, 6, 7]) let firstResponse = try self.frameMessage(firstResponseBytes, compress: false) @@ -813,9 +803,9 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { try stateMachine.receive(message: secondResponse, endStream: false) // Make sure messages have arrived - XCTAssertEqual(stateMachine.nextInboundMessage(), firstResponseBytes) - XCTAssertEqual(stateMachine.nextInboundMessage(), secondResponseBytes) - XCTAssertNil(stateMachine.nextInboundMessage()) + XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(firstResponseBytes)) + XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(secondResponseBytes)) + XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) // Server ends let metadataReceivedAction = try stateMachine.receive( @@ -825,7 +815,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertEqual(metadataReceivedAction, .receivedMetadata(Metadata(headers: .serverTrailers))) XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) - XCTAssertNil(stateMachine.nextInboundMessage()) + XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) } func testClientClosesBeforeServerResponds() throws { @@ -872,7 +862,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { try stateMachine.send(message: [], endStream: true) // Server sends response - XCTAssertNil(stateMachine.nextInboundMessage()) + XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) let firstResponseBytes = [UInt8]([5, 6, 7]) let firstResponse = try self.frameMessage(firstResponseBytes, compress: false) @@ -882,9 +872,9 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { try stateMachine.receive(message: secondResponse, endStream: false) // Make sure messages have arrived - XCTAssertEqual(stateMachine.nextInboundMessage(), firstResponseBytes) - XCTAssertEqual(stateMachine.nextInboundMessage(), secondResponseBytes) - XCTAssertNil(stateMachine.nextInboundMessage()) + XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(firstResponseBytes)) + XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(secondResponseBytes)) + XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) // Server ends let metadataReceivedAction = try stateMachine.receive( @@ -894,7 +884,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertEqual(metadataReceivedAction, .receivedMetadata(Metadata(headers: .serverTrailers))) XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) - XCTAssertNil(stateMachine.nextInboundMessage()) + XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) } } @@ -1263,25 +1253,14 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { ) } - func testReceiveMetadataWhenClientIdleAndServerIdle_WithEndStream() { + func testReceiveMetadataWhenClientIdleAndServerIdle_WithEndStream() throws { var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) - // If endStream is set, we should fail, because the client can only close by - // sending a message with endStream set. If they send metadata it has to be - // to open the stream (initial metadata). - XCTAssertThrowsError( - ofType: RPCError.self, - try stateMachine.receive(metadata: .clientInitialMetadata, endStream: true) - ) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual( - error.message, - """ - Client should have opened before ending the stream: \ - stream shouldn't have been closed when sending initial metadata. - """ - ) - } + let action = try stateMachine.receive(metadata: .clientInitialMetadata, endStream: true) + XCTAssertEqual( + action, + .receivedMetadata(Metadata(headers: .clientInitialMetadata)) + ) } func testReceiveMetadataWhenClientIdleAndServerIdle_MissingContentType() throws { @@ -1292,10 +1271,9 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { endStream: false ) - try self.assertRejectedRPC(action) { grpcStatus, trailers in - let unwrappedTrailers = try XCTUnwrap(trailers) - XCTAssertEqual(unwrappedTrailers.count, 1) - XCTAssertEqual(unwrappedTrailers.status, "415") + self.assertRejectedRPC(action) { trailers in + XCTAssertEqual(trailers.count, 1) + XCTAssertEqual(trailers.firstString(forKey: .status), "415") } } @@ -1307,10 +1285,9 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { endStream: false ) - try self.assertRejectedRPC(action) { grpcStatus, trailers in - let unwrappedTrailers = try XCTUnwrap(trailers) - XCTAssertEqual(unwrappedTrailers.count, 1) - XCTAssertEqual(unwrappedTrailers.status, "415") + self.assertRejectedRPC(action) { trailers in + XCTAssertEqual(trailers.count, 1) + XCTAssertEqual(trailers.firstString(forKey: .status), "415") } } @@ -1322,11 +1299,13 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { endStream: false ) - try self.assertRejectedRPC(action) { grpcStatus, trailers in - XCTAssertNil(trailers) - let unwrappedStatus = try XCTUnwrap(grpcStatus) - XCTAssertEqual(unwrappedStatus.code, .unimplemented) - XCTAssertEqual(unwrappedStatus.message, "No :path header has been set.") + self.assertRejectedRPC(action) { trailers in + XCTAssertEqual(trailers, [ + ":status": "200", + "content-type": "application/grpc", + "grpc-status": "12", + "grpc-status-message": "No :path header has been set." + ]) } } @@ -1340,20 +1319,14 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { endStream: false ) - try self.assertRejectedRPC(action) { grpcStatus, trailers in - let unwrappedTrailers = try XCTUnwrap(trailers) - XCTAssertEqual(unwrappedTrailers.count, 1) - XCTAssertEqual(unwrappedTrailers.acceptedEncodings, [.deflate]) - - let unwrappedStatus = try XCTUnwrap(grpcStatus) - XCTAssertEqual(unwrappedStatus.code, .unimplemented) - XCTAssertEqual( - unwrappedStatus.message, - """ - gzip compression is not supported; \ - supported algorithms are listed in grpc-accept-encoding - """ - ) + self.assertRejectedRPC(action) { trailers in + XCTAssertEqual(trailers, [ + ":status": "200", + "content-type": "application/grpc", + "grpc-accept-encoding": "deflate", + "grpc-status": "12", + "grpc-status-message": "gzip compression is not supported; supported algorithms are listed in grpc-accept-encoding" + ]) } } @@ -1695,13 +1668,12 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { func testNextInboundMessageWhenClientIdleAndServerIdle() { var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) - XCTAssertNil(stateMachine.nextInboundMessage()) + XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) } func testNextInboundMessageWhenClientOpenAndServerIdle() { var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerIdle) - - XCTAssertNil(stateMachine.nextInboundMessage()) + XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) } func testNextInboundMessageWhenClientOpenAndServerOpen() throws { @@ -1714,10 +1686,8 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { ]) try stateMachine.receive(message: receivedBytes, endStream: false) - let receivedMessage = try XCTUnwrap(stateMachine.nextInboundMessage()) - XCTAssertEqual(receivedMessage, [42, 42]) - - XCTAssertNil(stateMachine.nextInboundMessage()) + XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage([42, 42])) + XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) } func testNextInboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { @@ -1731,10 +1701,8 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { try stateMachine.receive(message: receivedBytes, endStream: false) - let receivedMessage = try XCTUnwrap(stateMachine.nextInboundMessage()) - XCTAssertEqual(receivedMessage, originalMessage) - - XCTAssertNil(stateMachine.nextInboundMessage()) + XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(originalMessage)) + XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) } func testNextInboundMessageWhenClientOpenAndServerClosed() throws { @@ -1756,16 +1724,13 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { ) ) - let receivedMessage = try XCTUnwrap(stateMachine.nextInboundMessage()) - XCTAssertEqual(receivedMessage, [42, 42]) - - XCTAssertNil(stateMachine.nextInboundMessage()) + XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage([42, 42])) + XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) } func testNextInboundMessageWhenClientClosedAndServerIdle() { var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerIdle) - - XCTAssertNil(stateMachine.nextInboundMessage()) + XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) } func testNextInboundMessageWhenClientClosedAndServerOpen() throws { @@ -1783,10 +1748,8 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Even though the client is closed, because the server received a message // while it was still open, we must get the message now. - let receivedMessage = try XCTUnwrap(stateMachine.nextInboundMessage()) - XCTAssertEqual(receivedMessage, [42, 42]) - - XCTAssertNil(stateMachine.nextInboundMessage()) + XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage([42, 42])) + XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) } func testNextInboundMessageWhenClientClosedAndServerClosed() throws { @@ -1813,10 +1776,8 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Even though the client and server are closed, because the server received // a message while the client was still open, we must get the message now. - let receivedMessage = try XCTUnwrap(stateMachine.nextInboundMessage()) - XCTAssertEqual(receivedMessage, [42, 42]) - - XCTAssertNil(stateMachine.nextInboundMessage()) + XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage([42, 42])) + XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) } // - MARK: Common paths @@ -1854,9 +1815,9 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { let secondMessage = completeMessage.getSlice(at: 4, length: completeMessage.readableBytes - 4)! try stateMachine.receive(message: firstMessage, endStream: false) - XCTAssertNil(stateMachine.nextInboundMessage()) + XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) try stateMachine.receive(message: secondMessage, endStream: false) - XCTAssertEqual(stateMachine.nextInboundMessage(), deframedMessage) + XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(deframedMessage)) // Server sends response let firstResponse = [UInt8]([5, 6, 7]) @@ -1881,7 +1842,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertEqual(response, ["grpc-status": "0"]) XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) - XCTAssertNil(stateMachine.nextInboundMessage()) + XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) } func testClientClosesBeforeServerOpens() throws { @@ -1905,9 +1866,9 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { let secondMessage = completeMessage.getSlice(at: 4, length: completeMessage.readableBytes - 4)! try stateMachine.receive(message: firstMessage, endStream: false) - XCTAssertNil(stateMachine.nextInboundMessage()) + XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) try stateMachine.receive(message: secondMessage, endStream: false) - XCTAssertEqual(stateMachine.nextInboundMessage(), deframedMessage) + XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(deframedMessage)) // Client sends end try stateMachine.receive(message: ByteBuffer(), endStream: true) @@ -1944,7 +1905,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertEqual(response, ["grpc-status": "0"]) XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) - XCTAssertNil(stateMachine.nextInboundMessage()) + XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) } func testClientClosesBeforeServerResponds() throws { @@ -1968,9 +1929,9 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { let secondMessage = completeMessage.getSlice(at: 4, length: completeMessage.readableBytes - 4)! try stateMachine.receive(message: firstMessage, endStream: false) - XCTAssertNil(stateMachine.nextInboundMessage()) + XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) try stateMachine.receive(message: secondMessage, endStream: false) - XCTAssertEqual(stateMachine.nextInboundMessage(), deframedMessage) + XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(deframedMessage)) // Server sends initial metadata let sentInitialHeaders = try stateMachine.send(metadata: Metadata(headers: ["custom": "value"])) @@ -2007,20 +1968,20 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertEqual(response, ["grpc-status": "0"]) XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) - XCTAssertNil(stateMachine.nextInboundMessage()) + XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) } } extension XCTestCase { func assertRejectedRPC( _ action: GRPCStreamStateMachine.OnMetadataReceived, - expression: (Status?, HPACKHeaders?) throws -> Void + expression: (HPACKHeaders) throws -> Void ) rethrows { - guard case .rejectRPC(let status, let trailers) = action else { + guard case .rejectRPC(let trailers) = action else { XCTFail("RPC should have been rejected.") return } - try expression(status, trailers) + try expression(trailers) } func frameMessage(_ message: [UInt8], compress: Bool) throws -> ByteBuffer { diff --git a/Tests/GRPCHTTP2CoreTests/Internal/GRPCStatusMessageMarshallerTests.swift b/Tests/GRPCHTTP2CoreTests/Internal/GRPCStatusMessageMarshallerTests.swift new file mode 100644 index 000000000..ac659fad9 --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/Internal/GRPCStatusMessageMarshallerTests.swift @@ -0,0 +1,44 @@ +/* + * 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 XCTest + +@testable import GRPCHTTP2Core + +class GRPCStatusMessageMarshallerTests: XCTestCase { + func testASCIIMarshallingAndUnmarshalling() { + XCTAssertEqual(GRPCStatusMessageMarshaller.marshall("Hello, World!"), "Hello, World!") + XCTAssertEqual(GRPCStatusMessageMarshaller.unmarshall("Hello, World!"), "Hello, World!") + } + + func testPercentMarshallingAndUnmarshalling() { + XCTAssertEqual(GRPCStatusMessageMarshaller.marshall("%"), "%25") + XCTAssertEqual(GRPCStatusMessageMarshaller.unmarshall("%25"), "%") + + XCTAssertEqual(GRPCStatusMessageMarshaller.marshall("25%"), "25%25") + XCTAssertEqual(GRPCStatusMessageMarshaller.unmarshall("25%25"), "25%") + } + + func testUnicodeMarshalling() { + XCTAssertEqual(GRPCStatusMessageMarshaller.marshall("🚀"), "%F0%9F%9A%80") + XCTAssertEqual(GRPCStatusMessageMarshaller.unmarshall("%F0%9F%9A%80"), "🚀") + + let message = "\t\ntest with whitespace\r\nand Unicode BMP ☺ and non-BMP 😈\t\n" + let marshalled = + "%09%0Atest with whitespace%0D%0Aand Unicode BMP %E2%98%BA and non-BMP %F0%9F%98%88%09%0A" + XCTAssertEqual(GRPCStatusMessageMarshaller.marshall(message), marshalled) + XCTAssertEqual(GRPCStatusMessageMarshaller.unmarshall(marshalled), message) + } +} From 67bfe05e8af9141ed4f3bcbae22b7d682ce56985 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Wed, 6 Mar 2024 17:22:54 +0000 Subject: [PATCH 31/51] Add tearDown method --- .../GRPCStreamStateMachine.swift | 98 ++++++++++++------- 1 file changed, 62 insertions(+), 36 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index bbff10062..1b5adb6c1 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -298,27 +298,22 @@ enum GRPCStreamStateMachineState { self.framer = previousState.framer self.compressor = previousState.compressor self.inboundMessageBuffer = previousState.inboundMessageBuffer - previousState.decompressor?.end() } init(previousState: ClientClosedServerIdleState) { self.framer = previousState.framer self.compressor = previousState.compressor self.inboundMessageBuffer = previousState.inboundMessageBuffer - previousState.decompressor?.end() } init(previousState: ClientOpenServerClosedState) { self.framer = previousState.framer self.compressor = previousState.compressor self.inboundMessageBuffer = previousState.inboundMessageBuffer - previousState.decompressor?.end() } } } -// - MARK: Client - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) struct GRPCStreamStateMachine { private var state: GRPCStreamStateMachineState @@ -378,6 +373,7 @@ struct GRPCStreamStateMachine { case receivedMetadata(Metadata) // Client-specific actions + case receivedStatusAndMetadata(status: Status, metadata: Metadata) case failedRequest(status: Status, metadata: Metadata) case doNothing @@ -445,8 +441,34 @@ struct GRPCStreamStateMachine { return self.serverNextInboundMessage() } } + + mutating func tearDown() { + switch self.state { + case .clientIdleServerIdle(let state): + () + case .clientOpenServerIdle(let state): + state.compressor?.end() + state.decompressor?.end() + case .clientOpenServerOpen(let state): + state.compressor?.end() + state.decompressor?.end() + case .clientOpenServerClosed(let state): + state.compressor?.end() + state.decompressor?.end() + case .clientClosedServerIdle(let state): + state.compressor?.end() + state.decompressor?.end() + case .clientClosedServerOpen(let state): + state.compressor?.end() + state.decompressor?.end() + case .clientClosedServerClosed(let state): + state.compressor?.end() + } + } } +// - MARK: Client + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension GRPCStreamStateMachine { private func makeClientHeaders( @@ -566,10 +588,6 @@ extension GRPCStreamStateMachine { self.state = .clientClosedServerIdle(state) return .sendMessage(request) } else { - // If the client is closed and there is no message to be sent, then we - // are done sending messages, as we cannot call send(message:) anymore. - // There are no more messages to be sent, so we can end the compressor. - state.compressor?.end() self.state = .clientClosedServerIdle(state) return .noMoreMessages } @@ -579,22 +597,11 @@ extension GRPCStreamStateMachine { self.state = .clientClosedServerOpen(state) return .sendMessage(request) } else { - // If the client is closed and there is no message to be sent, then we - // are done sending messages, as we cannot call send(message:) anymore. - // There are no more messages to be sent, so we can end the compressor. - state.compressor?.end() self.state = .clientClosedServerOpen(state) return .noMoreMessages } - case .clientOpenServerClosed(let state): - // No point in sending any more requests if the server is closed: - // we end the compressor. - state.compressor?.end() - return .noMoreMessages - case .clientClosedServerClosed(let state): - // No point in sending any more requests if the server is closed: - // we end the compressor. - state.compressor?.end() + case .clientOpenServerClosed, .clientClosedServerClosed: + // No point in sending any more requests if the server is closed. return .noMoreMessages } } @@ -675,6 +682,27 @@ extension GRPCStreamStateMachine { } return .success(inboundEncoding) } + + private func validateAndReturnStatusAndMetadata(_ metadata: HPACKHeaders) throws -> OnMetadataReceived { + let statusCode = metadata.firstString(forKey: .grpcStatus) + .flatMap { Int($0) } + .flatMap { Status.Code(rawValue: $0) } + guard let statusCode else { + try self.invalidState("Non-initial metadata must be a trailer containing grpc-status") + } + + let statusMessage = metadata.firstString(forKey: .grpcStatusMessage) + .map { GRPCStatusMessageMarshaller.unmarshall($0) } ?? "" + + var convertedMetadata = Metadata(headers: metadata) + convertedMetadata.removeAllValues(forKey: GRPCHTTP2Keys.grpcStatus.rawValue) + convertedMetadata.removeAllValues(forKey: GRPCHTTP2Keys.grpcStatusMessage.rawValue) + + return .receivedStatusAndMetadata( + status: Status(code: statusCode, message: statusMessage), + metadata: convertedMetadata + ) + } private mutating func clientReceive( metadata: HPACKHeaders, @@ -705,14 +733,15 @@ extension GRPCStreamStateMachine { return .receivedMetadata(Metadata(headers: metadata)) case .clientOpenServerOpen(let state): + // This state is valid even if endStream is not set: server can send + // trailing metadata without END_STREAM set, and follow it with an + // empty message frame where it is set. + // However, we must make sure that grpc-status is set, otherwise this + // is an invalid state. if endStream { self.state = .clientOpenServerClosed(.init(previousState: state)) - } else { - // This state is valid: server can send trailing metadata without grpc-status - // or END_STREAM set, and follow it with an empty message frame where they are set. - () } - return .receivedMetadata(Metadata(headers: metadata)) + return try self.validateAndReturnStatusAndMetadata(metadata) case .clientClosedServerIdle(let state): if let failedValidation = self.validateHeaders(metadata) { @@ -738,14 +767,15 @@ extension GRPCStreamStateMachine { return .receivedMetadata(Metadata(headers: metadata)) case .clientClosedServerOpen(let state): + // This state is valid even if endStream is not set: server can send + // trailing metadata without END_STREAM set, and follow it with an + // empty message frame where it is set. + // However, we must make sure that grpc-status is set, otherwise this + // is an invalid state. if endStream { self.state = .clientClosedServerClosed(.init(previousState: state)) - } else { - // This state is valid: server can send trailing metadata without grpc-status - // or END_STREAM set, and follow it with an empty message frame where they are set. - () } - return .receivedMetadata(Metadata(headers: metadata)) + return try self.validateAndReturnStatusAndMetadata(metadata) case .clientClosedServerClosed: // We could end up here if we received a grpc-status header in a previous @@ -1205,8 +1235,6 @@ extension GRPCStreamStateMachine { self.state = .clientOpenServerClosed(state) return .sendMessage(response) } else { - // There are no more messages to be sent, so we can end the compressor. - state.compressor?.end() self.state = .clientOpenServerClosed(state) return .noMoreMessages } @@ -1216,8 +1244,6 @@ extension GRPCStreamStateMachine { self.state = .clientClosedServerClosed(state) return .sendMessage(response) } else { - // There are no more messages to be sent, so we can end the compressor. - state.compressor?.end() self.state = .clientClosedServerClosed(state) return .noMoreMessages } From 2b58fd2aa416be80a6a34b91fdeaf56a1f39dc74 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Wed, 6 Mar 2024 17:47:55 +0000 Subject: [PATCH 32/51] Fix tests and formatting --- Sources/GRPCCore/Internal/Base64.swift | 4 +- .../GRPCStreamStateMachine.swift | 64 ++++--- .../GRPCStreamStateMachineTests.swift | 163 ++++++++++++++---- 3 files changed, 172 insertions(+), 59 deletions(-) diff --git a/Sources/GRPCCore/Internal/Base64.swift b/Sources/GRPCCore/Internal/Base64.swift index 70864d099..ef0a16b88 100644 --- a/Sources/GRPCCore/Internal/Base64.swift +++ b/Sources/GRPCCore/Internal/Base64.swift @@ -94,7 +94,7 @@ enum Base64 { // In Base64, 3 bytes become 4 output characters, and we pad to the // nearest multiple of four. let base64StringLength = ((bytes.count + 2) / 3) * 4 - let alphabet = Base64.encodeBase64 + let alphabet = Base64.base64Encodings return String(customUnsafeUninitializedCapacity: base64StringLength) { backingStorage in var input = bytes.makeIterator() @@ -267,7 +267,7 @@ enum Base64 { private static let encodePaddingCharacter: UInt8 = 61 - private static let encodeBase64: [UInt8] = [ + private static let base64Encodings: [UInt8] = [ UInt8(ascii: "A"), UInt8(ascii: "B"), UInt8(ascii: "C"), UInt8(ascii: "D"), UInt8(ascii: "E"), UInt8(ascii: "F"), UInt8(ascii: "G"), UInt8(ascii: "H"), UInt8(ascii: "I"), UInt8(ascii: "J"), UInt8(ascii: "K"), UInt8(ascii: "L"), diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 1b5adb6c1..22b5b9254 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -441,10 +441,10 @@ struct GRPCStreamStateMachine { return self.serverNextInboundMessage() } } - + mutating func tearDown() { switch self.state { - case .clientIdleServerIdle(let state): + case .clientIdleServerIdle: () case .clientOpenServerIdle(let state): state.compressor?.end() @@ -605,7 +605,7 @@ extension GRPCStreamStateMachine { return .noMoreMessages } } - + private mutating func validateHeaders(_ metadata: HPACKHeaders) -> OnMetadataReceived? { let grpcStatus = metadata.firstString(forKey: .grpcStatus) .flatMap { Int($0) } @@ -643,7 +643,7 @@ extension GRPCStreamStateMachine { metadata: Metadata(headers: metadata) ) } - + let contentTypeHeader = metadata.first(name: GRPCHTTP2Keys.contentType.rawValue) guard contentTypeHeader.flatMap(ContentType.init) != nil else { return .failedRequest( @@ -654,27 +654,29 @@ extension GRPCStreamStateMachine { metadata: Metadata(headers: metadata) ) } - + return nil } - + private enum ProcessInboundEncodingResult { case error(OnMetadataReceived) case success(CompressionAlgorithm) } - + private func processInboundEncoding(_ metadata: HPACKHeaders) -> ProcessInboundEncodingResult { let inboundEncoding: CompressionAlgorithm if let serverEncoding = metadata.first(name: GRPCHTTP2Keys.encoding.rawValue) { guard let parsedEncoding = CompressionAlgorithm(rawValue: serverEncoding) else { - return .error(.failedRequest( - status: .init( - code: .internalError, - message: - "The server picked a compression algorithm ('\(serverEncoding)') the client does not know about." - ), - metadata: Metadata(headers: metadata) - )) + return .error( + .failedRequest( + status: .init( + code: .internalError, + message: + "The server picked a compression algorithm ('\(serverEncoding)') the client does not know about." + ), + metadata: Metadata(headers: metadata) + ) + ) } inboundEncoding = parsedEncoding } else { @@ -682,22 +684,25 @@ extension GRPCStreamStateMachine { } return .success(inboundEncoding) } - - private func validateAndReturnStatusAndMetadata(_ metadata: HPACKHeaders) throws -> OnMetadataReceived { + + private func validateAndReturnStatusAndMetadata( + _ metadata: HPACKHeaders + ) throws -> OnMetadataReceived { let statusCode = metadata.firstString(forKey: .grpcStatus) .flatMap { Int($0) } .flatMap { Status.Code(rawValue: $0) } guard let statusCode else { try self.invalidState("Non-initial metadata must be a trailer containing grpc-status") } - - let statusMessage = metadata.firstString(forKey: .grpcStatusMessage) + + let statusMessage = + metadata.firstString(forKey: .grpcStatusMessage) .map { GRPCStatusMessageMarshaller.unmarshall($0) } ?? "" - + var convertedMetadata = Metadata(headers: metadata) convertedMetadata.removeAllValues(forKey: GRPCHTTP2Keys.grpcStatus.rawValue) convertedMetadata.removeAllValues(forKey: GRPCHTTP2Keys.grpcStatusMessage.rawValue) - + return .receivedStatusAndMetadata( status: Status(code: statusCode, message: statusMessage), metadata: convertedMetadata @@ -1102,7 +1107,11 @@ extension GRPCStreamStateMachine { else { if configuration.acceptedEncodings.isEmpty { let status = Status(code: .unimplemented, message: "Compression is not supported") - let trailers = self.makeTrailers(status: status, customMetadata: nil, trailersOnly: true) + let trailers = self.makeTrailers( + status: status, + customMetadata: nil, + trailersOnly: true + ) return .rejectRPC(trailers: trailers) } else { let status = Status( @@ -1114,9 +1123,16 @@ extension GRPCStreamStateMachine { ) var customTrailers = Metadata() for acceptedEncoding in configuration.acceptedEncodings { - customTrailers.addString(acceptedEncoding.name, forKey: GRPCHTTP2Keys.acceptEncoding.rawValue) + customTrailers.addString( + acceptedEncoding.name, + forKey: GRPCHTTP2Keys.acceptEncoding.rawValue + ) } - let trailers = self.makeTrailers(status: status, customMetadata: customTrailers, trailersOnly: true) + let trailers = self.makeTrailers( + status: status, + customMetadata: customTrailers, + trailersOnly: true + ) return .rejectRPC(trailers: trailers) } } diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index 3ae6bb0ca..261879285 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -58,14 +58,14 @@ extension HPACKHeaders { GRPCHTTP2Keys.acceptEncoding.rawValue: "gzip", GRPCHTTP2Keys.encoding.rawValue: "gzip", ] - static let receivedHeadersWithoutContentType: Self = [ + static let receivedWithoutContentType: Self = [ GRPCHTTP2Keys.path.rawValue: "test/test" ] - static let receivedHeadersWithInvalidContentType: Self = [ + static let receivedWithInvalidContentType: Self = [ GRPCHTTP2Keys.path.rawValue: "test/test", GRPCHTTP2Keys.contentType.rawValue: "invalid/invalid", ] - static let receivedHeadersWithoutEndpoint: Self = [ + static let receivedWithoutEndpoint: Self = [ GRPCHTTP2Keys.contentType.rawValue: "application/grpc" ] @@ -125,7 +125,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Open server XCTAssertNoThrow(try stateMachine.receive(metadata: serverMetadata, endStream: false)) // Close server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .serverTrailers, endStream: true)) case .clientClosedServerIdle: // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) @@ -146,7 +146,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) // Close server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .serverTrailers, endStream: true)) } return stateMachine @@ -253,7 +253,6 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertEqual(error.message, "Client cannot send status and trailer.") } } - } // - MARK: Receive initial metadata @@ -270,10 +269,9 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { } } - func testReceiveInitialMetadataWhenServerIdleOrOpen() throws { + func testReceiveInitialMetadataWhenServerIdle() throws { for targetState in [ - TargetStateMachineState.clientOpenServerIdle, .clientOpenServerOpen, .clientClosedServerIdle, - .clientClosedServerOpen, + TargetStateMachineState.clientOpenServerIdle, .clientClosedServerIdle, ] { var stateMachine = self.makeClientStateMachine(targetState: targetState) @@ -300,6 +298,64 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { } } + func testReceiveInitialMetadataWhenServerOpen() throws { + for targetState in [ + TargetStateMachineState.clientOpenServerOpen, .clientClosedServerOpen, + ] { + var stateMachine = self.makeClientStateMachine(targetState: targetState) + + // Receiving initial metadata again should throw if grpc-status is not present. + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive( + metadata: [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, + GRPCHTTP2Keys.encoding.rawValue: "deflate", + "custom": "123", + "custom-bin": String(base64Encoding: [42, 43, 44]), + ], + endStream: false + ) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual( + error.message, + "Non-initial metadata must be a trailer containing grpc-status" + ) + } + + // Now make sure everything works well if we include grpc-status + let action = try stateMachine.receive( + metadata: [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.grpcStatus.rawValue: String(Status.Code.ok.rawValue), + GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, + GRPCHTTP2Keys.encoding.rawValue: "deflate", + "custom": "123", + "custom-bin": String(base64Encoding: [42, 43, 44]), + ], + endStream: false + ) + + var expectedMetadata: Metadata = [ + ":status": "200", + "content-type": "application/grpc", + "grpc-encoding": "deflate", + "custom": "123", + ] + expectedMetadata.removeAllValues(forKey: GRPCHTTP2Keys.grpcStatus.rawValue) + expectedMetadata.addBinary([42, 43, 44], forKey: "custom-bin") + XCTAssertEqual( + action, + .receivedStatusAndMetadata( + status: Status(code: .ok, message: ""), + metadata: expectedMetadata + ) + ) + } + } + func testReceiveInitialMetadataWhenServerClosed() { for targetState in [TargetStateMachineState.clientOpenServerClosed, .clientClosedServerClosed] { var stateMachine = self.makeClientStateMachine(targetState: targetState) @@ -344,6 +400,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { let action = try stateMachine.receive( metadata: [ GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.grpcStatus.rawValue: String(Status.Code.ok.rawValue), GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, GRPCHTTP2Keys.encoding.rawValue: "deflate", "custom": "123", @@ -357,7 +414,13 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { "grpc-encoding": "deflate", "custom": "123", ] - XCTAssertEqual(action, .receivedMetadata(expectedMetadata)) + XCTAssertEqual( + action, + .receivedStatusAndMetadata( + status: .init(code: .ok, message: ""), + metadata: expectedMetadata + ) + ) } } @@ -574,7 +637,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) // Close server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .serverTrailers, endStream: true)) // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) @@ -634,7 +697,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { try stateMachine.receive(message: receivedBytes, endStream: false) // Close server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .serverTrailers, endStream: true)) XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage([42, 42])) XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) @@ -670,7 +733,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { try stateMachine.receive(message: receivedBytes, endStream: false) // Close server - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) + XCTAssertNoThrow(try stateMachine.receive(metadata: .serverTrailers, endStream: true)) // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) @@ -746,7 +809,16 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { metadata: .serverTrailers, endStream: true ) - XCTAssertEqual(metadataReceivedAction, .receivedMetadata(Metadata(headers: .serverTrailers))) + let receivedMetadata = { + var m = Metadata(headers: .serverTrailers) + m.removeAllValues(forKey: GRPCHTTP2Keys.grpcStatus.rawValue) + m.removeAllValues(forKey: GRPCHTTP2Keys.grpcStatusMessage.rawValue) + return m + }() + XCTAssertEqual( + metadataReceivedAction, + .receivedStatusAndMetadata(status: .init(code: .ok, message: ""), metadata: receivedMetadata) + ) XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) @@ -812,7 +884,16 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { metadata: .serverTrailers, endStream: true ) - XCTAssertEqual(metadataReceivedAction, .receivedMetadata(Metadata(headers: .serverTrailers))) + let receivedMetadata = { + var m = Metadata(headers: .serverTrailers) + m.removeAllValues(forKey: GRPCHTTP2Keys.grpcStatus.rawValue) + m.removeAllValues(forKey: GRPCHTTP2Keys.grpcStatusMessage.rawValue) + return m + }() + XCTAssertEqual( + metadataReceivedAction, + .receivedStatusAndMetadata(status: .init(code: .ok, message: ""), metadata: receivedMetadata) + ) XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) @@ -881,7 +962,16 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { metadata: .serverTrailers, endStream: true ) - XCTAssertEqual(metadataReceivedAction, .receivedMetadata(Metadata(headers: .serverTrailers))) + let receivedMetadata = { + var m = Metadata(headers: .serverTrailers) + m.removeAllValues(forKey: GRPCHTTP2Keys.grpcStatus.rawValue) + m.removeAllValues(forKey: GRPCHTTP2Keys.grpcStatusMessage.rawValue) + return m + }() + XCTAssertEqual( + metadataReceivedAction, + .receivedStatusAndMetadata(status: .init(code: .ok, message: ""), metadata: receivedMetadata) + ) XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) @@ -1267,7 +1357,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) let action = try stateMachine.receive( - metadata: .receivedHeadersWithoutContentType, + metadata: .receivedWithoutContentType, endStream: false ) @@ -1281,7 +1371,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) let action = try stateMachine.receive( - metadata: .receivedHeadersWithInvalidContentType, + metadata: .receivedWithInvalidContentType, endStream: false ) @@ -1295,17 +1385,20 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) let action = try stateMachine.receive( - metadata: .receivedHeadersWithoutEndpoint, + metadata: .receivedWithoutEndpoint, endStream: false ) self.assertRejectedRPC(action) { trailers in - XCTAssertEqual(trailers, [ - ":status": "200", - "content-type": "application/grpc", - "grpc-status": "12", - "grpc-status-message": "No :path header has been set." - ]) + XCTAssertEqual( + trailers, + [ + ":status": "200", + "content-type": "application/grpc", + "grpc-status": "12", + "grpc-status-message": "No :path header has been set.", + ] + ) } } @@ -1320,13 +1413,17 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { ) self.assertRejectedRPC(action) { trailers in - XCTAssertEqual(trailers, [ - ":status": "200", - "content-type": "application/grpc", - "grpc-accept-encoding": "deflate", - "grpc-status": "12", - "grpc-status-message": "gzip compression is not supported; supported algorithms are listed in grpc-accept-encoding" - ]) + XCTAssertEqual( + trailers, + [ + ":status": "200", + "content-type": "application/grpc", + "grpc-accept-encoding": "deflate", + "grpc-status": "12", + "grpc-status-message": + "gzip compression is not supported; supported algorithms are listed in grpc-accept-encoding", + ] + ) } } @@ -1988,7 +2085,7 @@ extension XCTestCase { try frameMessages([message], compress: compress) } - func frameMessages(_ messages: any Sequence<[UInt8]>, compress: Bool) throws -> ByteBuffer { + func frameMessages(_ messages: [[UInt8]], compress: Bool) throws -> ByteBuffer { var framer = GRPCMessageFramer() let compressor: Zlib.Compressor? = { if compress { From 960af7c803311f9aa0ffb1603812d530d722996b Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Fri, 8 Mar 2024 15:15:26 +0000 Subject: [PATCH 33/51] PR changes --- .../GRPCStreamStateMachine.swift | 55 +++++++++++-------- .../GRPCStatusMessageMarshaller.swift | 1 - .../GRPCStreamStateMachineTests.swift | 48 ++++++++++++++-- 3 files changed, 75 insertions(+), 29 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 22b5b9254..71f2dda77 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -17,7 +17,7 @@ import GRPCCore import NIOCore import NIOHPACK -import NIOHTTP1 +import enum NIOHTTP1.HTTPResponseStatus enum Scheme: String { case http @@ -55,8 +55,8 @@ enum GRPCStreamStateMachineState { } enum DecompressionState { - case decompressionNotYetKnown - case decompression(CompressionAlgorithm) + case notYetKnown + case known(CompressionAlgorithm) } struct ClientOpenServerIdleState { @@ -93,9 +93,9 @@ enum GRPCStreamStateMachineState { // In the case of the client, it will need to wait until the server responds // with its initial metadata. switch decompressionState { - case .decompressionNotYetKnown: + case .notYetKnown: self.deframer = nil - case .decompression(let decompressionAlgorithm): + case .known(let decompressionAlgorithm): if let zlibMethod = Zlib.Method(encoding: decompressionAlgorithm) { self.decompressor = Zlib.Decompressor(method: zlibMethod) } @@ -344,7 +344,7 @@ struct GRPCStreamStateMachine { case .client: try self.clientSend(message: message, endStream: endStream) case .server: - guard !endStream else { + if endStream { try self.invalidState( "Can't end response stream by sending a message - send(status:metadata:trailersOnly:) must be called" ) @@ -353,8 +353,11 @@ struct GRPCStreamStateMachine { } } - mutating func send(status: Status, metadata: Metadata, trailersOnly: Bool) throws -> HPACKHeaders - { + mutating func send( + status: Status, + metadata: Metadata, + trailersOnly: Bool + ) throws -> HPACKHeaders { switch self.configuration { case .client: try self.invalidState( @@ -515,7 +518,7 @@ extension GRPCStreamStateMachine { .init( previousState: state, compressionAlgorithm: configuration.outboundEncoding, - decompressionState: .decompressionNotYetKnown + decompressionState: .notYetKnown ) ) return self.makeClientHeaders( @@ -606,12 +609,16 @@ extension GRPCStreamStateMachine { } } - private mutating func validateHeaders(_ metadata: HPACKHeaders) -> OnMetadataReceived? { - let grpcStatus = metadata.firstString(forKey: .grpcStatus) - .flatMap { Int($0) } - .flatMap { Status.Code(rawValue: $0) } - let httpStatus = metadata.firstString(forKey: .status) - guard grpcStatus != nil || httpStatus == "200" else { + private mutating func clientValidateHeadersReceivedFromServer(_ metadata: HPACKHeaders) -> OnMetadataReceived? { + var httpStatus: String? { + metadata.firstString(forKey: .status) + } + var grpcStatus: Status.Code? { + metadata.firstString(forKey: .grpcStatus) + .flatMap { Int($0) } + .flatMap { Status.Code(rawValue: $0) } + } + guard httpStatus == "200" || grpcStatus != nil else { let httpStatusCode = httpStatus .flatMap { Int($0) } @@ -715,13 +722,14 @@ extension GRPCStreamStateMachine { ) throws -> OnMetadataReceived { switch self.state { case .clientOpenServerIdle(let state): - if let failedValidation = self.validateHeaders(metadata) { + if let failedValidation = self.clientValidateHeadersReceivedFromServer(metadata) { return failedValidation } if endStream { // This is a trailers-only response: close server. self.state = .clientOpenServerClosed(.init(previousState: state)) + return try self.validateAndReturnStatusAndMetadata(metadata) } else { switch self.processInboundEncoding(metadata) { case .error(let failure): @@ -733,9 +741,9 @@ extension GRPCStreamStateMachine { decompressionAlgorithm: inboundEncoding ) ) + return .receivedMetadata(Metadata(headers: metadata)) } } - return .receivedMetadata(Metadata(headers: metadata)) case .clientOpenServerOpen(let state): // This state is valid even if endStream is not set: server can send @@ -749,13 +757,14 @@ extension GRPCStreamStateMachine { return try self.validateAndReturnStatusAndMetadata(metadata) case .clientClosedServerIdle(let state): - if let failedValidation = self.validateHeaders(metadata) { + if let failedValidation = self.clientValidateHeadersReceivedFromServer(metadata) { return failedValidation } if endStream { // This is a trailers-only response: close server. self.state = .clientClosedServerClosed(.init(previousState: state)) + return try self.validateAndReturnStatusAndMetadata(metadata) } else { switch self.processInboundEncoding(metadata) { case .error(let failure): @@ -767,9 +776,9 @@ extension GRPCStreamStateMachine { decompressionAlgorithm: inboundEncoding ) ) + return .receivedMetadata(Metadata(headers: metadata)) } } - return .receivedMetadata(Metadata(headers: metadata)) case .clientClosedServerOpen(let state): // This state is valid even if endStream is not set: server can send @@ -966,6 +975,7 @@ extension GRPCStreamStateMachine { private func makeNon200StatusTrailers(_ customMetadata: Metadata?) -> HPACKHeaders { assert(customMetadata != nil, "Something is very wrong") var headers = HPACKHeaders() + headers.reserveCapacity(customMetadata?.count ?? 0) for metadataPair in customMetadata! { headers.add(name: metadataPair.key, value: metadataPair.value.encoded()) } @@ -1022,8 +1032,8 @@ extension GRPCStreamStateMachine { } private mutating func serverSend( - status: Status?, - customMetadata: Metadata?, + status: Status, + customMetadata: Metadata, trailersOnly: Bool ) throws -> HPACKHeaders { // Close the server. @@ -1174,7 +1184,7 @@ extension GRPCStreamStateMachine { .init( previousState: state, compressionAlgorithm: outboundEncoding, - decompressionState: .decompression(inboundEncoding) + decompressionState: .known(inboundEncoding) ) ) } @@ -1349,6 +1359,7 @@ extension Zlib.Method { extension Metadata { init(headers: HPACKHeaders) { var metadata = Metadata() + metadata.reserveCapacity(headers.count) for header in headers { if header.name.hasSuffix("-bin") { do { diff --git a/Sources/GRPCHTTP2Core/Internal/GRPCStatusMessageMarshaller.swift b/Sources/GRPCHTTP2Core/Internal/GRPCStatusMessageMarshaller.swift index 3db35b5d2..4f8b1eb40 100644 --- a/Sources/GRPCHTTP2Core/Internal/GRPCStatusMessageMarshaller.swift +++ b/Sources/GRPCHTTP2Core/Internal/GRPCStatusMessageMarshaller.swift @@ -14,7 +14,6 @@ * limitations under the License. */ -// swiftformat:disable:next enumNamespaces enum GRPCStatusMessageMarshaller { /// Adds percent encoding to the given message. /// diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index 261879285..92f553e96 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -385,11 +385,29 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { } } - func testReceiveEndTrailerWhenClientOpenAndServerIdle() { + func testReceiveEndTrailerWhenClientOpenAndServerIdle() throws { var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerIdle) - // Receive a trailer-only response - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) + // Receive a trailers-only response + let trailersOnlyResponse: HPACKHeaders = [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, + GRPCHTTP2Keys.grpcStatus.rawValue: String(Status.Code.internalError.rawValue), + GRPCHTTP2Keys.grpcStatusMessage.rawValue: GRPCStatusMessageMarshaller.marshall("Some status message")!, + "custom-key": "custom-value" + ] + let trailers = try stateMachine.receive(metadata: trailersOnlyResponse, endStream: true) + switch trailers { + case .receivedStatusAndMetadata(let status, let metadata): + XCTAssertEqual(status, Status(code: .internalError, message: "Some status message")) + XCTAssertEqual(metadata, [ + ":status": "200", + "content-type": "application/grpc", + "custom-key": "custom-value" + ]) + case .receivedMetadata, .failedRequest, .doNothing, .rejectRPC: + XCTFail("Expected .receivedStatusAndMetadata") + } } func testReceiveEndTrailerWhenServerOpen() throws { @@ -437,11 +455,29 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { } } - func testReceiveEndTrailerWhenClientClosedAndServerIdle() { + func testReceiveEndTrailerWhenClientClosedAndServerIdle() throws { var stateMachine = self.makeClientStateMachine(targetState: .clientClosedServerIdle) // Server sends a trailers-only response - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) + let trailersOnlyResponse: HPACKHeaders = [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, + GRPCHTTP2Keys.grpcStatus.rawValue: String(Status.Code.internalError.rawValue), + GRPCHTTP2Keys.grpcStatusMessage.rawValue: GRPCStatusMessageMarshaller.marshall("Some status message")!, + "custom-key": "custom-value" + ] + let trailers = try stateMachine.receive(metadata: trailersOnlyResponse, endStream: true) + switch trailers { + case .receivedStatusAndMetadata(let status, let metadata): + XCTAssertEqual(status, Status(code: .internalError, message: "Some status message")) + XCTAssertEqual(metadata, [ + ":status": "200", + "content-type": "application/grpc", + "custom-key": "custom-value" + ]) + case .receivedMetadata, .failedRequest, .doNothing, .rejectRPC: + XCTFail("Expected .receivedStatusAndMetadata") + } } func testReceiveEndTrailerWhenClientClosedAndServerClosed() { @@ -450,7 +486,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Close server again (endStream = true) and assert we don't throw. // This can happen if the previous close was caused by a grpc-status header // and then the server sends an empty frame with EOS set. - XCTAssertNoThrow(try stateMachine.receive(metadata: .init(), endStream: true)) + XCTAssertEqual(try stateMachine.receive(metadata: .init(), endStream: true), .doNothing) } // - MARK: Receive message From 5697b1c94380b4ac0de839443480c6e0a4dbb552 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Fri, 8 Mar 2024 16:45:09 +0000 Subject: [PATCH 34/51] Disable swift-format rule --- Sources/GRPCCore/Internal/Base64.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/GRPCCore/Internal/Base64.swift b/Sources/GRPCCore/Internal/Base64.swift index ef0a16b88..f2078331f 100644 --- a/Sources/GRPCCore/Internal/Base64.swift +++ b/Sources/GRPCCore/Internal/Base64.swift @@ -70,6 +70,7 @@ SOFTWARE. */ +// swift-format-ignore: DontRepeatTypeInStaticProperties enum Base64 { struct DecodingOptions: OptionSet { internal let rawValue: UInt @@ -94,7 +95,7 @@ enum Base64 { // In Base64, 3 bytes become 4 output characters, and we pad to the // nearest multiple of four. let base64StringLength = ((bytes.count + 2) / 3) * 4 - let alphabet = Base64.base64Encodings + let alphabet = Base64.encodeBase64 return String(customUnsafeUninitializedCapacity: base64StringLength) { backingStorage in var input = bytes.makeIterator() @@ -267,7 +268,7 @@ enum Base64 { private static let encodePaddingCharacter: UInt8 = 61 - private static let base64Encodings: [UInt8] = [ + private static let encodeBase64: [UInt8] = [ UInt8(ascii: "A"), UInt8(ascii: "B"), UInt8(ascii: "C"), UInt8(ascii: "D"), UInt8(ascii: "E"), UInt8(ascii: "F"), UInt8(ascii: "G"), UInt8(ascii: "H"), UInt8(ascii: "I"), UInt8(ascii: "J"), UInt8(ascii: "K"), UInt8(ascii: "L"), From 59793b666cf401f4a3babec9bf104845429a5ce8 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Fri, 8 Mar 2024 16:45:22 +0000 Subject: [PATCH 35/51] Change import --- Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 71f2dda77..1de1ee9d7 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -17,7 +17,7 @@ import GRPCCore import NIOCore import NIOHPACK -import enum NIOHTTP1.HTTPResponseStatus +import NIOHTTP1 enum Scheme: String { case http From 72b07540da2fc905f9a29b540b1cd48ca3928454 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Fri, 8 Mar 2024 16:45:28 +0000 Subject: [PATCH 36/51] Remove some code duplication --- .../GRPCStreamStateMachine.swift | 47 +++++++++---------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 1de1ee9d7..7ba193f98 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -1115,36 +1115,35 @@ extension GRPCStreamStateMachine { guard let clientEncoding = CompressionAlgorithm(rawValue: String(rawEncoding)), isIdentityOrCompatibleEncoding(clientEncoding) else { + let statusMessage: String + let customMetadata: Metadata? if configuration.acceptedEncodings.isEmpty { - let status = Status(code: .unimplemented, message: "Compression is not supported") - let trailers = self.makeTrailers( - status: status, - customMetadata: nil, - trailersOnly: true - ) - return .rejectRPC(trailers: trailers) + statusMessage = "Compression is not supported" + customMetadata = nil } else { - let status = Status( - code: .unimplemented, - message: """ + statusMessage = """ \(rawEncoding) compression is not supported; \ supported algorithms are listed in grpc-accept-encoding """ - ) - var customTrailers = Metadata() - for acceptedEncoding in configuration.acceptedEncodings { - customTrailers.addString( - acceptedEncoding.name, - forKey: GRPCHTTP2Keys.acceptEncoding.rawValue - ) - } - let trailers = self.makeTrailers( - status: status, - customMetadata: customTrailers, - trailersOnly: true - ) - return .rejectRPC(trailers: trailers) + customMetadata = { + var trailers = Metadata() + trailers.reserveCapacity(configuration.acceptedEncodings.count) + for acceptedEncoding in configuration.acceptedEncodings { + trailers.addString( + acceptedEncoding.name, + forKey: GRPCHTTP2Keys.acceptEncoding.rawValue + ) + } + return trailers + }() } + + let trailers = self.makeTrailers( + status: Status(code: .unimplemented, message: statusMessage), + customMetadata: customMetadata, + trailersOnly: true + ) + return .rejectRPC(trailers: trailers) } // Server supports client's encoding. From 2b383bd4804e92634c919d5c4b3092c1cf5d134a Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Fri, 8 Mar 2024 17:11:15 +0000 Subject: [PATCH 37/51] Remove unnecessary special handling of status message in metadata --- Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 7ba193f98..b97298e27 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -1368,12 +1368,7 @@ extension Metadata { metadata.addString(header.value, forKey: header.name) } } else { - if header.name == GRPCHTTP2Keys.grpcStatusMessage.rawValue { - let decodedStatusMessage = GRPCStatusMessageMarshaller.unmarshall(header.value) - metadata.addString(decodedStatusMessage, forKey: header.name) - } else { - metadata.addString(header.value, forKey: header.name) - } + metadata.addString(header.value, forKey: header.name) } } self = metadata From ee1209225d4b50cff7261baae7a908a71d5869a9 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Fri, 8 Mar 2024 17:12:16 +0000 Subject: [PATCH 38/51] Remove redundant enum case --- Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift | 9 ++++----- .../GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index b97298e27..e256e5c77 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -377,7 +377,6 @@ struct GRPCStreamStateMachine { // Client-specific actions case receivedStatusAndMetadata(status: Status, metadata: Metadata) - case failedRequest(status: Status, metadata: Metadata) case doNothing // Server-specific actions @@ -625,7 +624,7 @@ extension GRPCStreamStateMachine { .map { HTTPResponseStatus(statusCode: $0) } guard let httpStatusCode else { - return .failedRequest( + return .receivedStatusAndMetadata( status: .init(code: .unknown, message: "Unexpected non-200 HTTP Status Code."), metadata: Metadata(headers: metadata) ) @@ -642,7 +641,7 @@ extension GRPCStreamStateMachine { if case .clientOpenServerIdle(let state) = self.state { self.state = .clientClosedServerIdle(.init(previousState: state)) } - return .failedRequest( + return .receivedStatusAndMetadata( status: .init( code: Status.Code(httpStatusCode: httpStatusCode), message: "Unexpected non-200 HTTP Status Code." @@ -653,7 +652,7 @@ extension GRPCStreamStateMachine { let contentTypeHeader = metadata.first(name: GRPCHTTP2Keys.contentType.rawValue) guard contentTypeHeader.flatMap(ContentType.init) != nil else { - return .failedRequest( + return .receivedStatusAndMetadata( status: .init( code: .internalError, message: "Missing \(GRPCHTTP2Keys.contentType) header" @@ -675,7 +674,7 @@ extension GRPCStreamStateMachine { if let serverEncoding = metadata.first(name: GRPCHTTP2Keys.encoding.rawValue) { guard let parsedEncoding = CompressionAlgorithm(rawValue: serverEncoding) else { return .error( - .failedRequest( + .receivedStatusAndMetadata( status: .init( code: .internalError, message: diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index 92f553e96..b52bec294 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -405,7 +405,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { "content-type": "application/grpc", "custom-key": "custom-value" ]) - case .receivedMetadata, .failedRequest, .doNothing, .rejectRPC: + case .receivedMetadata, .doNothing, .rejectRPC: XCTFail("Expected .receivedStatusAndMetadata") } } @@ -475,7 +475,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { "content-type": "application/grpc", "custom-key": "custom-value" ]) - case .receivedMetadata, .failedRequest, .doNothing, .rejectRPC: + case .receivedMetadata, .doNothing, .rejectRPC: XCTFail("Expected .receivedStatusAndMetadata") } } From 1e3c9ddb294ff8eee0e3192f0620089b82dc1909 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Fri, 8 Mar 2024 17:13:10 +0000 Subject: [PATCH 39/51] Change status code in error case when grpc-status is missing --- Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift | 11 ++++++----- .../GRPCStreamStateMachineTests.swift | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index e256e5c77..14fe9a0c5 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -694,11 +694,12 @@ extension GRPCStreamStateMachine { private func validateAndReturnStatusAndMetadata( _ metadata: HPACKHeaders ) throws -> OnMetadataReceived { - let statusCode = metadata.firstString(forKey: .grpcStatus) - .flatMap { Int($0) } - .flatMap { Status.Code(rawValue: $0) } - guard let statusCode else { - try self.invalidState("Non-initial metadata must be a trailer containing grpc-status") + let rawStatusCode = metadata.firstString(forKey: .grpcStatus) + guard let rawStatusCode, + let intStatusCode = Int(rawStatusCode), + let statusCode = Status.Code(rawValue: intStatusCode) else { + let message = "Non-initial metadata must be a trailer containing a valid grpc-status" + (rawStatusCode.flatMap { "but was \($0)" } ?? "") + throw RPCError(code: .unknown, message: message) } let statusMessage = diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index b52bec294..d67abe2c4 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -318,10 +318,10 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { endStream: false ) ) { error in - XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.code, .unknown) XCTAssertEqual( error.message, - "Non-initial metadata must be a trailer containing grpc-status" + "Non-initial metadata must be a trailer containing a valid grpc-status" ) } From 1805b529f7e8c99903111ab6b91598485a01a1d3 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Fri, 8 Mar 2024 17:13:20 +0000 Subject: [PATCH 40/51] Avoid allocation --- .../GRPCStreamStateMachine.swift | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 14fe9a0c5..4ab6364e7 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -1154,21 +1154,20 @@ extension GRPCStreamStateMachine { // Secondly, find a compatible encoding the server can use to compress outbound messages, // based on the encodings the client has advertised. - let outboundEncoding: CompressionAlgorithm + var outboundEncoding: CompressionAlgorithm = .identity let clientAdvertisedEncodings = metadata.values( forHeader: GRPCHTTP2Keys.acceptEncoding.rawValue, canonicalForm: true ) - .compactMap { CompressionAlgorithm(rawValue: String($0)) } - if !clientAdvertisedEncodings.isEmpty { - // Find the preferred encoding and use it to compress responses. - // If it's identity, just skip it altogether, since we won't be - // compressing. - outboundEncoding = - clientAdvertisedEncodings - .first { isIdentityOrCompatibleEncoding($0) } ?? .identity - } else { - outboundEncoding = .identity + // Find the preferred encoding and use it to compress responses. + // If it's identity, just skip it altogether, since we won't be + // compressing. + for clientAdvertisedEncoding in clientAdvertisedEncodings { + if let algorithm = CompressionAlgorithm(rawValue: String(clientAdvertisedEncoding)), + isIdentityOrCompatibleEncoding(algorithm) { + outboundEncoding = algorithm + break + } } if endStream { From cfa9ce2a67cf0672b293c024bbafa0c52746568a Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Fri, 8 Mar 2024 17:46:29 +0000 Subject: [PATCH 41/51] Move state change outside of validation method --- Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 4ab6364e7..53f0020af 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -637,10 +637,7 @@ extension GRPCStreamStateMachine { return .doNothing } - // Close the client if open, and forward the mapped status code. - if case .clientOpenServerIdle(let state) = self.state { - self.state = .clientClosedServerIdle(.init(previousState: state)) - } + // Forward the mapped status code. return .receivedStatusAndMetadata( status: .init( code: Status.Code(httpStatusCode: httpStatusCode), @@ -723,6 +720,7 @@ extension GRPCStreamStateMachine { switch self.state { case .clientOpenServerIdle(let state): if let failedValidation = self.clientValidateHeadersReceivedFromServer(metadata) { + self.state = .clientClosedServerIdle(.init(previousState: state)) return failedValidation } From 154e2c596c7fba3c8b37d02353081ec7185492de Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Fri, 8 Mar 2024 17:46:34 +0000 Subject: [PATCH 42/51] Remove unused code --- .../GRPCStreamStateMachine.swift | 21 +------------------ 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 53f0020af..51ccbceed 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -970,30 +970,11 @@ extension GRPCStreamStateMachine { } } - private func makeNon200StatusTrailers(_ customMetadata: Metadata?) -> HPACKHeaders { - assert(customMetadata != nil, "Something is very wrong") - var headers = HPACKHeaders() - headers.reserveCapacity(customMetadata?.count ?? 0) - for metadataPair in customMetadata! { - headers.add(name: metadataPair.key, value: metadataPair.value.encoded()) - } - let httpStatus = headers.firstString(forKey: .status) - assert(httpStatus != nil && httpStatus != "200") - return headers - } - private func makeTrailers( - status: Status?, + status: Status, customMetadata: Metadata?, trailersOnly: Bool ) -> HPACKHeaders { - // If status isn't present, it means we're returning a non-200 HTTP :status - // header. This should already be included in the custom metadata: assert - // this and simply return those headers. - guard let status else { - return makeNon200StatusTrailers(customMetadata) - } - // Trailers always contain the grpc-status header, and optionally, // grpc-status-message, and custom metadata. // If it's a trailers-only response, they will also contain :status and From cec6788681bf5a05d8f44dae686bee0a3c41ad9b Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Fri, 8 Mar 2024 18:52:30 +0000 Subject: [PATCH 43/51] Simplify compression state logic --- .../GRPCStreamStateMachine.swift | 116 ++++++++---------- 1 file changed, 51 insertions(+), 65 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 51ccbceed..390bad82d 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -54,11 +54,6 @@ enum GRPCStreamStateMachineState { let maximumPayloadSize: Int } - enum DecompressionState { - case notYetKnown - case known(CompressionAlgorithm) - } - struct ClientOpenServerIdleState { let maximumPayloadSize: Int var framer: GRPCMessageFramer @@ -76,36 +71,16 @@ enum GRPCStreamStateMachineState { init( previousState: ClientIdleServerIdleState, - compressionAlgorithm: CompressionAlgorithm, - decompressionState: DecompressionState + compressor: Zlib.Compressor?, + framer: GRPCMessageFramer, + decompressor: Zlib.Decompressor?, + deframer: NIOSingleStepByteToMessageProcessor? ) { self.maximumPayloadSize = previousState.maximumPayloadSize - - if let zlibMethod = Zlib.Method(encoding: compressionAlgorithm) { - self.compressor = Zlib.Compressor(method: zlibMethod) - } - self.framer = GRPCMessageFramer() - self.outboundCompression = compressionAlgorithm - - // In the case of the server, we will know what the decompression algorithm - // will be, since we know what the inbound encoding is, as the client has - // sent it when starting the request. - // In the case of the client, it will need to wait until the server responds - // with its initial metadata. - switch decompressionState { - case .notYetKnown: - self.deframer = nil - case .known(let decompressionAlgorithm): - if let zlibMethod = Zlib.Method(encoding: decompressionAlgorithm) { - self.decompressor = Zlib.Decompressor(method: zlibMethod) - } - let decoder = GRPCMessageDeframer( - maximumPayloadSize: previousState.maximumPayloadSize, - decompressor: self.decompressor - ) - self.deframer = NIOSingleStepByteToMessageProcessor(decoder) - } - + self.compressor = compressor + self.framer = framer + self.decompressor = decompressor + self.deframer = deframer self.inboundMessageBuffer = .init() } } @@ -119,38 +94,16 @@ enum GRPCStreamStateMachineState { var inboundMessageBuffer: OneOrManyQueue<[UInt8]> - /// This should be called from the server path, as the deframer will already be configured in this scenario. - init(previousState: ClientOpenServerIdleState) { - self.framer = previousState.framer - self.compressor = previousState.compressor - - // In the case of the server, it will already have a deframer set up, - // because it already knows what encoding the client is using: - // it's okay to force-unwrap. - self.deframer = previousState.deframer! - self.decompressor = previousState.decompressor - - self.inboundMessageBuffer = previousState.inboundMessageBuffer - } - - /// This should only be called from the client path, as the deframer has not yet been set up. init( previousState: ClientOpenServerIdleState, - decompressionAlgorithm: CompressionAlgorithm + deframer: NIOSingleStepByteToMessageProcessor, + decompressor: Zlib.Decompressor? ) { self.framer = previousState.framer self.compressor = previousState.compressor - // In the case of the client, it will only be able to set up the deframer - // after it receives the chosen encoding from the server. - if let zlibMethod = Zlib.Method(encoding: decompressionAlgorithm) { - self.decompressor = Zlib.Decompressor(method: zlibMethod) - } - let decoder = GRPCMessageDeframer( - maximumPayloadSize: previousState.maximumPayloadSize, - decompressor: self.decompressor - ) - self.deframer = NIOSingleStepByteToMessageProcessor(decoder) + self.deframer = deframer + self.decompressor = decompressor self.inboundMessageBuffer = previousState.inboundMessageBuffer } @@ -513,11 +466,16 @@ extension GRPCStreamStateMachine { // Client sends metadata only when opening the stream. switch self.state { case .clientIdleServerIdle(let state): + let outboundEncoding = configuration.outboundEncoding + let compressor = Zlib.Method(encoding: outboundEncoding) + .flatMap { Zlib.Compressor(method: $0) } self.state = .clientOpenServerIdle( .init( previousState: state, - compressionAlgorithm: configuration.outboundEncoding, - decompressionState: .notYetKnown + compressor: compressor, + framer: GRPCMessageFramer(), + decompressor: nil, + deframer: nil ) ) return self.makeClientHeaders( @@ -733,10 +691,18 @@ extension GRPCStreamStateMachine { case .error(let failure): return failure case .success(let inboundEncoding): + let decompressor = Zlib.Method(encoding: inboundEncoding) + .flatMap { Zlib.Decompressor(method: $0) } + let deframer = GRPCMessageDeframer( + maximumPayloadSize: state.maximumPayloadSize, + decompressor: decompressor + ) + self.state = .clientOpenServerOpen( .init( previousState: state, - decompressionAlgorithm: inboundEncoding + deframer: NIOSingleStepByteToMessageProcessor(deframer), + decompressor: decompressor ) ) return .receivedMetadata(Metadata(headers: metadata)) @@ -923,7 +889,16 @@ extension GRPCStreamStateMachine { // Server sends initial metadata switch self.state { case .clientOpenServerIdle(let state): - self.state = .clientOpenServerOpen(.init(previousState: state)) + self.state = .clientOpenServerOpen( + .init( + previousState: state, + // In the case of the server, it will already have a deframer set up, + // because it already knows what encoding the client is using: + // it's okay to force-unwrap. + deframer: state.deframer!, + decompressor: state.decompressor + ) + ) return self.makeResponseHeaders( outboundEncoding: state.outboundCompression, configuration: configuration, @@ -1157,11 +1132,22 @@ extension GRPCStreamStateMachine { ) ) } else { + let compressor = Zlib.Method(encoding: outboundEncoding) + .flatMap { Zlib.Compressor(method: $0) } + let decompressor = Zlib.Method(encoding: inboundEncoding) + .flatMap { Zlib.Decompressor(method: $0) } + let deframer = GRPCMessageDeframer( + maximumPayloadSize: state.maximumPayloadSize, + decompressor: decompressor + ) + self.state = .clientOpenServerIdle( .init( previousState: state, - compressionAlgorithm: outboundEncoding, - decompressionState: .known(inboundEncoding) + compressor: compressor, + framer: GRPCMessageFramer(), + decompressor: decompressor, + deframer: NIOSingleStepByteToMessageProcessor(deframer) ) ) } From ed17c557c37998d7fba5ad23aafa5bfd32bb0caa Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Fri, 8 Mar 2024 18:52:48 +0000 Subject: [PATCH 44/51] Formatting --- .../GRPCStreamStateMachine.swift | 26 ++++++++----- .../GRPCStreamStateMachineTests.swift | 38 ++++++++++++------- 2 files changed, 40 insertions(+), 24 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 390bad82d..323f7369a 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -566,7 +566,9 @@ extension GRPCStreamStateMachine { } } - private mutating func clientValidateHeadersReceivedFromServer(_ metadata: HPACKHeaders) -> OnMetadataReceived? { + private mutating func clientValidateHeadersReceivedFromServer( + _ metadata: HPACKHeaders + ) -> OnMetadataReceived? { var httpStatus: String? { metadata.firstString(forKey: .status) } @@ -651,9 +653,12 @@ extension GRPCStreamStateMachine { ) throws -> OnMetadataReceived { let rawStatusCode = metadata.firstString(forKey: .grpcStatus) guard let rawStatusCode, - let intStatusCode = Int(rawStatusCode), - let statusCode = Status.Code(rawValue: intStatusCode) else { - let message = "Non-initial metadata must be a trailer containing a valid grpc-status" + (rawStatusCode.flatMap { "but was \($0)" } ?? "") + let intStatusCode = Int(rawStatusCode), + let statusCode = Status.Code(rawValue: intStatusCode) + else { + let message = + "Non-initial metadata must be a trailer containing a valid grpc-status" + + (rawStatusCode.flatMap { "but was \($0)" } ?? "") throw RPCError(code: .unknown, message: message) } @@ -697,7 +702,7 @@ extension GRPCStreamStateMachine { maximumPayloadSize: state.maximumPayloadSize, decompressor: decompressor ) - + self.state = .clientOpenServerOpen( .init( previousState: state, @@ -1076,9 +1081,9 @@ extension GRPCStreamStateMachine { customMetadata = nil } else { statusMessage = """ - \(rawEncoding) compression is not supported; \ - supported algorithms are listed in grpc-accept-encoding - """ + \(rawEncoding) compression is not supported; \ + supported algorithms are listed in grpc-accept-encoding + """ customMetadata = { var trailers = Metadata() trailers.reserveCapacity(configuration.acceptedEncodings.count) @@ -1118,7 +1123,8 @@ extension GRPCStreamStateMachine { // compressing. for clientAdvertisedEncoding in clientAdvertisedEncodings { if let algorithm = CompressionAlgorithm(rawValue: String(clientAdvertisedEncoding)), - isIdentityOrCompatibleEncoding(algorithm) { + isIdentityOrCompatibleEncoding(algorithm) + { outboundEncoding = algorithm break } @@ -1140,7 +1146,7 @@ extension GRPCStreamStateMachine { maximumPayloadSize: state.maximumPayloadSize, decompressor: decompressor ) - + self.state = .clientOpenServerIdle( .init( previousState: state, diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index d67abe2c4..f11897c1a 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -393,18 +393,23 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { GRPCHTTP2Keys.status.rawValue: "200", GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, GRPCHTTP2Keys.grpcStatus.rawValue: String(Status.Code.internalError.rawValue), - GRPCHTTP2Keys.grpcStatusMessage.rawValue: GRPCStatusMessageMarshaller.marshall("Some status message")!, - "custom-key": "custom-value" + GRPCHTTP2Keys.grpcStatusMessage.rawValue: GRPCStatusMessageMarshaller.marshall( + "Some status message" + )!, + "custom-key": "custom-value", ] let trailers = try stateMachine.receive(metadata: trailersOnlyResponse, endStream: true) switch trailers { case .receivedStatusAndMetadata(let status, let metadata): XCTAssertEqual(status, Status(code: .internalError, message: "Some status message")) - XCTAssertEqual(metadata, [ - ":status": "200", - "content-type": "application/grpc", - "custom-key": "custom-value" - ]) + XCTAssertEqual( + metadata, + [ + ":status": "200", + "content-type": "application/grpc", + "custom-key": "custom-value", + ] + ) case .receivedMetadata, .doNothing, .rejectRPC: XCTFail("Expected .receivedStatusAndMetadata") } @@ -463,18 +468,23 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { GRPCHTTP2Keys.status.rawValue: "200", GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, GRPCHTTP2Keys.grpcStatus.rawValue: String(Status.Code.internalError.rawValue), - GRPCHTTP2Keys.grpcStatusMessage.rawValue: GRPCStatusMessageMarshaller.marshall("Some status message")!, - "custom-key": "custom-value" + GRPCHTTP2Keys.grpcStatusMessage.rawValue: GRPCStatusMessageMarshaller.marshall( + "Some status message" + )!, + "custom-key": "custom-value", ] let trailers = try stateMachine.receive(metadata: trailersOnlyResponse, endStream: true) switch trailers { case .receivedStatusAndMetadata(let status, let metadata): XCTAssertEqual(status, Status(code: .internalError, message: "Some status message")) - XCTAssertEqual(metadata, [ - ":status": "200", - "content-type": "application/grpc", - "custom-key": "custom-value" - ]) + XCTAssertEqual( + metadata, + [ + ":status": "200", + "content-type": "application/grpc", + "custom-key": "custom-value", + ] + ) case .receivedMetadata, .doNothing, .rejectRPC: XCTFail("Expected .receivedStatusAndMetadata") } From 627ce2a356710d2966ba1053735af22fd9716018 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Mon, 11 Mar 2024 14:37:52 +0000 Subject: [PATCH 45/51] Change some types to be fileprivate --- Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift | 2 +- Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 323f7369a..84fbf485a 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -41,7 +41,7 @@ enum GRPCStreamStateMachineConfiguration { } } -enum GRPCStreamStateMachineState { +fileprivate enum GRPCStreamStateMachineState { case clientIdleServerIdle(ClientIdleServerIdleState) case clientOpenServerIdle(ClientOpenServerIdleState) case clientOpenServerOpen(ClientOpenServerOpenState) diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index f11897c1a..a5e2502e5 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -21,7 +21,7 @@ import XCTest @testable import GRPCHTTP2Core -enum TargetStateMachineState: CaseIterable { +fileprivate enum TargetStateMachineState: CaseIterable { case clientIdleServerIdle case clientOpenServerIdle case clientOpenServerOpen @@ -31,7 +31,7 @@ enum TargetStateMachineState: CaseIterable { case clientClosedServerClosed } -extension HPACKHeaders { +fileprivate extension HPACKHeaders { // Client static let clientInitialMetadata: Self = [ GRPCHTTP2Keys.path.rawValue: "test/test", From 54c8e1c580b134e39d2c981df1bf42d5b04c9778 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Mon, 11 Mar 2024 14:42:59 +0000 Subject: [PATCH 46/51] Remove some duplication --- Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 84fbf485a..53c6beef2 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -544,20 +544,18 @@ extension GRPCStreamStateMachine { return request.map { .sendMessage($0) } ?? .awaitMoreMessages case .clientClosedServerIdle(var state): let request = try state.framer.next(compressor: state.compressor) + self.state = .clientClosedServerIdle(state) if let request { - self.state = .clientClosedServerIdle(state) return .sendMessage(request) } else { - self.state = .clientClosedServerIdle(state) return .noMoreMessages } case .clientClosedServerOpen(var state): let request = try state.framer.next(compressor: state.compressor) + self.state = .clientClosedServerOpen(state) if let request { - self.state = .clientClosedServerOpen(state) return .sendMessage(request) } else { - self.state = .clientClosedServerOpen(state) return .noMoreMessages } case .clientOpenServerClosed, .clientClosedServerClosed: @@ -1226,20 +1224,18 @@ extension GRPCStreamStateMachine { return response.map { .sendMessage($0) } ?? .awaitMoreMessages case .clientOpenServerClosed(var state): let response = try state.framer.next(compressor: state.compressor) + self.state = .clientOpenServerClosed(state) if let response { - self.state = .clientOpenServerClosed(state) return .sendMessage(response) } else { - self.state = .clientOpenServerClosed(state) return .noMoreMessages } case .clientClosedServerClosed(var state): let response = try state.framer.next(compressor: state.compressor) + self.state = .clientClosedServerClosed(state) if let response { - self.state = .clientClosedServerClosed(state) return .sendMessage(response) } else { - self.state = .clientClosedServerClosed(state) return .noMoreMessages } } From 83661a1fcddcb62f2f069327ebffebed2a251add Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Tue, 12 Mar 2024 11:08:55 +0000 Subject: [PATCH 47/51] Remove trailersOnly parameter from send(status:metadata:) --- .../GRPCStreamStateMachine.swift | 31 +++-- .../GRPCStreamStateMachineTests.swift | 131 ++++++++++-------- 2 files changed, 95 insertions(+), 67 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 53c6beef2..3b277a7dc 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -308,8 +308,7 @@ struct GRPCStreamStateMachine { mutating func send( status: Status, - metadata: Metadata, - trailersOnly: Bool + metadata: Metadata ) throws -> HPACKHeaders { switch self.configuration { case .client: @@ -319,8 +318,7 @@ struct GRPCStreamStateMachine { case .server: return try self.serverSend( status: status, - customMetadata: metadata, - trailersOnly: trailersOnly + customMetadata: metadata ) } } @@ -990,8 +988,7 @@ extension GRPCStreamStateMachine { private mutating func serverSend( status: Status, - customMetadata: Metadata, - trailersOnly: Bool + customMetadata: Metadata ) throws -> HPACKHeaders { // Close the server. switch self.state { @@ -1000,18 +997,32 @@ extension GRPCStreamStateMachine { return self.makeTrailers( status: status, customMetadata: customMetadata, - trailersOnly: trailersOnly + trailersOnly: false ) case .clientClosedServerOpen(let state): self.state = .clientClosedServerClosed(.init(previousState: state)) return self.makeTrailers( status: status, customMetadata: customMetadata, - trailersOnly: trailersOnly + trailersOnly: false ) - case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: + case .clientOpenServerIdle(let state): + self.state = .clientOpenServerClosed(.init(previousState: state)) + return self.makeTrailers( + status: status, + customMetadata: customMetadata, + trailersOnly: true + ) + case .clientClosedServerIdle(let state): + self.state = .clientClosedServerClosed(.init(previousState: state)) + return self.makeTrailers( + status: status, + customMetadata: customMetadata, + trailersOnly: true + ) + case .clientIdleServerIdle: try self.invalidState( - "Server can't send status if idle." + "Server can't send status if client is idle." ) case .clientOpenServerClosed, .clientClosedServerClosed: try self.invalidState( diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index a5e2502e5..1a5ea30cb 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -245,8 +245,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { ofType: RPCError.self, try stateMachine.send( status: Status(code: .ok, message: ""), - metadata: .init(), - trailersOnly: false + metadata: .init() ) ) { error in XCTAssertEqual(error.code, .internalError) @@ -1063,8 +1062,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertNoThrow( try stateMachine.send( status: .init(code: .ok, message: ""), - metadata: [], - trailersOnly: false + metadata: [] ) ) case .clientClosedServerIdle: @@ -1090,8 +1088,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertNoThrow( try stateMachine.send( status: .init(code: .ok, message: ""), - metadata: [], - trailersOnly: false + metadata: [] ) ) } @@ -1263,48 +1260,58 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // - MARK: Send Status and Trailers - func testSendStatusAndTrailersWhenClientIdleAndServerIdle() { + func testSendStatusAndTrailersWhenClientIdle() { var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) XCTAssertThrowsError( ofType: RPCError.self, try stateMachine.send( status: .init(code: .ok, message: ""), - metadata: .init(), - trailersOnly: false + metadata: .init() ) ) { error in XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Server can't send status if idle.") + XCTAssertEqual(error.message, "Server can't send status if client is idle.") } } - func testSendStatusAndTrailersWhenClientOpenAndServerIdle() { + func testSendStatusAndTrailersWhenClientOpenAndServerIdle() throws { var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerIdle) + let trailers = try stateMachine.send( + status: .init(code: .unknown, message: "RPC unknown"), + metadata: .init() + ) + + // Make sure it's a trailers-only response: it must have :status header and content-type + XCTAssertEqual(trailers, [ + ":status": "200", + "content-type": "application/grpc", + "grpc-status": "2", + "grpc-status-message": "RPC unknown" + ]) + + // Try sending another message: it should fail because server is now closed. XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.send( - status: .init(code: .ok, message: ""), - metadata: .init(), - trailersOnly: false - ) + try stateMachine.send(message: [], endStream: false) ) { error in XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Server can't send status if idle.") + XCTAssertEqual(error.message, "Server can't send a message if it's closed.") } } - func testSendStatusAndTrailersWhenClientOpenAndServerOpen() { + func testSendStatusAndTrailersWhenClientOpenAndServerOpen() throws { var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) - XCTAssertNoThrow( - try stateMachine.send( - status: .init(code: .ok, message: ""), - metadata: .init(), - trailersOnly: false - ) + let trailers = try stateMachine.send( + status: .init(code: .ok, message: ""), + metadata: .init() ) + + // Make sure it's NOT a trailers-only response, because the server was + // already open (so it sent initial metadata): it shouldn't have :status or content-type headers + XCTAssertEqual(trailers, ["grpc-status": "0"]) // Try sending another message: it should fail because server is now closed. XCTAssertThrowsError( @@ -1323,8 +1330,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { ofType: RPCError.self, try stateMachine.send( status: .init(code: .ok, message: ""), - metadata: .init(), - trailersOnly: false + metadata: .init() ) ) { error in XCTAssertEqual(error.code, .internalError) @@ -1332,33 +1338,52 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } } - func testSendStatusAndTrailersWhenClientClosedAndServerIdle() { + func testSendStatusAndTrailersWhenClientClosedAndServerIdle() throws { var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerIdle) + let trailers = try stateMachine.send( + status: .init(code: .unknown, message: "RPC unknown"), + metadata: .init() + ) + + // Make sure it's a trailers-only response: it must have :status header and content-type + XCTAssertEqual(trailers, [ + ":status": "200", + "content-type": "application/grpc", + "grpc-status": "2", + "grpc-status-message": "RPC unknown" + ]) + + // Try sending another message: it should fail because server is now closed. XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.send( - status: .init(code: .ok, message: ""), - metadata: .init(), - trailersOnly: false - ) + try stateMachine.send(message: [], endStream: false) ) { error in XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Server can't send status if idle.") + XCTAssertEqual(error.message, "Server can't send a message if it's closed.") } } - func testSendStatusAndTrailersWhenClientClosedAndServerOpen() { + func testSendStatusAndTrailersWhenClientClosedAndServerOpen() throws { var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerOpen) - // Client is closed but may still be awaiting response, so we should be able to send it. - XCTAssertNoThrow( - try stateMachine.send( - status: .init(code: .ok, message: ""), - metadata: .init(), - trailersOnly: false - ) + let trailers = try stateMachine.send( + status: .init(code: .ok, message: ""), + metadata: .init() ) + + // Make sure it's NOT a trailers-only response, because the server was + // already open (so it sent initial metadata): it shouldn't have :status or content-type headers + XCTAssertEqual(trailers, ["grpc-status": "0"]) + + // Try sending another message: it should fail because server is now closed. + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send(message: [], endStream: false) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Server can't send a message if it's closed.") + } } func testSendStatusAndTrailersWhenClientClosedAndServerClosed() { @@ -1368,8 +1393,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { ofType: RPCError.self, try stateMachine.send( status: .init(code: .ok, message: ""), - metadata: .init(), - trailersOnly: false + metadata: .init() ) ) { error in XCTAssertEqual(error.code, .internalError) @@ -1721,8 +1745,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertNoThrow( try stateMachine.send( status: .init(code: .ok, message: ""), - metadata: [], - trailersOnly: false + metadata: [] ) ) @@ -1788,8 +1811,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertNoThrow( try stateMachine.send( status: .init(code: .ok, message: ""), - metadata: [], - trailersOnly: false + metadata: [] ) ) @@ -1862,8 +1884,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertNoThrow( try stateMachine.send( status: .init(code: .ok, message: ""), - metadata: [], - trailersOnly: false + metadata: [] ) ) @@ -1909,8 +1930,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertNoThrow( try stateMachine.send( status: .init(code: .ok, message: ""), - metadata: [], - trailersOnly: false + metadata: [] ) ) @@ -1979,8 +1999,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Server ends let response = try stateMachine.send( status: .init(code: .ok, message: ""), - metadata: [], - trailersOnly: false + metadata: [] ) XCTAssertEqual(response, ["grpc-status": "0"]) @@ -2042,8 +2061,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Server ends let response = try stateMachine.send( status: .init(code: .ok, message: ""), - metadata: [], - trailersOnly: false + metadata: [] ) XCTAssertEqual(response, ["grpc-status": "0"]) @@ -2105,8 +2123,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Server ends let response = try stateMachine.send( status: .init(code: .ok, message: ""), - metadata: [], - trailersOnly: false + metadata: [] ) XCTAssertEqual(response, ["grpc-status": "0"]) From 9aa7705a9acd8ab8e6cf25516f0507a3ee54839a Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Tue, 12 Mar 2024 11:10:11 +0000 Subject: [PATCH 48/51] Formatting --- .../GRPCStreamStateMachine.swift | 2 +- .../GRPCStreamStateMachineTests.swift | 62 ++++++++++--------- 2 files changed, 35 insertions(+), 29 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 3b277a7dc..17007ef51 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -41,7 +41,7 @@ enum GRPCStreamStateMachineConfiguration { } } -fileprivate enum GRPCStreamStateMachineState { +private enum GRPCStreamStateMachineState { case clientIdleServerIdle(ClientIdleServerIdleState) case clientOpenServerIdle(ClientOpenServerIdleState) case clientOpenServerOpen(ClientOpenServerOpenState) diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index 1a5ea30cb..705caa3e2 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -21,7 +21,7 @@ import XCTest @testable import GRPCHTTP2Core -fileprivate enum TargetStateMachineState: CaseIterable { +private enum TargetStateMachineState: CaseIterable { case clientIdleServerIdle case clientOpenServerIdle case clientOpenServerOpen @@ -31,16 +31,16 @@ fileprivate enum TargetStateMachineState: CaseIterable { case clientClosedServerClosed } -fileprivate extension HPACKHeaders { +extension HPACKHeaders { // Client - static let clientInitialMetadata: Self = [ + fileprivate static let clientInitialMetadata: Self = [ GRPCHTTP2Keys.path.rawValue: "test/test", GRPCHTTP2Keys.scheme.rawValue: "http", GRPCHTTP2Keys.method.rawValue: "POST", GRPCHTTP2Keys.contentType.rawValue: "application/grpc", GRPCHTTP2Keys.te.rawValue: "trailers", ] - static let clientInitialMetadataWithDeflateCompression: Self = [ + fileprivate static let clientInitialMetadataWithDeflateCompression: Self = [ GRPCHTTP2Keys.path.rawValue: "test/test", GRPCHTTP2Keys.contentType.rawValue: "application/grpc", GRPCHTTP2Keys.method.rawValue: "POST", @@ -49,7 +49,7 @@ fileprivate extension HPACKHeaders { GRPCHTTP2Keys.acceptEncoding.rawValue: "deflate", GRPCHTTP2Keys.encoding.rawValue: "deflate", ] - static let clientInitialMetadataWithGzipCompression: Self = [ + fileprivate static let clientInitialMetadataWithGzipCompression: Self = [ GRPCHTTP2Keys.path.rawValue: "test/test", GRPCHTTP2Keys.contentType.rawValue: "application/grpc", GRPCHTTP2Keys.method.rawValue: "POST", @@ -58,30 +58,30 @@ fileprivate extension HPACKHeaders { GRPCHTTP2Keys.acceptEncoding.rawValue: "gzip", GRPCHTTP2Keys.encoding.rawValue: "gzip", ] - static let receivedWithoutContentType: Self = [ + fileprivate static let receivedWithoutContentType: Self = [ GRPCHTTP2Keys.path.rawValue: "test/test" ] - static let receivedWithInvalidContentType: Self = [ + fileprivate static let receivedWithInvalidContentType: Self = [ GRPCHTTP2Keys.path.rawValue: "test/test", GRPCHTTP2Keys.contentType.rawValue: "invalid/invalid", ] - static let receivedWithoutEndpoint: Self = [ + fileprivate static let receivedWithoutEndpoint: Self = [ GRPCHTTP2Keys.contentType.rawValue: "application/grpc" ] // Server - static let serverInitialMetadata: Self = [ + fileprivate static let serverInitialMetadata: Self = [ GRPCHTTP2Keys.status.rawValue: "200", GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, GRPCHTTP2Keys.acceptEncoding.rawValue: "deflate", ] - static let serverInitialMetadataWithDeflateCompression: Self = [ + fileprivate static let serverInitialMetadataWithDeflateCompression: Self = [ GRPCHTTP2Keys.status.rawValue: "200", GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, GRPCHTTP2Keys.encoding.rawValue: "deflate", GRPCHTTP2Keys.acceptEncoding.rawValue: "deflate", ] - static let serverTrailers: Self = [ + fileprivate static let serverTrailers: Self = [ GRPCHTTP2Keys.status.rawValue: "200", GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, GRPCHTTP2Keys.grpcStatus.rawValue: "0", @@ -1282,14 +1282,17 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { status: .init(code: .unknown, message: "RPC unknown"), metadata: .init() ) - + // Make sure it's a trailers-only response: it must have :status header and content-type - XCTAssertEqual(trailers, [ - ":status": "200", - "content-type": "application/grpc", - "grpc-status": "2", - "grpc-status-message": "RPC unknown" - ]) + XCTAssertEqual( + trailers, + [ + ":status": "200", + "content-type": "application/grpc", + "grpc-status": "2", + "grpc-status-message": "RPC unknown", + ] + ) // Try sending another message: it should fail because server is now closed. XCTAssertThrowsError( @@ -1308,7 +1311,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { status: .init(code: .ok, message: ""), metadata: .init() ) - + // Make sure it's NOT a trailers-only response, because the server was // already open (so it sent initial metadata): it shouldn't have :status or content-type headers XCTAssertEqual(trailers, ["grpc-status": "0"]) @@ -1345,15 +1348,18 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { status: .init(code: .unknown, message: "RPC unknown"), metadata: .init() ) - + // Make sure it's a trailers-only response: it must have :status header and content-type - XCTAssertEqual(trailers, [ - ":status": "200", - "content-type": "application/grpc", - "grpc-status": "2", - "grpc-status-message": "RPC unknown" - ]) - + XCTAssertEqual( + trailers, + [ + ":status": "200", + "content-type": "application/grpc", + "grpc-status": "2", + "grpc-status-message": "RPC unknown", + ] + ) + // Try sending another message: it should fail because server is now closed. XCTAssertThrowsError( ofType: RPCError.self, @@ -1371,7 +1377,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { status: .init(code: .ok, message: ""), metadata: .init() ) - + // Make sure it's NOT a trailers-only response, because the server was // already open (so it sent initial metadata): it shouldn't have :status or content-type headers XCTAssertEqual(trailers, ["grpc-status": "0"]) From 61135de82e45b225d383ace7f8e93ffa710866c9 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Thu, 14 Mar 2024 14:55:53 +0000 Subject: [PATCH 49/51] Better handle invalid headers on client side --- .../GRPCStreamStateMachine.swift | 86 ++++++++++++------- .../GRPCStreamStateMachineTests.swift | 22 +++++ 2 files changed, 78 insertions(+), 30 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 17007ef51..2a346e476 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -259,6 +259,12 @@ private enum GRPCStreamStateMachineState { self.inboundMessageBuffer = previousState.inboundMessageBuffer } + init(previousState: ClientOpenServerIdleState) { + self.framer = previousState.framer + self.compressor = previousState.compressor + self.inboundMessageBuffer = previousState.inboundMessageBuffer + } + init(previousState: ClientOpenServerClosedState) { self.framer = previousState.framer self.compressor = previousState.compressor @@ -562,9 +568,14 @@ extension GRPCStreamStateMachine { } } + private enum ServerHeadersValidationResult { + case valid + case invalid(OnMetadataReceived) + } + private mutating func clientValidateHeadersReceivedFromServer( _ metadata: HPACKHeaders - ) -> OnMetadataReceived? { + ) -> ServerHeadersValidationResult { var httpStatus: String? { metadata.firstString(forKey: .status) } @@ -580,9 +591,11 @@ extension GRPCStreamStateMachine { .map { HTTPResponseStatus(statusCode: $0) } guard let httpStatusCode else { - return .receivedStatusAndMetadata( - status: .init(code: .unknown, message: "Unexpected non-200 HTTP Status Code."), - metadata: Metadata(headers: metadata) + return .invalid( + .receivedStatusAndMetadata( + status: .init(code: .unknown, message: "HTTP Status Code is missing."), + metadata: Metadata(headers: metadata) + ) ) } @@ -590,31 +603,35 @@ extension GRPCStreamStateMachine { // For 1xx status codes, the entire header should be skipped and a // subsequent header should be read. // See https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md - return .doNothing + return .invalid(.doNothing) } // Forward the mapped status code. - return .receivedStatusAndMetadata( - status: .init( - code: Status.Code(httpStatusCode: httpStatusCode), - message: "Unexpected non-200 HTTP Status Code." - ), - metadata: Metadata(headers: metadata) + return .invalid( + .receivedStatusAndMetadata( + status: .init( + code: Status.Code(httpStatusCode: httpStatusCode), + message: "Unexpected non-200 HTTP Status Code." + ), + metadata: Metadata(headers: metadata) + ) ) } let contentTypeHeader = metadata.first(name: GRPCHTTP2Keys.contentType.rawValue) guard contentTypeHeader.flatMap(ContentType.init) != nil else { - return .receivedStatusAndMetadata( - status: .init( - code: .internalError, - message: "Missing \(GRPCHTTP2Keys.contentType) header" - ), - metadata: Metadata(headers: metadata) + return .invalid( + .receivedStatusAndMetadata( + status: .init( + code: .internalError, + message: "Missing \(GRPCHTTP2Keys.contentType) header" + ), + metadata: Metadata(headers: metadata) + ) ) } - return nil + return .valid } private enum ProcessInboundEncodingResult { @@ -678,16 +695,20 @@ extension GRPCStreamStateMachine { ) throws -> OnMetadataReceived { switch self.state { case .clientOpenServerIdle(let state): - if let failedValidation = self.clientValidateHeadersReceivedFromServer(metadata) { + switch (self.clientValidateHeadersReceivedFromServer(metadata), endStream) { + case (.invalid(let action), true): + // The headers are invalid, but the server signalled that it was + // closing the stream, so close both client and server. + self.state = .clientClosedServerClosed(.init(previousState: state)) + return action + case (.invalid(let action), false): self.state = .clientClosedServerIdle(.init(previousState: state)) - return failedValidation - } - - if endStream { + return action + case (.valid, true): // This is a trailers-only response: close server. self.state = .clientOpenServerClosed(.init(previousState: state)) return try self.validateAndReturnStatusAndMetadata(metadata) - } else { + case (.valid, false): switch self.processInboundEncoding(metadata) { case .error(let failure): return failure @@ -722,15 +743,20 @@ extension GRPCStreamStateMachine { return try self.validateAndReturnStatusAndMetadata(metadata) case .clientClosedServerIdle(let state): - if let failedValidation = self.clientValidateHeadersReceivedFromServer(metadata) { - return failedValidation - } - - if endStream { + switch (self.clientValidateHeadersReceivedFromServer(metadata), endStream) { + case (.invalid(let action), true): + // The headers are invalid, but the server signalled that it was + // closing the stream, so close the server side too. + self.state = .clientClosedServerClosed(.init(previousState: state)) + return action + case (.invalid(let action), false): + // Client is already closed, so we don't need to update our state. + return action + case (.valid, true): // This is a trailers-only response: close server. self.state = .clientClosedServerClosed(.init(previousState: state)) return try self.validateAndReturnStatusAndMetadata(metadata) - } else { + case (.valid, false): switch self.processInboundEncoding(metadata) { case .error(let failure): return failure diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index 705caa3e2..34e615623 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -268,6 +268,28 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { } } + func testReceiveInvalidInitialMetadataWhenServerIdle() throws { + for targetState in [ + TargetStateMachineState.clientOpenServerIdle, .clientClosedServerIdle, + ] { + var stateMachine = self.makeClientStateMachine(targetState: targetState) + + // Receive metadata with unexpected non-200 status code + let action = try stateMachine.receive( + metadata: [GRPCHTTP2Keys.status.rawValue: "300"], + endStream: false + ) + + XCTAssertEqual( + action, + .receivedStatusAndMetadata( + status: .init(code: .unknown, message: "Unexpected non-200 HTTP Status Code."), + metadata: [":status": "300"] + ) + ) + } + } + func testReceiveInitialMetadataWhenServerIdle() throws { for targetState in [ TargetStateMachineState.clientOpenServerIdle, .clientClosedServerIdle, From 96c0f8ada4b3e234478c91884e7f3246f41fd83d Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Fri, 15 Mar 2024 10:05:39 +0000 Subject: [PATCH 50/51] Change order of required request headers --- Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 2a346e476..3a5ef5f22 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -440,11 +440,16 @@ extension GRPCStreamStateMachine { var headers = HPACKHeaders() headers.reserveCapacity(7 + customMetadata.count) - // Add required headers + // Add required headers. // See https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests - headers.add(methodDescriptor.fullyQualifiedMethod, forKey: .path) - headers.add(scheme.rawValue, forKey: .scheme) + + // The order is important here: reserved HTTP2 headers (those starting with `:`) + // must come before all other headers. headers.add("POST", forKey: .method) + headers.add(scheme.rawValue, forKey: .scheme) + headers.add(methodDescriptor.fullyQualifiedMethod, forKey: .path) + + // Add required gRPC headers. headers.add(ContentType.grpc.canonicalValue, forKey: .contentType) headers.add("trailers", forKey: .te) // Used to detect incompatible proxies From c33dcc93490f67f0a2e3d9f73dbe998e5a4d21c6 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Fri, 15 Mar 2024 10:06:50 +0000 Subject: [PATCH 51/51] Fix server transition from idle to open when client is closed --- .../GRPCHTTP2Core/GRPCStreamStateMachine.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 3a5ef5f22..0cd0ca797 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -187,7 +187,7 @@ private enum GRPCStreamStateMachineState { var framer: GRPCMessageFramer var compressor: Zlib.Compressor? - let deframer: NIOSingleStepByteToMessageProcessor + let deframer: NIOSingleStepByteToMessageProcessor? var decompressor: Zlib.Decompressor? var inboundMessageBuffer: OneOrManyQueue<[UInt8]> @@ -205,11 +205,10 @@ private enum GRPCStreamStateMachineState { self.framer = previousState.framer self.compressor = previousState.compressor - // In the case of the server, it will already have a deframer set up, - // because it already knows what encoding the client is using: - // it's okay to force-unwrap. - self.deframer = previousState.deframer! - self.decompressor = previousState.decompressor + // In the case of the server, we don't need to deframe/decompress any more + // messages, since the client's closed. + self.deframer = nil + self.decompressor = nil self.inboundMessageBuffer = previousState.inboundMessageBuffer } @@ -442,13 +441,13 @@ extension GRPCStreamStateMachine { // Add required headers. // See https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests - + // The order is important here: reserved HTTP2 headers (those starting with `:`) // must come before all other headers. headers.add("POST", forKey: .method) headers.add(scheme.rawValue, forKey: .scheme) headers.add(methodDescriptor.fullyQualifiedMethod, forKey: .path) - + // Add required gRPC headers. headers.add(ContentType.grpc.canonicalValue, forKey: .contentType) headers.add("trailers", forKey: .te) // Used to detect incompatible proxies @@ -834,7 +833,8 @@ extension GRPCStreamStateMachine { case .clientClosedServerOpen(var state): // The client may have sent the end stream and thus it's closed, // but the server may still be responding. - try state.deframer.process(buffer: bytes) { deframedMessage in + // The client must have a deframer set up, so force-unwrap is okay. + try state.deframer!.process(buffer: bytes) { deframedMessage in state.inboundMessageBuffer.append(deframedMessage) } if endStream {