From 7f211936b80b8a78f8ec36f0cbc1129e07d16556 Mon Sep 17 00:00:00 2001 From: "Taro L. Saito" Date: Wed, 27 Apr 2022 16:01:38 -0700 Subject: [PATCH] airframe-grpc: Propagate RPCException to clients (#2139) * Add RPCStatus.ofCodeName * Add RPCStatus.toException * propargate stack trace to RPC clients * Check stacktrace propagation * Change the key name * Test suppressed stacktrace * Fix status code list --- .../airframe/codec/StringUnapplyCodec.scala | 3 +- .../wvlet/airframe/codec/ThrowableCodec.scala | 12 ++- .../http/grpc/internal/GrpcException.scala | 23 ++++- .../http/grpc/GrpcErrorHandlingTest.scala | 93 +++++++++++++++++-- .../airframe/http/grpc/example/DemoApi.scala | 37 +++++++- .../wvlet/airframe/http/RPCException.scala | 89 +++++++++++++++--- .../scala/wvlet/airframe/http/RPCStatus.scala | 78 ++++++++++++---- .../airframe/http/RPCExceptionTest.scala | 34 ++++++- .../wvlet/airframe/http/RPCStatusTest.scala | 30 +++++- 9 files changed, 347 insertions(+), 52 deletions(-) diff --git a/airframe-codec/.jvm/src/main/scala/wvlet/airframe/codec/StringUnapplyCodec.scala b/airframe-codec/.jvm/src/main/scala/wvlet/airframe/codec/StringUnapplyCodec.scala index 321351cc07..3444ea0c15 100644 --- a/airframe-codec/.jvm/src/main/scala/wvlet/airframe/codec/StringUnapplyCodec.scala +++ b/airframe-codec/.jvm/src/main/scala/wvlet/airframe/codec/StringUnapplyCodec.scala @@ -24,8 +24,9 @@ class StringUnapplyCodec[A](codec: Surface) extends MessageCodec[A] with LogSupp override def pack(p: Packer, v: A): Unit = { p.packString(v.toString) } + override def unpack(u: Unpacker, v: MessageContext): Unit = { - val s = u.unpackString + val s = u.unpackValue.toString TypeConverter.convertToCls(s, codec.rawType) match { case Some(x) => v.setObject(x) diff --git a/airframe-codec/src/main/scala/wvlet/airframe/codec/ThrowableCodec.scala b/airframe-codec/src/main/scala/wvlet/airframe/codec/ThrowableCodec.scala index 34ffd7f5b8..a5dcdf9772 100644 --- a/airframe-codec/src/main/scala/wvlet/airframe/codec/ThrowableCodec.scala +++ b/airframe-codec/src/main/scala/wvlet/airframe/codec/ThrowableCodec.scala @@ -60,10 +60,7 @@ case class GenericException( } object GenericException { - def fromThrowable(e: Throwable, seen: Set[Throwable] = Set.empty): GenericException = { - val exceptionClass = e.getClass.getName - val message = Option(e.getMessage).getOrElse(e.getClass.getSimpleName) - + def extractStackTrace(e: Throwable): Seq[GenericStackTraceElement] = { val stackTrace = for (x <- e.getStackTrace) yield { GenericStackTraceElement( className = x.getClassName, @@ -72,7 +69,14 @@ object GenericException { lineNumber = x.getLineNumber ) } + stackTrace + } + + def fromThrowable(e: Throwable, seen: Set[Throwable] = Set.empty): GenericException = { + val exceptionClass = e.getClass.getName + val message = Option(e.getMessage).getOrElse(e.getClass.getSimpleName) + val stackTrace = extractStackTrace(e) val cause = Option(e.getCause).flatMap { ce => if (seen.contains(ce)) { None diff --git a/airframe-http-grpc/src/main/scala/wvlet/airframe/http/grpc/internal/GrpcException.scala b/airframe-http-grpc/src/main/scala/wvlet/airframe/http/grpc/internal/GrpcException.scala index 4706179175..9035818512 100644 --- a/airframe-http-grpc/src/main/scala/wvlet/airframe/http/grpc/internal/GrpcException.scala +++ b/airframe-http-grpc/src/main/scala/wvlet/airframe/http/grpc/internal/GrpcException.scala @@ -15,7 +15,7 @@ package wvlet.airframe.http.grpc.internal import io.grpc.{Metadata, Status, StatusException, StatusRuntimeException} import wvlet.airframe.codec.MessageCodecException -import wvlet.airframe.http.{GrpcStatus, HttpServerException, HttpStatus} +import wvlet.airframe.http.{GrpcStatus, HttpServerException, HttpStatus, RPCException} import wvlet.log.LogSupport import java.lang.reflect.InvocationTargetException @@ -26,7 +26,7 @@ import scala.concurrent.ExecutionException */ object GrpcException extends LogSupport { - private[grpc] val rpcErrorKey = Metadata.Key.of[String]("airframe_rpc_error", Metadata.ASCII_STRING_MARSHALLER) + private[grpc] val rpcErrorBodyKey = Metadata.Key.of[String]("airframe_rpc_error", Metadata.ASCII_STRING_MARSHALLER) /** * Convert an exception to gRPC-specific exception types @@ -81,11 +81,28 @@ object GrpcException extends LogSupport { if (e.message.nonEmpty) { val m = e.message val metadata = new Metadata() - metadata.put[String](rpcErrorKey, s"${m.toContentString}") + metadata.put[String](rpcErrorBodyKey, s"${m.toContentString}") s.asRuntimeException(metadata) } else { s.asRuntimeException() } + case e: RPCException => + val grpcStatus = e.status.grpcStatus + val s = Status + .fromCodeValue(grpcStatus.code) + .withCause(e.cause.getOrElse(null)) + .withDescription(e.getMessage) + + val metadata = new Metadata() + try { + metadata.put[String](rpcErrorBodyKey, e.toJson) + } catch { + case ex: Throwable => + // Failed to build JSON data. + // Just show warning so as not to block the RPC response + warn(s"Failed to serialize RPCException: ${e}", ex) + } + s.asRuntimeException(metadata) case other => io.grpc.Status.INTERNAL .withCause(other) diff --git a/airframe-http-grpc/src/test/scala/wvlet/airframe/http/grpc/GrpcErrorHandlingTest.scala b/airframe-http-grpc/src/test/scala/wvlet/airframe/http/grpc/GrpcErrorHandlingTest.scala index deed9bf8c8..133ebf8394 100644 --- a/airframe-http-grpc/src/test/scala/wvlet/airframe/http/grpc/GrpcErrorHandlingTest.scala +++ b/airframe-http-grpc/src/test/scala/wvlet/airframe/http/grpc/GrpcErrorHandlingTest.scala @@ -16,11 +16,14 @@ package wvlet.airframe.http.grpc import io.grpc.Status import io.grpc.Status.Code import io.grpc.StatusRuntimeException -import wvlet.airframe.http.Router +import wvlet.airframe.http.{RPCException, RPCStatus, Router} import wvlet.airframe.http.grpc.GrpcErrorLogTest.DemoApiDebug import wvlet.airframe.http.grpc.example.DemoApi.DemoApiClient import wvlet.airframe.http.grpc.internal.GrpcException import wvlet.airspec.AirSpec +import wvlet.log.{LogLevel, Logger} + +import java.io.{PrintWriter, StringWriter} object GrpcErrorHandlingTest extends AirSpec { @@ -31,16 +34,86 @@ object GrpcErrorHandlingTest extends AirSpec { .designWithChannel } - test("handle error") { (client: DemoApiClient) => + private def suppressLog(loggerName: String)(body: => Unit): Unit = { + val l = Logger(loggerName) + val previousLogLevel = l.getLogLevel + try { + // Suppress error logs + l.setLogLevel(LogLevel.OFF) + body + } finally { + l.setLogLevel(previousLogLevel) + } + } + test("exception test") { (client: DemoApiClient) => warn("Starting a gRPC error handling test") - val ex = intercept[StatusRuntimeException] { - client.error409Test + suppressLog("wvlet.airframe.http.grpc.internal") { + + test("propagate HttpServerException") { + val ex = intercept[StatusRuntimeException] { + client.error409Test + } + ex.getMessage.contains("409") shouldBe true + ex.getStatus.isOk shouldBe false + ex.getStatus.getCode shouldBe Code.ABORTED + val trailers = Status.trailersFromThrowable(ex) + val rpcError = trailers.get[String](GrpcException.rpcErrorBodyKey) + rpcError.contains("test message") shouldBe true + } + + test("propagate RPCException") { + val ex = intercept[StatusRuntimeException] { + client.rpcExceptionTest(false) + } + val trailers = Status.trailersFromThrowable(ex) + val rpcErrorJson = trailers.get[String](GrpcException.rpcErrorBodyKey) + val e = RPCException.fromJson(rpcErrorJson) + + e.status shouldBe RPCStatus.SYNTAX_ERROR_U3 + e.message shouldBe "test RPC exception" + e.cause shouldNotBe empty + e.appErrorCode shouldBe Some(11) + e.metadata shouldBe Map("retry" -> 0) + + // Extract stack trace + val s = new StringWriter() + val out = new PrintWriter(s) + e.printStackTrace(out) + out.flush() + + val stackTrace = s.toString + // Stack trace should contain two traces from the exception itself and its cause + stackTrace.contains("wvlet.airframe.http.RPCStatus.newException") shouldBe true + stackTrace.contains("wvlet.airframe.http.grpc.example.DemoApi.throwEx") shouldBe true + stackTrace.contains("wvlet.airframe.http.grpc.example.DemoApi.rpcExceptionTest") shouldBe true + } + + test("suppress RPCException stacktrace") { + val ex = intercept[StatusRuntimeException] { + client.rpcExceptionTest(true) + } + val trailers = Status.trailersFromThrowable(ex) + val rpcErrorJson = trailers.get[String](GrpcException.rpcErrorBodyKey) + val e = RPCException.fromJson(rpcErrorJson) + + e.status shouldBe RPCStatus.SYNTAX_ERROR_U3 + e.message shouldBe "test RPC exception" + e.cause shouldBe empty + e.appErrorCode shouldBe Some(11) + e.metadata shouldBe Map("retry" -> 0) + + // Extract stack trace + val s = new StringWriter() + val out = new PrintWriter(s) + e.printStackTrace(out) + out.flush() + + val stackTrace = s.toString + // Stack trace should not have detailed information when RPCException.noStackTrace is used + stackTrace.contains("wvlet.airframe.http.RPCStatus.newException") shouldBe false + stackTrace.contains("wvlet.airframe.http.grpc.example.DemoApi.throwEx") shouldBe false + stackTrace.contains("wvlet.airframe.http.grpc.example.DemoApi.rpcExceptionTest") shouldBe false + } } - ex.getMessage.contains("409") shouldBe true - ex.getStatus.isOk shouldBe false - ex.getStatus.getCode shouldBe Code.ABORTED - val trailers = Status.trailersFromThrowable(ex) - val rpcError = trailers.get[String](GrpcException.rpcErrorKey) - info(s"error trailer: ${rpcError}") } } diff --git a/airframe-http-grpc/src/test/scala/wvlet/airframe/http/grpc/example/DemoApi.scala b/airframe-http-grpc/src/test/scala/wvlet/airframe/http/grpc/example/DemoApi.scala index 5b689eb533..6f95d3e8ca 100644 --- a/airframe-http-grpc/src/test/scala/wvlet/airframe/http/grpc/example/DemoApi.scala +++ b/airframe-http-grpc/src/test/scala/wvlet/airframe/http/grpc/example/DemoApi.scala @@ -20,7 +20,7 @@ import wvlet.airframe.codec.MessageCodecFactory import wvlet.airframe.http.grpc.internal.GrpcServiceBuilder import wvlet.airframe.http.grpc._ import wvlet.airframe.http.router.Route -import wvlet.airframe.http.{Http, HttpStatus, RPC, Router} +import wvlet.airframe.http.{Http, HttpStatus, RPC, RPCStatus, Router} import wvlet.airframe.msgpack.spi.MsgPack import wvlet.airframe.rx.{Rx, RxStream} import wvlet.log.LogSupport @@ -65,6 +65,27 @@ trait DemoApi extends LogSupport { def error409Test: String = { throw Http.serverException(HttpStatus.Conflict_409).withContent("test message") } + + private def throwEx = throw new IllegalArgumentException("syntax error") + + def rpcExceptionTest(suppress: Boolean): String = { + try { + throwEx + "" + } catch { + case e: Throwable => + val ex = RPCStatus.SYNTAX_ERROR_U3.newException( + message = "test RPC exception", + cause = e, + appErrorCode = 11, + metadata = Map("retry" -> 0) + ) + if (suppress) { + ex.noStackTrace + } + throw ex + } + } } object DemoApi { @@ -118,6 +139,8 @@ object DemoApi { GrpcServiceBuilder.buildMethodDescriptor(getRoute("returnUnit"), codecFactory) private val errorTestMethodDescriptor = GrpcServiceBuilder.buildMethodDescriptor(getRoute("error409Test"), codecFactory) + private val rpcExceptionTestMethodDescriptor = + GrpcServiceBuilder.buildMethodDescriptor(getRoute("rpcExceptionTest"), codecFactory) def withEncoding(encoding: GrpcEncoding): DemoApiClient = { this.copy(encoding = encoding) @@ -209,6 +232,18 @@ object DemoApi { resp.asInstanceOf[String] } + + def rpcExceptionTest(suppress: Boolean): String = { + val resp = ClientCalls + .blockingUnaryCall( + _channel, + rpcExceptionTestMethodDescriptor, + getCallOptions, + encode(Map("suppress" -> suppress)) + ) + + resp.asInstanceOf[String] + } } } diff --git a/airframe-http/src/main/scala/wvlet/airframe/http/RPCException.scala b/airframe-http/src/main/scala/wvlet/airframe/http/RPCException.scala index e37dda0017..3ccc0d6f47 100644 --- a/airframe-http/src/main/scala/wvlet/airframe/http/RPCException.scala +++ b/airframe-http/src/main/scala/wvlet/airframe/http/RPCException.scala @@ -13,27 +13,86 @@ */ package wvlet.airframe.http +import wvlet.airframe.codec.{GenericException, GenericStackTraceElement, MessageCodec} + /** - * RPCException provides a backend-independent (e.g., Finagle or gRPC) RPC error reporting mechanism. + * RPCException provides a backend-independent (e.g., Finagle or gRPC) RPC error reporting mechanism. Create this + * exception with (RPCStatus code).toException(...) method. * - * @param rpcError + * If necessary, we can add more standard error_details parameter like + * https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto */ -class RPCException( - rpcError: RPCError -) extends Exception(rpcError.toString, rpcError.cause.getOrElse(null)) - -case class RPCError( +case class RPCException( // RPC status - status: RPCStatus, + status: RPCStatus = RPCStatus.INTERNAL_ERROR_I0, // Error message - message: String, + message: String = "", // Cause of the exception cause: Option[Throwable] = None, - // Custom data + // [optional] Application-specific status code + appErrorCode: Option[Int] = None, + // [optional] Application-specific metadata + metadata: Map[String, Any] = Map.empty +) extends Exception(s"[${status}] ${message}", cause.getOrElse(null)) { + + private var _includeStackTrace: Boolean = true + + /** + * Do not embed stacktrace and the cause objects in the RPC exception error response + */ + def noStackTrace: RPCException = { + _includeStackTrace = false + this + } + + def toMessage: RPCErrorMessage = { + RPCErrorMessage( + code = status.code, + codeName = status.name, + message = message, + stackTrace = if (_includeStackTrace) Some(GenericException.extractStackTrace(this)) else None, + cause = if (_includeStackTrace) cause else None, + appErrorCode = appErrorCode, + metadata = metadata + ) + } + + def toJson: String = { + MessageCodec.of[RPCErrorMessage].toJson(toMessage) + } +} + +/** + * A model class for RPC error message body. This message will be embedded to HTTP response body or gRPC trailer. + * + * We need this class to avoid directly serde RPCException classes with airframe-codec, so that we can properly + * propagate the exact stack trace to the client. + */ +case class RPCErrorMessage( + code: Int = RPCStatus.UNKNOWN_I1.code, + codeName: String = RPCStatus.UNKNOWN_I1.name, + message: String = "", + stackTrace: Option[Seq[GenericStackTraceElement]] = None, + cause: Option[Throwable] = None, + appErrorCode: Option[Int] = None, metadata: Map[String, Any] = Map.empty -) { - override def toString: String = s"[${status}] ${message}" - def toException: RPCException = new RPCException(this) - def withMessage(newMessage: String): RPCError = this.copy(message = newMessage) - def withMetadata(newMetadata: Map[String, Any]): RPCError = this.copy(metadata = newMetadata) +) + +object RPCException { + def fromJson(json: String): RPCException = { + val codec = MessageCodec.of[RPCErrorMessage] + val m = codec.fromJson(json) + val ex = new RPCException( + status = RPCStatus.ofCode(m.code), + message = m.message, + cause = m.cause, + appErrorCode = m.appErrorCode, + metadata = m.metadata + ) + // Recover the original stack trace + m.stackTrace.foreach { x => + ex.setStackTrace(x.map(_.toJavaStackTraceElement).toArray) + } + ex + } } diff --git a/airframe-http/src/main/scala/wvlet/airframe/http/RPCStatus.scala b/airframe-http/src/main/scala/wvlet/airframe/http/RPCStatus.scala index f340eec3e6..c363dbb5b0 100644 --- a/airframe-http/src/main/scala/wvlet/airframe/http/RPCStatus.scala +++ b/airframe-http/src/main/scala/wvlet/airframe/http/RPCStatus.scala @@ -14,7 +14,7 @@ package wvlet.airframe.http import wvlet.airframe.codec.PackSupport -import wvlet.airframe.msgpack.spi.Value.{IntegerValue, LongValue} +import wvlet.airframe.msgpack.spi.Value.{IntegerValue, LongValue, StringValue} import wvlet.airframe.msgpack.spi.{Packer, Value} import scala.util.Try @@ -30,12 +30,21 @@ object RPCStatus { import RPCStatusType._ - private val codeTable: Map[Int, RPCStatus] = all.map { x => x.code -> x }.toMap + private lazy val codeTable: Map[Int, RPCStatus] = all.map { x => x.code -> x }.toMap + private lazy val codeNameTable: Map[String, RPCStatus] = all.map { x => x.name -> x }.toMap + + def unapply(s: String): Option[RPCStatus] = { + Try(ofCode(s.toInt)).toOption + } def unapply(v: Value): Option[RPCStatus] = { v match { case l: LongValue => Try(ofCode(l.asInt)).toOption + case s: StringValue => + Try(ofCodeName(s.toString)) + .orElse(Try(ofCode(s.toString.toInt))) + .toOption case _ => None } @@ -45,6 +54,10 @@ object RPCStatus { codeTable.getOrElse(code, throw new IllegalArgumentException(s"Invalid RPCStatus code: ${code}")) } + def ofCodeName(name: String): RPCStatus = { + codeNameTable.getOrElse(name, throw new IllegalArgumentException(s"Invalid RPCStatus name: ${name}")) + } + def all: Seq[RPCStatus] = successes ++ userErrors ++ internalErrors ++ resourceErrors @@ -68,13 +81,14 @@ object RPCStatus { private def internalErrors: Seq[RPCStatus] = Seq( INTERNAL_ERROR_I0, - UNAVAILABLE_I1, - TIMEOUT_I2, - DEADLINE_EXCEEDED_I3, - INTERRUPTED_I4, - SERVICE_STARTING_UP_I5, - SERVICE_SHUTTING_DOWN_I6, - DATA_LOSS_I7 + UNKNOWN_I1, + UNAVAILABLE_I2, + TIMEOUT_I3, + DEADLINE_EXCEEDED_I4, + INTERRUPTED_I5, + SERVICE_STARTING_UP_I6, + SERVICE_SHUTTING_DOWN_I7, + DATA_LOSS_I8 ) private def resourceErrors: Seq[RPCStatus] = Seq( @@ -172,41 +186,46 @@ object RPCStatus { */ case object INTERNAL_ERROR_I0 extends RPCStatus(INTERNAL_ERROR, GrpcStatus.INTERNAL_13) + /** + * An unknown internal error + */ + case object UNKNOWN_I1 extends RPCStatus(INTERNAL_ERROR, GrpcStatus.UNKNOWN_2) + /** * The service is unavailable. */ - case object UNAVAILABLE_I1 extends RPCStatus(INTERNAL_ERROR, GrpcStatus.UNAVAILABLE_14) + case object UNAVAILABLE_I2 extends RPCStatus(INTERNAL_ERROR, GrpcStatus.UNAVAILABLE_14) /** * The service respond the request in time (e.g., circuit breaker is open, timeout exceeded, etc.) For operations * that change the system state, this error might be returned even if the operation has completed successfully. */ - case object TIMEOUT_I2 extends RPCStatus(INTERNAL_ERROR, GrpcStatus.DEADLINE_EXCEEDED_4) + case object TIMEOUT_I3 extends RPCStatus(INTERNAL_ERROR, GrpcStatus.DEADLINE_EXCEEDED_4) /** * The request cannot be processed in the user-specified deadline. The client may retry the request */ - case object DEADLINE_EXCEEDED_I3 extends RPCStatus(INTERNAL_ERROR, GrpcStatus.DEADLINE_EXCEEDED_4) + case object DEADLINE_EXCEEDED_I4 extends RPCStatus(INTERNAL_ERROR, GrpcStatus.DEADLINE_EXCEEDED_4) /** * The request is interrupted at the service */ - case object INTERRUPTED_I4 extends RPCStatus(INTERNAL_ERROR, GrpcStatus.INTERNAL_13) + case object INTERRUPTED_I5 extends RPCStatus(INTERNAL_ERROR, GrpcStatus.INTERNAL_13) /** * The service is starting now. The client can retry the request after a while */ - case object SERVICE_STARTING_UP_I5 extends RPCStatus(INTERNAL_ERROR, GrpcStatus.UNAVAILABLE_14) + case object SERVICE_STARTING_UP_I6 extends RPCStatus(INTERNAL_ERROR, GrpcStatus.UNAVAILABLE_14) /** * The service is shutting down now. */ - case object SERVICE_SHUTTING_DOWN_I6 extends RPCStatus(INTERNAL_ERROR, GrpcStatus.UNAVAILABLE_14) + case object SERVICE_SHUTTING_DOWN_I7 extends RPCStatus(INTERNAL_ERROR, GrpcStatus.UNAVAILABLE_14) /** * Data loss or corrupted data */ - case object DATA_LOSS_I7 extends RPCStatus(INTERNAL_ERROR, GrpcStatus.DATA_LOSS_15) + case object DATA_LOSS_I8 extends RPCStatus(INTERNAL_ERROR, GrpcStatus.DATA_LOSS_15) /** * The resource for completing the request is insufficient. @@ -315,4 +334,31 @@ sealed abstract class RPCStatus( override def pack(p: Packer): Unit = { p.packInt(code) } + + /** + * Create a new RPCException with this RPCStatus. + * @param message + * the error message (required) + * @param cause + * the cause of the error (optional) + * @param appErrorCode + * application-specific error code. default: -1 (None) + * @param metadata + * application-specific metadata (optional) + */ + def newException( + message: String, + cause: Throwable = null, + appErrorCode: Int = -1, + metadata: Map[String, Any] = Map.empty + ): RPCException = { + RPCException( + status = this, + message = message, + cause = Option(cause), + appErrorCode = if (appErrorCode == -1) None else Some(appErrorCode), + metadata = metadata + ) + } + } diff --git a/airframe-http/src/test/scala/wvlet/airframe/http/RPCExceptionTest.scala b/airframe-http/src/test/scala/wvlet/airframe/http/RPCExceptionTest.scala index 9711886836..85a87eec82 100644 --- a/airframe-http/src/test/scala/wvlet/airframe/http/RPCExceptionTest.scala +++ b/airframe-http/src/test/scala/wvlet/airframe/http/RPCExceptionTest.scala @@ -16,8 +16,40 @@ package wvlet.airframe.http import wvlet.airspec.AirSpec class RPCExceptionTest extends AirSpec { + + private def newTestException = RPCStatus.INVALID_REQUEST_U1.newException( + "invalid RPC request", + new IllegalArgumentException("syntax error"), + appErrorCode = 10, + metadata = Map("line" -> 100, "pos" -> 10) + ) + test("Create a new RPCException") { - // RPCException.userError + RPCStatus.USER_ERROR_U0.newException(s"user error test") + } + + test("toMap error contents") { + val e1 = newTestException + val m = e1.toMessage + m.code shouldBe e1.status.code + m.codeName shouldBe e1.status.name + m.message shouldBe e1.message + m.appErrorCode shouldBe e1.appErrorCode + m.metadata shouldBe e1.metadata + m.cause shouldNotBe empty + } + + test("hide stack trace") { + val e1 = newTestException + e1.noStackTrace + val m = e1.toMessage + m.cause shouldBe empty + } + + test("toJson error contents") { + val e1 = newTestException + // sanity test + val json = e1.toJson } } diff --git a/airframe-http/src/test/scala/wvlet/airframe/http/RPCStatusTest.scala b/airframe-http/src/test/scala/wvlet/airframe/http/RPCStatusTest.scala index 354c16e4de..61c74de0cb 100644 --- a/airframe-http/src/test/scala/wvlet/airframe/http/RPCStatusTest.scala +++ b/airframe-http/src/test/scala/wvlet/airframe/http/RPCStatusTest.scala @@ -19,12 +19,22 @@ import wvlet.airspec.AirSpec class RPCStatusTest extends AirSpec { - test("No duplicates") { + test("Have no duplicates and no gaps") { var knownCodes = Set.empty[Int] + var counter = 0 + var currentType: Option[RPCStatusType] = None + RPCStatus.all.foreach { x => + // Bump the counter for each statusType + if (currentType.isEmpty || currentType != Some(x.statusType)) { + currentType = Some(x.statusType) + counter = x.statusType.minCode + } knownCodes.contains(x.code) shouldBe false knownCodes += x.code + x.code shouldBe counter + counter += 1 // sanity test val errorDetails = s"${x}[${x.code}] ${x.grpcStatus} ${x.httpStatus}" @@ -38,6 +48,24 @@ class RPCStatusTest extends AirSpec { } } + test("ofCode('unknown code')") { + intercept[IllegalArgumentException] { + RPCStatus.ofCode(-1) shouldBe None + } + } + + test("ofCodeName maps to the right code") { + RPCStatus.all.foreach { x => + RPCStatus.ofCodeName(x.name) shouldBe x + } + } + + test("ofCodeName('unknown code name')") { + intercept[IllegalArgumentException] { + RPCStatus.ofCodeName("INVALID_CODE_000") shouldBe None + } + } + test("serialize as integer") { RPCStatus.all.foreach { x => val packer = MessagePack.newBufferPacker