Skip to content

Commit fc8e3b2

Browse files
authored
Merge pull request #137 from moleike/add-status-codes
Add gRPC status codes
2 parents 3e6d932 + 8c5e58a commit fc8e3b2

File tree

6 files changed

+147
-43
lines changed

6 files changed

+147
-43
lines changed

codegen/testing/src/test/scala/hello/world/TestServiceSuite.scala

+41-4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import fs2.Stream
66
import munit._
77
import org.http4s._
88
import org.http4s.client.Client
9+
import org.http4s.grpc.GrpcStatus._
910
import org.http4s.syntax.all._
1011
import org.scalacheck._
1112
import org.scalacheck.effect.PropF.forAllF
@@ -127,7 +128,7 @@ class TestServiceSuite extends CatsEffectSuite with ScalaCheckEffectSuite {
127128
status.pure[IO]
128129
}
129130
.assertEquals(
130-
Some(org.http4s.grpc.codecs.NamedHeaders.GrpcStatus(12))
131+
Some(org.http4s.grpc.codecs.NamedHeaders.GrpcStatus(Unimplemented))
131132
)
132133
}
133134

@@ -136,7 +137,7 @@ class TestServiceSuite extends CatsEffectSuite with ScalaCheckEffectSuite {
136137
val route = org.http4s.HttpRoutes.of[IO] { case _ =>
137138
Response(Status.Ok)
138139
.putHeaders(
139-
org.http4s.grpc.codecs.NamedHeaders.GrpcStatus(12)
140+
org.http4s.grpc.codecs.NamedHeaders.GrpcStatus(Unimplemented)
140141
)
141142
.pure[IO]
142143
}
@@ -148,7 +149,7 @@ class TestServiceSuite extends CatsEffectSuite with ScalaCheckEffectSuite {
148149
.`export`(msg, Headers.empty)
149150
.attemptNarrow[org.http4s.grpc.GrpcExceptions.StatusRuntimeException]
150151
.map(_.leftMap(grpcFailed => grpcFailed.status))
151-
.assertEquals(Either.left(12))
152+
.assertEquals(Either.left(Unimplemented))
152153
}
153154
}
154155

@@ -179,7 +180,43 @@ class TestServiceSuite extends CatsEffectSuite with ScalaCheckEffectSuite {
179180
.noStreaming(msg, Headers.empty)
180181
.attemptNarrow[org.http4s.grpc.GrpcExceptions.StatusRuntimeException]
181182
.map(_.leftMap(grpcFailed => grpcFailed.status))
182-
.assertEquals(Either.left(2))
183+
.assertEquals(Either.left(Unknown))
184+
}
185+
186+
}
187+
188+
test("Server Fails with an Status Code") {
189+
190+
implicit val arbitraryStatusCode: Arbitrary[Code] = Arbitrary(
191+
Gen.oneOf(codeValues.filter(_ != Ok))
192+
)
193+
194+
forAllF { (msg: TestMessage, statusCode: Code) =>
195+
val ts = new TestService[IO] {
196+
def noStreaming(request: TestMessage, ctx: Headers): IO[TestMessage] =
197+
IO(request) <* IO.raiseError(statusCode.asStatusRuntimeException())
198+
199+
def clientStreaming(request: Stream[IO, TestMessage], ctx: Headers): IO[TestMessage] =
200+
request.compile.lastOrError
201+
202+
def serverStreaming(request: TestMessage, ctx: Headers): Stream[IO, TestMessage] =
203+
Stream.emit(request)
204+
205+
def bothStreaming(request: Stream[IO, TestMessage], ctx: Headers): Stream[IO, TestMessage] =
206+
request
207+
208+
def `export`(request: TestMessage, ctx: Headers): IO[TestMessage] = IO(request)
209+
}
210+
val client = TestService.fromClient[IO](
211+
Client.fromHttpApp(TestService.toRoutes[IO](ts).orNotFound),
212+
Uri(),
213+
)
214+
215+
client
216+
.noStreaming(msg, Headers.empty)
217+
.attemptNarrow[org.http4s.grpc.GrpcExceptions.StatusRuntimeException]
218+
.map(_.leftMap(grpcFailed => grpcFailed.status))
219+
.assertEquals(Either.left(statusCode))
183220
}
184221

185222
}

core/src/main/scala/org/http4s/grpc/ClientGrpc.scala

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import cats.syntax.all._
66
import fs2._
77
import org.http4s._
88
import org.http4s.client.Client
9+
import org.http4s.grpc.GrpcStatus._
910
import org.http4s.grpc.codecs.NamedHeaders
1011
import org.http4s.h2.H2Keys
1112
import scodec.Decoder
@@ -177,7 +178,7 @@ object ClientGrpc {
177178
val reason = headers.get[NamedHeaders.GrpcMessage]
178179

179180
status match {
180-
case Some(NamedHeaders.GrpcStatus(0)) => ().pure[F]
181+
case Some(NamedHeaders.GrpcStatus(Ok)) => ().pure[F]
181182
case Some(NamedHeaders.GrpcStatus(status)) =>
182183
GrpcExceptions.StatusRuntimeException(status, reason.map(_.message)).raiseError[F, Unit]
183184
case None => ().pure[F]
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package org.http4s.grpc
22

33
object GrpcExceptions {
4-
final case class StatusRuntimeException(status: Int, message: Option[String])
4+
final case class StatusRuntimeException(status: GrpcStatus.Code, message: Option[String])
55
extends RuntimeException({
66
val me = message.fold("")((m: String) => s", Message-${m}")
7-
s"Grpc Failed: Status-$status${me}"
8-
})
7+
s"Grpc Failed: Status-${status.value}${me}"
8+
}) {
9+
assert(status != GrpcStatus.Ok)
10+
}
911
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package org.http4s.grpc
2+
3+
import GrpcExceptions.StatusRuntimeException
4+
5+
object GrpcStatus {
6+
7+
sealed abstract class Code(val value: Int) extends Product with Serializable {
8+
def asStatusRuntimeException(message: Option[String] = None): StatusRuntimeException =
9+
StatusRuntimeException(this, message)
10+
}
11+
12+
case object Ok extends Code(0)
13+
14+
case object Cancelled extends Code(1)
15+
16+
case object Unknown extends Code(2)
17+
18+
case object InvalidArgument extends Code(3)
19+
20+
case object DeadlineExceeded extends Code(4)
21+
22+
case object NotFound extends Code(5)
23+
24+
case object AlreadyExists extends Code(6)
25+
26+
case object PermissionDenied extends Code(7)
27+
28+
case object ResourceExhausted extends Code(8)
29+
30+
case object FailedPrecondition extends Code(9)
31+
32+
case object Aborted extends Code(10)
33+
34+
case object OutOfRange extends Code(11)
35+
36+
case object Unimplemented extends Code(12)
37+
38+
case object Internal extends Code(13)
39+
40+
case object Unavailable extends Code(14)
41+
42+
case object DataLoss extends Code(15)
43+
44+
case object Unauthenticated extends Code(16)
45+
46+
def fromCodeValue(value: Int): Option[Code] = codeValues.find(_.value == value)
47+
48+
val codeValues: List[Code] = List(
49+
Ok,
50+
Cancelled,
51+
Unknown,
52+
InvalidArgument,
53+
DeadlineExceeded,
54+
NotFound,
55+
AlreadyExists,
56+
PermissionDenied,
57+
ResourceExhausted,
58+
FailedPrecondition,
59+
Aborted,
60+
OutOfRange,
61+
Unimplemented,
62+
Internal,
63+
Unavailable,
64+
DataLoss,
65+
Unauthenticated,
66+
)
67+
68+
}

core/src/main/scala/org/http4s/grpc/ServerGrpc.scala

+24-32
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import cats.syntax.all._
66
import fs2._
77
import org.http4s._
88
import org.http4s.dsl.request._
9+
import org.http4s.grpc.GrpcExceptions.StatusRuntimeException
10+
import org.http4s.grpc.GrpcStatus._
911
import org.http4s.grpc.codecs.NamedHeaders
1012
import org.http4s.headers.Allow
1113
import org.http4s.headers.Trailer
@@ -38,7 +40,7 @@ object ServerGrpc {
3840
): HttpRoutes[F] = HttpRoutes.of[F] {
3941
case req @ POST -> Root / sN / mN if sN === serviceName && mN === methodName =>
4042
for {
41-
status <- Deferred[F, (Int, Option[String])]
43+
status <- Deferred[F, (Code, Option[String])]
4244
trailers = status.get.map { case (i, message) =>
4345
Headers(
4446
NamedHeaders.GrpcStatus(i)
@@ -51,12 +53,7 @@ object ServerGrpc {
5153
.evalMap(f(_, req.headers))
5254
.flatMap(codecs.Messages.encodeSingle(encode)(_))
5355
.through(timeoutStream(_)(timeout.map(_.duration)))
54-
.onFinalizeCaseWeak {
55-
case Resource.ExitCase.Errored(_: TimeoutException) => status.complete((4, None)).void
56-
case Resource.ExitCase.Errored(e) => status.complete((2, e.toString().some)).void
57-
case Resource.ExitCase.Canceled => status.complete((1, None)).void
58-
case Resource.ExitCase.Succeeded => status.complete((0, None)).void
59-
}
56+
.onFinalizeCaseWeak(updateStatus(status))
6057
.mask // ensures body closure without rst-stream
6158

6259
Response[F](Status.Ok, HttpVersion.`HTTP/2`)
@@ -81,7 +78,7 @@ object ServerGrpc {
8178
): HttpRoutes[F] = HttpRoutes.of[F] {
8279
case req @ POST -> Root / sN / mN if sN === serviceName && mN === methodName =>
8380
for {
84-
status <- Deferred[F, (Int, Option[String])]
81+
status <- Deferred[F, (Code, Option[String])]
8582
trailers = status.get.map { case (i, message) =>
8683
Headers(
8784
NamedHeaders.GrpcStatus(i)
@@ -94,12 +91,7 @@ object ServerGrpc {
9491
.flatMap(f(_, req.headers))
9592
.through(codecs.Messages.encode(encode))
9693
.through(timeoutStream(_)(timeout.map(_.duration)))
97-
.onFinalizeCaseWeak {
98-
case Resource.ExitCase.Errored(_: TimeoutException) => status.complete((4, None)).void
99-
case Resource.ExitCase.Errored(e) => status.complete((2, e.toString().some)).void
100-
case Resource.ExitCase.Canceled => status.complete((1, None)).void
101-
case Resource.ExitCase.Succeeded => status.complete((0, None)).void
102-
}
94+
.onFinalizeCaseWeak(updateStatus(status))
10395
.mask // ensures body closure without rst-stream
10496
Response[F](Status.Ok, HttpVersion.`HTTP/2`)
10597
.putHeaders(
@@ -123,7 +115,7 @@ object ServerGrpc {
123115
): HttpRoutes[F] = HttpRoutes.of[F] {
124116
case req @ POST -> Root / sN / mN if sN === serviceName && mN === methodName =>
125117
for {
126-
status <- Deferred[F, (Int, Option[String])]
118+
status <- Deferred[F, (Code, Option[String])]
127119
trailers = status.get.map { case (i, message) =>
128120
Headers(
129121
NamedHeaders.GrpcStatus(i)
@@ -136,12 +128,7 @@ object ServerGrpc {
136128
.eval(f(codecs.Messages.decode(decode)(req.body), req.headers))
137129
.flatMap(codecs.Messages.encodeSingle(encode)(_))
138130
.through(timeoutStream(_)(timeout.map(_.duration)))
139-
.onFinalizeCaseWeak {
140-
case Resource.ExitCase.Errored(_: TimeoutException) => status.complete((4, None)).void
141-
case Resource.ExitCase.Errored(e) => status.complete((2, e.toString().some)).void
142-
case Resource.ExitCase.Canceled => status.complete((1, None)).void
143-
case Resource.ExitCase.Succeeded => status.complete((0, None)).void
144-
}
131+
.onFinalizeCaseWeak(updateStatus(status))
145132
.mask // ensures body closure without rst-stream
146133

147134
Response[F](Status.Ok, HttpVersion.`HTTP/2`)
@@ -166,7 +153,7 @@ object ServerGrpc {
166153
): HttpRoutes[F] = HttpRoutes.of[F] {
167154
case req @ POST -> Root / sN / mN if sN === serviceName && mN === methodName =>
168155
for {
169-
status <- Deferred[F, (Int, Option[String])]
156+
status <- Deferred[F, (Code, Option[String])]
170157
trailers = status.get.map { case (i, message) =>
171158
Headers(
172159
NamedHeaders.GrpcStatus(i)
@@ -178,12 +165,7 @@ object ServerGrpc {
178165
val body = f(codecs.Messages.decode(decode)(req.body), req.headers)
179166
.through(codecs.Messages.encode(encode))
180167
.through(timeoutStream(_)(timeout.map(_.duration)))
181-
.onFinalizeCaseWeak {
182-
case Resource.ExitCase.Errored(_: TimeoutException) => status.complete((4, None)).void
183-
case Resource.ExitCase.Errored(e) => status.complete((2, e.toString().some)).void
184-
case Resource.ExitCase.Canceled => status.complete((1, None)).void
185-
case Resource.ExitCase.Succeeded => status.complete((0, None)).void
186-
}
168+
.onFinalizeCaseWeak(updateStatus(status))
187169
.mask // ensures body closure without rst-stream
188170

189171
Response[F](Status.Ok, HttpVersion.`HTTP/2`)
@@ -204,7 +186,7 @@ object ServerGrpc {
204186
.putHeaders(
205187
SharedGrpc.ContentType,
206188
SharedGrpc.TE,
207-
NamedHeaders.GrpcStatus(12),
189+
NamedHeaders.GrpcStatus(Unimplemented),
208190
"grpc-message" -> s"unknown method $mN for service $sN",
209191
)
210192
.pure[F]
@@ -216,7 +198,7 @@ object ServerGrpc {
216198
.putHeaders(
217199
SharedGrpc.ContentType,
218200
SharedGrpc.TE,
219-
NamedHeaders.GrpcStatus(12),
201+
NamedHeaders.GrpcStatus(Unimplemented),
220202
"grpc-message" -> s"unknown service $sN",
221203
)
222204
.pure[F]
@@ -225,7 +207,7 @@ object ServerGrpc {
225207
.putHeaders(
226208
SharedGrpc.ContentType,
227209
SharedGrpc.TE,
228-
NamedHeaders.GrpcStatus(12),
210+
NamedHeaders.GrpcStatus(Unimplemented),
229211
"grpc-message" -> s"unknown method $other",
230212
)
231213
.pure[F]
@@ -234,7 +216,7 @@ object ServerGrpc {
234216
.putHeaders(
235217
SharedGrpc.ContentType,
236218
SharedGrpc.TE,
237-
NamedHeaders.GrpcStatus(12),
219+
NamedHeaders.GrpcStatus(Unimplemented),
238220
"grpc-message" -> s"unknown request",
239221
)
240222
.pure[F]
@@ -248,4 +230,14 @@ object ServerGrpc {
248230
case Some(value) => s.timeout(value)
249231
}
250232

233+
private def updateStatus[F[_]: Concurrent](
234+
status: Deferred[F, (Code, Option[String])]
235+
): Resource.ExitCase => F[Unit] = {
236+
case Resource.ExitCase.Errored(StatusRuntimeException(c, m)) => status.complete((c, m)).void
237+
case Resource.ExitCase.Errored(_: TimeoutException) =>
238+
status.complete((DeadlineExceeded, None)).void
239+
case Resource.ExitCase.Errored(e) => status.complete((Unknown, e.toString().some)).void
240+
case Resource.ExitCase.Canceled => status.complete((Cancelled, None)).void
241+
case Resource.ExitCase.Succeeded => status.complete((Ok, None)).void
242+
}
251243
}

core/src/main/scala/org/http4s/grpc/codecs/NamedHeaders.scala

+7-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import cats.parse.Parser
44
import cats.syntax.all._
55
import org.http4s.Header
66
import org.http4s.ParseResult
7+
import org.http4s.grpc.GrpcStatus.Code
8+
import org.http4s.grpc.GrpcStatus.fromCodeValue
79
import org.http4s.internal.parsing.CommonRules.ows
810
import org.http4s.parser.AdditionalRules
911
import org.typelevel.ci.CIString
@@ -55,14 +57,16 @@ object NamedHeaders {
5557
}
5658

5759
// https://grpc.github.io/grpc/core/md_doc_statuscodes.html
58-
final case class GrpcStatus(statusCode: Int)
60+
final case class GrpcStatus(statusCode: Code)
5961

6062
object GrpcStatus {
61-
private val parser = cats.parse.Numbers.nonNegativeIntString.map(s => GrpcStatus(s.toInt))
63+
private val parser = cats.parse.Numbers.nonNegativeIntString
64+
.mapFilter(s => fromCodeValue(s.toInt))
65+
.map(GrpcStatus(_))
6266

6367
implicit val header: Header[GrpcStatus, Header.Single] = Header.create(
6468
CIString("grpc-status"),
65-
(t: GrpcStatus) => t.statusCode.toString(),
69+
(t: GrpcStatus) => t.statusCode.value.toString(),
6670
(s: String) => ParseResult.fromParser(parser, "Invalid GrpcStatus")(s),
6771
)
6872
}

0 commit comments

Comments
 (0)