All Downloads are FREE. Search and download functionalities are using the official Maven repository.

wvlet.airframe.http.RPCStatus.scala Maven / Gradle / Ivy

There is a newer version: 24.12.2
Show newest version
/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package wvlet.airframe.http

import wvlet.airframe.codec.PackSupport
import wvlet.airframe.msgpack.spi.Value.{IntegerValue, LongValue, StringValue}
import wvlet.airframe.msgpack.spi.{Packer, Value}

import scala.util.Try

/**
  * Define the standard RPC code that can be used for generic RPC service implementation.
  *
  * This covers all gRPC statuses and have pre-defined mappings to HTTP status (4xx, 5xx) code.
  *
  * If you need an application-specific error code, use an additional argument of the RPCError class.
  */
object RPCStatus {

  import RPCStatusType._

  // These variables need to be lazy to avoid NPE during the initialization
  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
  private lazy val grpcStatusCodeTable: Map[Int, RPCStatus] = grpcStatusCodeMapping.map { case (g, r) =>
    g.code -> r
  }.toMap

  // Default mapping from gRPC status code
  private lazy val grpcStatusCodeMapping = Map(
    GrpcStatus.OK_0                  -> RPCStatus.SUCCESS_S0,
    GrpcStatus.CANCELLED_1           -> RPCStatus.CANCELLED_U11,
    GrpcStatus.UNKNOWN_2             -> RPCStatus.UNKNOWN_I1,
    GrpcStatus.INVALID_ARGUMENT_3    -> RPCStatus.INVALID_ARGUMENT_U2,
    GrpcStatus.DEADLINE_EXCEEDED_4   -> RPCStatus.DEADLINE_EXCEEDED_I4,
    GrpcStatus.NOT_FOUND_5           -> RPCStatus.NOT_FOUND_U5,
    GrpcStatus.ALREADY_EXISTS_6      -> RPCStatus.ALREADY_EXISTS_U6,
    GrpcStatus.PERMISSION_DENIED_7   -> RPCStatus.PERMISSION_DENIED_U14,
    GrpcStatus.RESOURCE_EXHAUSTED_8  -> RPCStatus.RESOURCE_EXHAUSTED_R0,
    GrpcStatus.FAILED_PRECONDITION_9 -> RPCStatus.UNEXPECTED_STATE_U9,
    GrpcStatus.ABORTED_10            -> RPCStatus.ABORTED_U12,
    GrpcStatus.OUT_OF_RANGE_11       -> RPCStatus.OUT_OF_RANGE_U4,
    GrpcStatus.UNIMPLEMENTED_12      -> RPCStatus.UNIMPLEMENTED_U8,
    GrpcStatus.INTERNAL_13           -> RPCStatus.INTERNAL_ERROR_I0,
    GrpcStatus.UNAVAILABLE_14        -> RPCStatus.UNAVAILABLE_I2,
    GrpcStatus.DATA_LOSS_15          -> RPCStatus.DATA_LOSS_I8,
    GrpcStatus.UNAUTHENTICATED_16    -> RPCStatus.UNAUTHENTICATED_U13
  )

