diff --git a/library/kotlin/src/io/envoyproxy/envoymobile/EnvoyStreamEmitter.kt b/library/kotlin/src/io/envoyproxy/envoymobile/EnvoyStreamEmitter.kt index b21bb492e7..415aad6475 100644 --- a/library/kotlin/src/io/envoyproxy/envoymobile/EnvoyStreamEmitter.kt +++ b/library/kotlin/src/io/envoyproxy/envoymobile/EnvoyStreamEmitter.kt @@ -36,12 +36,16 @@ class EnvoyStreamEmitter( /** * For ending an associated stream and sending trailers. * - * @param trailers to send with ending a stream. If no trailers are needed, empty map will be the default. + * @param trailers to send with ending a stream. If null, stream will be closed with an empty data frame. * @throws IllegalStateException when the stream is not active. * @throws EnvoyException when there is an exception ending the stream or sending trailers. */ - override fun close(trailers: Map>) { - stream.sendTrailers(trailers) + override fun close(trailers: Map>?) { + trailers?.let { + stream.sendTrailers(it) + return + } + stream.sendData(ByteBuffer.allocate(0), true) } /** diff --git a/library/kotlin/src/io/envoyproxy/envoymobile/StreamEmitter.kt b/library/kotlin/src/io/envoyproxy/envoymobile/StreamEmitter.kt index a425e2a486..fb7408cb5a 100644 --- a/library/kotlin/src/io/envoyproxy/envoymobile/StreamEmitter.kt +++ b/library/kotlin/src/io/envoyproxy/envoymobile/StreamEmitter.kt @@ -44,10 +44,10 @@ interface StreamEmitter : CancelableStream { /** * For ending an associated stream and sending trailers. * - * @param trailers to send with ending a stream. If no trailers are needed, empty map will be the default. + * @param trailers to send with ending a stream. If null, stream will be closed with an empty data frame. * @throws IllegalStateException when the stream is not active. * @throws EnvoyException when there is an exception ending the stream or sending trailers. */ @Throws(EnvoyException::class) - fun close(trailers: Map> = emptyMap()) + fun close(trailers: Map>?) } diff --git a/library/kotlin/test/io/envoyproxy/envoymobile/EnvoyClientTest.kt b/library/kotlin/test/io/envoyproxy/envoymobile/EnvoyClientTest.kt index b25793341c..16e2fe3c8f 100644 --- a/library/kotlin/test/io/envoyproxy/envoymobile/EnvoyClientTest.kt +++ b/library/kotlin/test/io/envoyproxy/envoymobile/EnvoyClientTest.kt @@ -84,7 +84,7 @@ class EnvoyClientTest { } @Test - fun `closing stream sends empty trailers to the underlying stream`() { + fun `closing stream sends empty data to the underlying stream`() { `when`(engine.startStream(any())).thenReturn(stream) val envoy = Envoy(engine, config) @@ -97,9 +97,9 @@ class EnvoyClientTest { .build(), ResponseHandler(Executor {})) - emitter.close() + emitter.close(null) - verify(stream).sendTrailers(emptyMap()) + verify(stream).sendData(ByteBuffer.allocate(0), true) } @Test diff --git a/library/swift/src/EnvoyStreamEmitter.swift b/library/swift/src/EnvoyStreamEmitter.swift index df18ac02fa..5f9ccefae4 100644 --- a/library/swift/src/EnvoyStreamEmitter.swift +++ b/library/swift/src/EnvoyStreamEmitter.swift @@ -21,8 +21,12 @@ extension EnvoyStreamEmitter: StreamEmitter { return self } - func close(trailers: [String: [String]]) { - self.stream.sendTrailers(trailers) + func close(trailers: [String: [String]]?) { + if let trailers = trailers { + self.stream.sendTrailers(trailers) + } else { + self.stream.send(Data(), close: true) + } } func cancel() { diff --git a/library/swift/src/GRPCStreamEmitter.swift b/library/swift/src/GRPCStreamEmitter.swift index 960e072312..c444dc020b 100644 --- a/library/swift/src/GRPCStreamEmitter.swift +++ b/library/swift/src/GRPCStreamEmitter.swift @@ -44,6 +44,9 @@ public final class GRPCStreamEmitter: NSObject { /// Close this connection. public func close() { - self.underlyingEmitter.close() + // The gRPC protocol requires the client stream to close with a DATA frame. + // More information here: + // https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests + self.underlyingEmitter.close(trailers: nil) } } diff --git a/library/swift/src/StreamEmitter.swift b/library/swift/src/StreamEmitter.swift index 028d4dab34..1d40d1725b 100644 --- a/library/swift/src/StreamEmitter.swift +++ b/library/swift/src/StreamEmitter.swift @@ -26,15 +26,9 @@ public protocol StreamEmitter: CancelableStream { @discardableResult func sendMetadata(_ metadata: [String: [String]]) -> StreamEmitter - /// End the stream after sending any provided trailers. + /// End the stream. /// - /// - parameter trailers: Trailers to send over the stream. - func close(trailers: [String: [String]]) -} - -extension StreamEmitter { - /// Convenience function for ending the stream without sending any trailers. - public func close() { - self.close(trailers: [:]) - } + /// - parameter trailers: Trailers with which to close the stream. + // If nil, stream will be closed with an empty data frame. + func close(trailers: [String: [String]]?) } diff --git a/library/swift/test/GRPCStreamEmitterTests.swift b/library/swift/test/GRPCStreamEmitterTests.swift index 1b568df74e..855fd802ca 100644 --- a/library/swift/test/GRPCStreamEmitterTests.swift +++ b/library/swift/test/GRPCStreamEmitterTests.swift @@ -20,7 +20,7 @@ private final class MockEmitter: StreamEmitter { return self } - func close(trailers: [String: [String]]) {} + func close(trailers: [String: [String]]?) {} func cancel() {} }