com.avast.grpc.jsonbridge.akkahttp.AkkaHttp.scala Maven / Gradle / Ivy
package com.avast.grpc.jsonbridge.akkahttp
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
import akka.http.scaladsl.model.StatusCodes.ClientError
import akka.http.scaladsl.model._
import akka.http.scaladsl.model.headers.`Content-Type`
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.{PathMatcher, Route}
import cats.data.NonEmptyList
import cats.effect.Effect
import cats.effect.implicits._
import com.avast.grpc.jsonbridge.GrpcJsonBridge.GrpcMethodName
import com.avast.grpc.jsonbridge.{BridgeError, BridgeErrorResponse, GrpcJsonBridge}
import com.typesafe.scalalogging.LazyLogging
import io.grpc.Status.Code
import spray.json._
import scala.util.control.NonFatal
import scala.util.{Failure, Success}
object AkkaHttp extends SprayJsonSupport with DefaultJsonProtocol with LazyLogging {
private implicit val grpcStatusJsonFormat: RootJsonFormat[BridgeErrorResponse] = jsonFormat3(BridgeErrorResponse.apply)
private[akkahttp] final val JsonContentType: `Content-Type` = `Content-Type` {
ContentType.WithMissingCharset(MediaType.applicationWithOpenCharset("json"))
}
def apply[F[_]: Effect](configuration: Configuration)(bridge: GrpcJsonBridge[F]): Route = {
val pathPattern = configuration.pathPrefix
.map { case NonEmptyList(head, tail) =>
val rest = if (tail.nonEmpty) {
tail.foldLeft[PathMatcher[Unit]](Neutral)(_ / _)
} else Neutral
head ~ rest
}
.map(_ / Segment / Segment)
.getOrElse(Segment / Segment)
logger.info(s"Creating Akka HTTP service proxying gRPC services: ${bridge.servicesNames.mkString("[", ", ", "]")}")
post {
path(pathPattern) { (serviceName, methodName) =>
extractRequest { request =>
val headers = request.headers
request.header[`Content-Type`] match {
case Some(`JsonContentType`) =>
entity(as[String]) { body =>
val methodNameString = GrpcMethodName(serviceName, methodName)
val headersString = mapHeaders(headers)
val methodCall = bridge.invoke(methodNameString, body, headersString).toIO.unsafeToFuture()
onComplete(methodCall) {
case Success(result) =>
result match {
case Right(resp) =>
logger.trace("Request successful: {}", resp.substring(0, 100))
respondWithHeader(JsonContentType) {
complete(resp)
}
case Left(er) =>
er match {
case BridgeError.GrpcMethodNotFound =>
val message = s"Method '${methodNameString.fullName}' not found"
logger.debug(message)
respondWithHeader(JsonContentType) {
complete(StatusCodes.NotFound, BridgeErrorResponse.fromMessage(message))
}
case er: BridgeError.Json =>
val message = "Wrong JSON"
logger.debug(message, er.t)
respondWithHeader(JsonContentType) {
complete(StatusCodes.BadRequest, BridgeErrorResponse.fromException(message, er.t))
}
case er: BridgeError.Grpc =>
val message = "gRPC error" + Option(er.s.getDescription).map(": " + _).getOrElse("")
logger.trace(message, er.s.getCause)
val (s, body) = mapStatus(er.s)
respondWithHeader(JsonContentType) {
complete(s, body)
}
case er: BridgeError.Unknown =>
val message = "Unknown error"
logger.warn(message, er.t)
respondWithHeader(JsonContentType) {
complete(StatusCodes.InternalServerError, BridgeErrorResponse.fromException(message, er.t))
}
}
}
case Failure(NonFatal(er)) =>
val message = "Unknown exception"
logger.debug(message, er)
respondWithHeader(JsonContentType) {
complete(StatusCodes.InternalServerError, BridgeErrorResponse.fromException(message, er))
}
case Failure(e) => throw e // scalafix:ok
}
}
case Some(c) =>
val message = s"Content-Type must be '$JsonContentType', it is '$c'"
logger.debug(message)
respondWithHeader(JsonContentType) {
complete(StatusCodes.BadRequest, BridgeErrorResponse.fromMessage(message))
}
case None =>
val message = s"Content-Type must be '$JsonContentType'"
logger.debug(message)
respondWithHeader(JsonContentType) {
complete(StatusCodes.BadRequest, BridgeErrorResponse.fromMessage(message))
}
}
}
}
} ~ get {
path(Segment) { serviceName =>
NonEmptyList.fromList(bridge.methodsNames.filter(_.service == serviceName).toList) match {
case None =>
val message = s"Service '$serviceName' not found"
logger.debug(message)
respondWithHeader(JsonContentType) {
complete(StatusCodes.NotFound, BridgeErrorResponse.fromMessage(message))
}
case Some(methods) =>
complete(methods.map(_.fullName).toList.mkString("\n"))
}
}
} ~ get {
path(PathEnd) {
complete(bridge.methodsNames.map(_.fullName).mkString("\n"))
}
}
}
private def mapHeaders(headers: Seq[HttpHeader]): Map[String, String] = headers.toList.map(h => (h.name(), h.value())).toMap
// https://github.com/grpc/grpc/blob/master/doc/statuscodes.md
private def mapStatus(s: io.grpc.Status): (StatusCode, BridgeErrorResponse) = {
val description = BridgeErrorResponse.fromGrpcStatus(s)
s.getCode match {
case Code.OK => (StatusCodes.OK, description)
case Code.CANCELLED =>
(ClientError(499)("Client Closed Request", "The operation was cancelled, typically by the caller."), description)
case Code.UNKNOWN => (StatusCodes.InternalServerError, description)
case Code.INVALID_ARGUMENT => (StatusCodes.BadRequest, description)
case Code.DEADLINE_EXCEEDED => (StatusCodes.GatewayTimeout, description)
case Code.NOT_FOUND => (StatusCodes.NotFound, description)
case Code.ALREADY_EXISTS => (StatusCodes.Conflict, description)
case Code.PERMISSION_DENIED => (StatusCodes.Forbidden, description)
case Code.RESOURCE_EXHAUSTED => (StatusCodes.TooManyRequests, description)
case Code.FAILED_PRECONDITION => (StatusCodes.BadRequest, description)
case Code.ABORTED => (StatusCodes.Conflict, description)
case Code.OUT_OF_RANGE => (StatusCodes.BadRequest, description)
case Code.UNIMPLEMENTED => (StatusCodes.NotImplemented, description)
case Code.INTERNAL => (StatusCodes.InternalServerError, description)
case Code.UNAVAILABLE => (StatusCodes.ServiceUnavailable, description)
case Code.DATA_LOSS => (StatusCodes.InternalServerError, description)
case Code.UNAUTHENTICATED => (StatusCodes.Unauthorized, description)
}
}
}
final case class Configuration private (pathPrefix: Option[NonEmptyList[String]])
object Configuration {
val Default: Configuration = Configuration(
pathPrefix = None
)
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy