Skip to content

Commit

Permalink
Treat response trailers as regular headers
Browse files Browse the repository at this point in the history
  • Loading branch information
igor-vovk committed Dec 1, 2024
1 parent 676a9ae commit 9116cd3
Show file tree
Hide file tree
Showing 5 changed files with 40 additions and 18 deletions.
25 changes: 14 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Scala:

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

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

Expand Down Expand Up @@ -71,7 +71,7 @@ supports_message_receive_limit: false
## Usage
Library is installed via SBT (you also need to install particular `http4s` server implementation):
Installing with SBT (you also need to install particular `http4s` server implementation):

```scala
libraryDependencies ++= Seq(
Expand Down Expand Up @@ -109,10 +109,10 @@ val httpServer: Resource[IO, org.http4s.server.Server] = {
httpServer.use(_ => IO.never).unsafeRunSync()
```

### Tip: GRPC OpenTelemetry integration
### Hint: GRPC OpenTelemetry integration

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

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

Expand All @@ -129,8 +129,11 @@ ConnectRouteBuilder.forServices[IO](grpcServices)
.build
```

This will make sure that all the traffic going through the Connect-RPC server will be captured by the same
opentelemetry.
### ZIO Interop

Because the library uses the Tagless Final pattern, it is perfectly possible to use it with ZIO. You might check
`zio_interop` branch, where conformance is implemented with `ZIO` and `ZIO-gRPC`.
You can read [this](https://zio.dev/guides/interop/with-cats-effect/).

## Development

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

### Conformance tests status

Current status: 6/79 tests pass
Current status: 11/79 tests pass.

Known issues:

* `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)
* `google.protobuf.Any` serialization doesn't follow Connect-RPC spec: [#32](https://github.com/igor-vovk/connect-rpc-scala/issues/32)
* `google.protobuf.Any` serialization doesn't follow Connect-RPC
spec: [#32](https://github.com/igor-vovk/connect-rpc-scala/issues/32)

## Future improvements

* Support GET-requests
* Support non-unary (streaming) methods
[x] Support GET-requests
[ ] Support non-unary (streaming) methods
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,10 @@ class ConformanceServiceImpl[F[_] : Async] extends ConformanceServiceFs2GrpcTrai
throw new StatusRuntimeException(status)
}

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

Async[F].sleep(Duration(responseDefinition.responseDelayMs, TimeUnit.MILLISECONDS)) *>
Async[F].pure(UnaryHandlerResponse(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class ConnectHandler[F[_] : Async](
methodRegistry: MethodRegistry,
channel: Channel,
httpDsl: Http4sDsl[F],
treatTrailersAsHeaders: Boolean,
) {

import httpDsl.*
Expand Down Expand Up @@ -119,12 +120,11 @@ class ConnectHandler[F[_] : Async](
message
)
}).map { response =>
val headers = org.http4s.Headers.empty ++
responseHeaderMetadata.get.toHeaders ++
responseTrailerMetadata.get.toTrailingHeaders
val headers = responseHeaderMetadata.get.toHeaders() ++
responseTrailerMetadata.get.toHeaders(trailing = !treatTrailersAsHeaders)

if (logger.isTraceEnabled) {
logger.trace(s"<<< Headers: ${headers.redactSensitive}")
logger.trace(s"<<< Headers: ${headers.redactSensitive()}")
}

Response(Ok, headers = headers).withEntity(response)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ case class ConnectRouteBuilder[F[_] : Async] private(
channelConfigurator: Endo[ManagedChannelBuilder[_]] = identity,
executor: Executor = ExecutionContext.global,
waitForShutdown: Duration = 5.seconds,
treatTrailersAsHeaders: Boolean = true,
) {

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

/**
* If enabled, trailers will be treated as headers (no "trailer-" prefix).
*
* Both `fs2-grpc` and `zio-grpc` support trailing headers only, so enabling this option is a single way to
* send headers from the server to the client.
*
* Enabled by default.
*/
def withTreatTrailersAsHeaders(enabled: Boolean): ConnectRouteBuilder[F] =
copy(treatTrailersAsHeaders = enabled)

/**
* Method can be used if you want to add additional routes to the server.
* Otherwise, it is preferred to use the [[build]] method.
Expand Down Expand Up @@ -86,6 +98,7 @@ case class ConnectRouteBuilder[F[_] : Async] private(
methodRegistry,
channel,
httpDsl,
treatTrailersAsHeaders,
)

HttpRoutes.of[F] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,12 @@ trait HeaderMappings {
new Headers(b.result())
}

def toHeaders: Headers = headers()
def toHeaders(trailing: Boolean = false): Headers = {
val prefix = if trailing then "trailer-" else ""

headers(prefix)
}

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

}
Expand Down

0 comments on commit 9116cd3

Please sign in to comment.