Skip to content

Commit 67dd651

Browse files
authored
Treat response trailers as regular headers (#38)
1 parent 676a9ae commit 67dd651

File tree

5 files changed

+40
-18
lines changed

5 files changed

+40
-18
lines changed

README.md

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Scala:
1313

1414
* [ScalaPB](https://scalapb.github.io) services with `Future` monad
1515
* [fs2-grpc](https://github.com/typelevel/fs2-grpc), built on top of `cats-effect` and `fs2`
16-
* [ZIO gRPC](https://scalapb.github.io/zio-grpc/), built on top of `ZIO` monad (the most feature-rich implementation)
16+
* [ZIO gRPC](https://scalapb.github.io/zio-grpc/), built on top of `ZIO`
1717

1818
*Note*: at the moment, only unary (non-streaming) methods are supported.
1919

@@ -71,7 +71,7 @@ supports_message_receive_limit: false
7171
7272
## Usage
7373
74-
Library is installed via SBT (you also need to install particular `http4s` server implementation):
74+
Installing with SBT (you also need to install particular `http4s` server implementation):
7575

7676
```scala
7777
libraryDependencies ++= Seq(
@@ -109,10 +109,10 @@ val httpServer: Resource[IO, org.http4s.server.Server] = {
109109
httpServer.use(_ => IO.never).unsafeRunSync()
110110
```
111111

112-
### Tip: GRPC OpenTelemetry integration
112+
### Hint: GRPC OpenTelemetry integration
113113

114114
Since the library creates a separate "fake" GRPC server, traffic going through it won't be captured by the
115-
instrumentation of the main GRPC server.
115+
instrumentation of your main GRPC server (if any).
116116

117117
Here is how you can integrate OpenTelemetry with the Connect-RPC server:
118118

@@ -129,8 +129,11 @@ ConnectRouteBuilder.forServices[IO](grpcServices)
129129
.build
130130
```
131131

132-
This will make sure that all the traffic going through the Connect-RPC server will be captured by the same
133-
opentelemetry.
132+
### ZIO Interop
133+
134+
Because the library uses the Tagless Final pattern, it is perfectly possible to use it with ZIO. You might check
135+
`zio_interop` branch, where conformance is implemented with `ZIO` and `ZIO-gRPC`.
136+
You can read [this](https://zio.dev/guides/interop/with-cats-effect/).
134137

135138
## Development
136139

@@ -147,14 +150,14 @@ Diagnostic data from the server itself is written to the log file `out/out.log`.
147150

148151
### Conformance tests status
149152

150-
Current status: 6/79 tests pass
153+
Current status: 11/79 tests pass.
151154

152155
Known issues:
153156

154-
* `fs2-grpc` server implementation doesn't support setting response headers (which is required by the tests): [#31](https://github.com/igor-vovk/connect-rpc-scala/issues/31)
155-
* `google.protobuf.Any` serialization doesn't follow Connect-RPC spec: [#32](https://github.com/igor-vovk/connect-rpc-scala/issues/32)
157+
* `google.protobuf.Any` serialization doesn't follow Connect-RPC
158+
spec: [#32](https://github.com/igor-vovk/connect-rpc-scala/issues/32)
156159

157160
## Future improvements
158161

159-
* Support GET-requests
160-
* Support non-unary (streaming) methods
162+
[x] Support GET-requests
163+
[ ] Support non-unary (streaming) methods

conformance/src/main/scala/org/ivovk/connect_rpc_scala/conformance/ConformanceServiceImpl.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,10 @@ class ConformanceServiceImpl[F[_] : Async] extends ConformanceServiceFs2GrpcTrai
7575
throw new StatusRuntimeException(status)
7676
}
7777

78-
val trailers = mkMetadata(responseDefinition.responseTrailers)
78+
val trailers = mkMetadata(Seq.concat(
79+
responseDefinition.responseHeaders,
80+
responseDefinition.responseTrailers.map(h => h.copy(name = s"trailer-${h.name}")),
81+
))
7982

8083
Async[F].sleep(Duration(responseDefinition.responseDelayMs, TimeUnit.MILLISECONDS)) *>
8184
Async[F].pure(UnaryHandlerResponse(

core/src/main/scala/org/ivovk/connect_rpc_scala/ConnectHandler.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class ConnectHandler[F[_] : Async](
2727
methodRegistry: MethodRegistry,
2828
channel: Channel,
2929
httpDsl: Http4sDsl[F],
30+
treatTrailersAsHeaders: Boolean,
3031
) {
3132

3233
import httpDsl.*
@@ -119,12 +120,11 @@ class ConnectHandler[F[_] : Async](
119120
message
120121
)
121122
}).map { response =>
122-
val headers = org.http4s.Headers.empty ++
123-
responseHeaderMetadata.get.toHeaders ++
124-
responseTrailerMetadata.get.toTrailingHeaders
123+
val headers = responseHeaderMetadata.get.toHeaders() ++
124+
responseTrailerMetadata.get.toHeaders(trailing = !treatTrailersAsHeaders)
125125

126126
if (logger.isTraceEnabled) {
127-
logger.trace(s"<<< Headers: ${headers.redactSensitive}")
127+
logger.trace(s"<<< Headers: ${headers.redactSensitive()}")
128128
}
129129

130130
Response(Ok, headers = headers).withEntity(response)

core/src/main/scala/org/ivovk/connect_rpc_scala/ConnectRouteBuilder.scala

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ case class ConnectRouteBuilder[F[_] : Async] private(
3535
channelConfigurator: Endo[ManagedChannelBuilder[_]] = identity,
3636
executor: Executor = ExecutionContext.global,
3737
waitForShutdown: Duration = 5.seconds,
38+
treatTrailersAsHeaders: Boolean = true,
3839
) {
3940

4041
import Mappings.*
@@ -54,6 +55,17 @@ case class ConnectRouteBuilder[F[_] : Async] private(
5455
def withWaitForShutdown(duration: Duration): ConnectRouteBuilder[F] =
5556
copy(waitForShutdown = duration)
5657

58+
/**
59+
* If enabled, trailers will be treated as headers (no "trailer-" prefix).
60+
*
61+
* Both `fs2-grpc` and `zio-grpc` support trailing headers only, so enabling this option is a single way to
62+
* send headers from the server to the client.
63+
*
64+
* Enabled by default.
65+
*/
66+
def withTreatTrailersAsHeaders(enabled: Boolean): ConnectRouteBuilder[F] =
67+
copy(treatTrailersAsHeaders = enabled)
68+
5769
/**
5870
* Method can be used if you want to add additional routes to the server.
5971
* Otherwise, it is preferred to use the [[build]] method.
@@ -86,6 +98,7 @@ case class ConnectRouteBuilder[F[_] : Async] private(
8698
methodRegistry,
8799
channel,
88100
httpDsl,
101+
treatTrailersAsHeaders,
89102
)
90103

91104
HttpRoutes.of[F] {

core/src/main/scala/org/ivovk/connect_rpc_scala/Mappings.scala

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,12 @@ trait HeaderMappings {
4040
new Headers(b.result())
4141
}
4242

43-
def toHeaders: Headers = headers()
43+
def toHeaders(trailing: Boolean = false): Headers = {
44+
val prefix = if trailing then "trailer-" else ""
45+
46+
headers(prefix)
47+
}
4448

45-
def toTrailingHeaders: Headers = headers("trailer-")
4649
}
4750

4851
}

0 commit comments

Comments
 (0)