  import HttpStatus._
  private lazy val httpStatusMapping: Map[HttpStatus, RPCStatus] = Map(
    Unknown_000                       -> UNKNOWN_I1,
    Continue_100                      -> UNKNOWN_I1,
    SwitchingProtocols_101            -> UNKNOWN_I1,
    Processing_102                    -> UNKNOWN_I1,
    Ok_200                            -> SUCCESS_S0,
    Created_201                       -> SUCCESS_S0,
    Accepted_202                      -> SUCCESS_S0,
    NonAuthoritativeInformation_203   -> SUCCESS_S0,
    NoContent_204                     -> SUCCESS_S0,
    ResetContent_205                  -> SUCCESS_S0,
    PartialContent_206                -> SUCCESS_S0,
    MultiStatus_207                   -> SUCCESS_S0,
    MultipleChoices_300               -> NOT_FOUND_U5,
    MovedPermanently_301              -> NOT_FOUND_U5,
    Found_302                         -> NOT_FOUND_U5,
    SeeOther_303                      -> NOT_FOUND_U5,
    NotModified_304                   -> NOT_FOUND_U5,
    UseProxy_305                      -> NOT_FOUND_U5,
    TemporaryRedirect_307             -> NOT_FOUND_U5,
    PermanentRedirect_308             -> NOT_FOUND_U5,
    BadRequest_400                    -> INVALID_REQUEST_U1,
    Unauthorized_401                  -> UNAUTHENTICATED_U13,
    PaymentRequired_402               -> EXCEEDED_BUDGET_R8,
    Forbidden_403                     -> PERMISSION_DENIED_U14,
    NotFound_404                      -> NOT_FOUND_U5,
    MethodNotAllowed_405              -> NOT_SUPPORTED_U7,
    NotAcceptable_406                 -> NOT_SUPPORTED_U7,
    ProxyAuthenticationRequired_407   -> UNAUTHENTICATED_U13,
    RequestTimeout_408                -> DEADLINE_EXCEEDED_I4,
    Conflict_409                      -> ABORTED_U12,
    Gone_410                          -> NOT_FOUND_U5,
    LengthRequired_411                -> INVALID_ARGUMENT_U2,
    PreconditionFailed_412            -> UNEXPECTED_STATE_U9,
    RequestEntityTooLarge_413         -> INVALID_ARGUMENT_U2,
    RequestURITooLong_414             -> INVALID_ARGUMENT_U2,
    UnsupportedMediaType_415          -> NOT_SUPPORTED_U7,
    RequestedRangeNotSatisfiable_416  -> OUT_OF_RANGE_U4,
    ExpectationFailed_417             -> UNEXPECTED_STATE_U9,
    EnhanceYourCalm_420               -> RESOURCE_EXHAUSTED_R0,
    UnprocessableEntity_422           -> INVALID_REQUEST_U1,
    Locked_423                        -> UNEXPECTED_STATE_U9,
    FailedDependency_424              -> UNEXPECTED_STATE_U9,
    UnorderedCollection_425           -> INVALID_ARGUMENT_U2,
    UpgradeRequired_426               -> UNEXPECTED_STATE_U9,
    PreconditionRequired_428          -> UNEXPECTED_STATE_U9,
    TooManyRequests_429               -> RESOURCE_EXHAUSTED_R0,
    RequestHeaderFieldsTooLarge_431   -> INVALID_ARGUMENT_U2,
    UnavailableForLegalReasons_451    -> PERMISSION_DENIED_U14,
    ClientClosedRequest_499           -> CANCELLED_U11,
    InternalServerError_500           -> INTERNAL_ERROR_I0,
    NotImplemented_501                -> UNIMPLEMENTED_U8,
    BadGateway_502                    -> UNAVAILABLE_I2,
    ServiceUnavailable_503            -> UNAVAILABLE_I2,
    GatewayTimeout_504                -> DEADLINE_EXCEEDED_I4,
    HttpVersionNotSupported_505       -> NOT_SUPPORTED_U7,
    VariantAlsoNegotiates_506         -> INTERNAL_ERROR_I0,
    InsufficientStorage_507           -> EXCEEDED_STORAGE_LIMIT_R7,
    NotExtended_510                   -> INTERNAL_ERROR_I0,
    NetworkAuthenticationRequired_511 -> UNAUTHENTICATED_U13
  )

  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
    }
  }

  def ofCode(code: Int): 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 fromGrpcStatusCode(grpcStatusCode: Int): RPCStatus = {
    grpcStatusCodeTable.getOrElse(
      grpcStatusCode,
      throw new IllegalArgumentException(s"Invalid gRPC status code: ${grpcStatusCode}")
    )
  }

  /**
    * Mapping HttpStatus to RPCStatus. This is useful when mapping third-party API statuses into RPCStatus
    */
  def fromHttpStatus(httpStatus: HttpStatus): RPCStatus = {
    httpStatusMapping.get(httpStatus) match {
      case Some(status) => status
      case _ =>
        if (httpStatus.isSuccessful) {
          RPCStatus.SUCCESS_S0
        } else if (httpStatus.isClientError) {
          RPCStatus.USER_ERROR_U0
        } else if (httpStatus.isServerError) {
          RPCStatus.INTERNAL_ERROR_I0
        } else {
          RPCStatus.UNKNOWN_I1
        }
    }
  }

  def all: Seq[RPCStatus] =
    successes ++ userErrors ++ internalErrors ++ resourceErrors

  private def successes: Seq[RPCStatus] = Seq(
    SUCCESS_S0
  )

  private def userErrors: Seq[RPCStatus] = Seq(
    USER_ERROR_U0,
    INVALID_REQUEST_U1,
    INVALID_ARGUMENT_U2,
    SYNTAX_ERROR_U3,
    OUT_OF_RANGE_U4,
    NOT_FOUND_U5,
    ALREADY_EXISTS_U6,
    NOT_SUPPORTED_U7,
    UNIMPLEMENTED_U8,
    UNEXPECTED_STATE_U9,
    INCONSISTENT_STATE_U10,
    CANCELLED_U11,
    ABORTED_U12,
    UNAUTHENTICATED_U13,
    PERMISSION_DENIED_U14
  )

  private def internalErrors: Seq[RPCStatus] = Seq(
    INTERNAL_ERROR_I0,
    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(
    RESOURCE_EXHAUSTED_R0,
    OUT_OF_MEMORY_R1,
    EXCEEDED_RATE_LIMIT_R2,
    EXCEEDED_CPU_LIMIT_R3,
    EXCEEDED_MEMORY_LIMIT_R4,
    EXCEEDED_TIME_LIMIT_R5,
    EXCEEDED_DATA_SIZE_LIMIT_R6,
    EXCEEDED_STORAGE_LIMIT_R7,
    EXCEEDED_BUDGET_R8
  )

  case object SUCCESS_S0 extends RPCStatus(SUCCESS, GrpcStatus.OK_0)

  case object USER_ERROR_U0 extends RPCStatus(USER_ERROR, GrpcStatus.INVALID_ARGUMENT_3)

  /**
    * Invalid RPC request. The user should not retry the request in general.
    */
  case object INVALID_REQUEST_U1 extends RPCStatus(USER_ERROR, GrpcStatus.INVALID_ARGUMENT_3)

  /**
    * RPC request arguments have invalid values
    */
  case object INVALID_ARGUMENT_U2 extends RPCStatus(USER_ERROR, GrpcStatus.INVALID_ARGUMENT_3)

  /**
    * Syntax error in an RPC argument
    */
  case object SYNTAX_ERROR_U3 extends RPCStatus(USER_ERROR, GrpcStatus.INVALID_ARGUMENT_3)

  /**
    * Invalid range data is given to an RPC request argument.
    */
  case object OUT_OF_RANGE_U4 extends RPCStatus(USER_ERROR, GrpcStatus.OUT_OF_RANGE_11)

  /**
    * The requested resource or RPC method is not found
    */
  case object NOT_FOUND_U5 extends RPCStatus(USER_ERROR, GrpcStatus.NOT_FOUND_5)

  /**
    * The resource creation request failed because it already exists.
    */
  case object ALREADY_EXISTS_U6 extends RPCStatus(USER_ERROR, GrpcStatus.ALREADY_EXISTS_6)

  /**
    * The requested RPC method is not supported.
    */
  case object NOT_SUPPORTED_U7 extends RPCStatus(USER_ERROR, GrpcStatus.UNIMPLEMENTED_12)

  /**
    * The requested RPC method is not implemented.
    */
  case object UNIMPLEMENTED_U8 extends RPCStatus(USER_ERROR, GrpcStatus.UNIMPLEMENTED_12)

  /**
    * Some precondition to succeed this request is not met (e.g., invalid configuration). The client should not retry
    * the request until fixing the state.
    */
  case object UNEXPECTED_STATE_U9 extends RPCStatus(USER_ERROR, GrpcStatus.FAILED_PRECONDITION_9)

  /**
    * The service or the use has an inconsistent state and cannot fulfill the request. The client should not retry the
    * request until fixing the state.
    */
  case object INCONSISTENT_STATE_U10 extends RPCStatus(USER_ERROR, GrpcStatus.FAILED_PRECONDITION_9)

  /**
    * The request was cancelled, typically by the client. The client should not retry the request unless it's a network
    * issue.
    */
  case object CANCELLED_U11 extends RPCStatus(USER_ERROR, GrpcStatus.CANCELLED_1)

  /**
    * The request is aborted (e.g., dead-lock, transaction conflicts, etc.) The client should retry the request it a
    * higher-level.
    */
  case object ABORTED_U12 extends RPCStatus(USER_ERROR, GrpcStatus.ABORTED_10)

  /**
    * The user has not been authenticated
    */
  case object UNAUTHENTICATED_U13 extends RPCStatus(USER_ERROR, GrpcStatus.UNAUTHENTICATED_16)

  /**
    * The user does not have a permission to access the resource
    */
  case object PERMISSION_DENIED_U14 extends RPCStatus(USER_ERROR, GrpcStatus.PERMISSION_DENIED_7)

  /**
    * Internal failures where the user can retry the request in general
    */
  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_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_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_I4 extends RPCStatus(INTERNAL_ERROR, GrpcStatus.DEADLINE_EXCEEDED_4)

  /**
    * The request is interrupted at the service
    */
  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_I6 extends RPCStatus(INTERNAL_ERROR, GrpcStatus.UNAVAILABLE_14)

  /**
    * The service is shutting down now.
    */
  case object SERVICE_SHUTTING_DOWN_I7 extends RPCStatus(INTERNAL_ERROR, GrpcStatus.UNAVAILABLE_14)

  /**
    * Data loss or corrupted data
    */
  case object DATA_LOSS_I8 extends RPCStatus(INTERNAL_ERROR, GrpcStatus.DATA_LOSS_15)

  /**
    * The resource for completing the request is insufficient.
    */
  case object RESOURCE_EXHAUSTED_R0 extends RPCStatus(RESOURCE_EXHAUSTED, GrpcStatus.RESOURCE_EXHAUSTED_8)

  /**
    * The service is experiencing insufficient memory
    */
  case object OUT_OF_MEMORY_R1 extends RPCStatus(RESOURCE_EXHAUSTED, GrpcStatus.RESOURCE_EXHAUSTED_8)

  /**
    * There are too many requests. The user needs to retry the request after a while
    */
  case object EXCEEDED_RATE_LIMIT_R2 extends RPCStatus(RESOURCE_EXHAUSTED, GrpcStatus.RESOURCE_EXHAUSTED_8)

  /**
    * The user has reached its CPU usage limit
    */
  case object EXCEEDED_CPU_LIMIT_R3 extends RPCStatus(RESOURCE_EXHAUSTED, GrpcStatus.RESOURCE_EXHAUSTED_8)

  /**
    * The user has reached its memory usage limit
    */
  case object EXCEEDED_MEMORY_LIMIT_R4 extends RPCStatus(RESOURCE_EXHAUSTED, GrpcStatus.RESOURCE_EXHAUSTED_8)

  /**
    * The user has reached its running time limit
    */
  case object EXCEEDED_TIME_LIMIT_R5 extends RPCStatus(RESOURCE_EXHAUSTED, GrpcStatus.RESOURCE_EXHAUSTED_8)

  /**
    * The user has reached its data size limit
    */
  case object EXCEEDED_DATA_SIZE_LIMIT_R6 extends RPCStatus(RESOURCE_EXHAUSTED, GrpcStatus.RESOURCE_EXHAUSTED_8)

  /**
    * The user has reached its storage size limit
    */
  case object EXCEEDED_STORAGE_LIMIT_R7 extends RPCStatus(RESOURCE_EXHAUSTED, GrpcStatus.RESOURCE_EXHAUSTED_8)

  /**
    * The user has exhausted the budget for processing the request.
    */
  case object EXCEEDED_BUDGET_R8 extends RPCStatus(RESOURCE_EXHAUSTED, GrpcStatus.RESOURCE_EXHAUSTED_8)

  /**
    * Extracting the error code from the name
    */
  private def extractErrorCode(name: String): Int = {
    val separatorPos = name.lastIndexOf("_")
    separatorPos match {
      case -1 =>
        throw new AssertionError(s"Invalid code name ${name}. It must end with (U/I/R)[0-9]+")
      case pos =>
        val suffix = name.substring(pos + 1)
        if (suffix.length < 2) {
          throw new AssertionError(
            s"Invalid code suffix ${name}. It must have a suffix (U/I/R)[0-9]+"
          )
        }

        try {
          val errorCode  = suffix.substring(1).toInt
          val statusType = RPCStatusType.ofPrefix(suffix.charAt(0))
          statusType.minCode + errorCode
        } catch {
          case e: NumberFormatException =>
            throw new AssertionError(
              s"Invalid code suffix ${name}. It must have an integer suffix: ${e.getMessage}"
            )
        }
    }
  }
}

/**
  * A base class for defining standard RPC error codes
  */
sealed abstract class RPCStatus(
    // Error type (user, internal, or resource)
    val statusType: RPCStatusType,
    // Mapping to an gRPC status code
    val grpcStatus: GrpcStatus
) extends PackSupport {
  assert(statusType.isValidCode(code), s"Status code ${code} is invalid for ${statusType} status type")
  assert(statusType.isValidHttpStatus(httpStatus), s"Unexpected http status ${httpStatus} for the code: ${name}")

  import RPCStatus._

  def isSuccess: Boolean = statusType == RPCStatusType.SUCCESS
  def isFailure: Boolean = !isSuccess

  /**
    * Integer-based error code
    */
  lazy val code: Int = extractErrorCode(name)

  /**
    * The error code name.
    */
  def name: String = this.toString()

  /**
    * http status code derived from grpc status code
    */
  def httpStatus: HttpStatus = grpcStatus.httpStatus

  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
    )
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy