0

Here is my ever-growing validationRejectionHandler for my Spray project:

implicit def validationRejectionHandler = RejectionHandler {
    case ValidationRejection(errMsg,_) :: _ =>
      logger.info(s"received validation error: $errMsg")
      complete(StatusCodes.Unauthorized,errMsg)
    case MalformedQueryParamRejection(parameterName, errorMsg, cause) :: _ =>
      logger.debug(s"received MalformedQueryParamRejection error: $errorMsg")
      complete(BadRequest -> GenericMessageObj(s"The query parameter $parameterName was malformed: $errorMsg"))
    case spray.routing.AuthorizationFailedRejection :: _ =>
      //todo - this string shouldn't be here
      logger.info("received authentication error")
      complete(StatusCodes.Unauthorized, "User is not authorized to this resource")
    case MalformedRequestContentRejection(msg, causeOpt) :: _ =>
      complete {
        causeOpt.map { cause =>
          cause match {
            case e: InvalidFormatException =>
              val fieldNameMatcher = """\["(.+)"\]""".r.unanchored
              val fieldTypeMatcher = """(\w+)$""".r.unanchored
              e.getPath.toString match {
                case fieldNameMatcher(fieldName) =>
                  e.getTargetType.toString match {
                    case fieldTypeMatcher(fieldType) =>
                      val fieldTypeLowerCase = fieldType.toLowerCase()
                      BadRequest -> GenericMessageObj(s"""Invalid data: "${fieldName}" must be a ${fieldTypeLowerCase} value, but received ${e.getValue}""")
                    case _ =>
                      BadRequest -> GenericMessageObj(s"""${e.getValue} is an improper type for field "${fieldName}""")
                  }
                case _ =>
                  logger.debug(s"Failed pattern match: ${e.getPath.toString}")
                  BadRequest -> GenericMessageObj("Invalid payload format")
            }

            case e: UnrecognizedPropertyException => BadRequest -> GenericMessageObj(s"Unrecognized property: ${e.getPropertyName}")

            case e: JsonMappingException =>
              if(cause.getCause == null || cause.getCause.getMessage == null){
                val deserializationMsgMatcher = """Can not deserialize instance of scala\.collection\.(Seq) out of (VALUE_NUMBER_INT) [\s\S]+"(name)":[+-]?\d\};[\s\S]+\["(\3)"\].+""".r.unanchored
                cause.getMessage match {
                  case deserializationMsgMatcher(expected, actual, fieldName, _) =>
                    logger.debug(s"Desrializaiton error at $fieldName: Found $actual instead of $expected")
                    BadRequest -> GenericMessageObj(s"Invalid format for $fieldName")
                  case _ =>
                    BadRequest -> GenericMessageObj(s"${cause.getMessage}")
                  }
              } else if (!cause.getCause.getMessage.isEmpty) {
                BadRequest -> GenericMessageObj(cause.getCause.getMessage)
              } else {
                BadRequest -> GenericMessageObj(s"Invalid data format")
              }
            case _ =>  BadRequest -> GenericMessageObj(s"An unknown error occurred.")
          }
        }
      }
    case spray.routing.MissingHeaderRejection(headerName) :: _ =>
      complete(BadRequest -> GenericMessageObj("%s header is missing.".format(headerName)))
  }
}

It seems crazy to me that this convoluted bit of code - complete with regexes to decipher various types of errors - is the way to handle rejections in Spray so that it doesn't spit back ugly messages like this to API clients:

{ "message": "Missing required creator property 'value' (index 2)\n at [Source: {\n \"source\": \"k\",\n \"values\": [\n {\n \"dt\": \"2015-10-15T16:27:42.014Z\",\n \"id\":\"0022A3000004E6E1\",\n \n \"attribute\":\"a\",\n \"_id\": \"45809haoua\",\n \"__id\": \"a2p49t7ya4wop9h\",\n \"___id\": \"q2ph84yhtq4pthqg\"\n }]\n}; line: 12, column: 9] (through reference chain: io.keenhome.device.models.DatumList[\"values\"]->com.fasterxml.jackson.module.scala.deser.BuilderWrapper[0])" }

How can I handle these error messages (eg return non-garbage to API clients) without having to do a regex on strings that are likely to change?

threejeez
  • 2,314
  • 6
  • 30
  • 51
  • Your question is too general. Was it you who wrote code above? If yes you could rewrite it in more simple way. Otherwise I recommend asking more specific question. – expert May 14 '16 at 17:46
  • Updated to ask a more specific question – threejeez May 14 '16 at 18:46

1 Answers1

0

Ah I see. These crazy messages are generated by Jackson (not by Spray). So you have couple options:

1) Handle JSONObjectException in handleExceptions directive.

2) Or catch and transform JSONObjectException into your exception class before it reaches routing. Then I'd handle response with custom exception handler.

implicit def myExceptionHandler =
  ExceptionHandler {
    case ex: ExceptionWithUser => ctx => {
      val user = ex.user
      val cause = ex.getCause
      logger.error(s"${cause.getClass.getSimpleName} for ${ex.user.name} [${ex.user.id}]", cause)
      ctx.complete(StatusCodes.InternalServerError, ErrorResponse(ex.code, ex))
    }
    case ex: Throwable => ctx =>
      logger.warning("Request {} could not be handled normally", ctx.request)
      ctx.complete(StatusCodes.InternalServerError, ErrorResponse(StatusCodes.InternalServerError.intValue, ex))
  }

and for rejections I use custom rejection handler that converts default text akka-http rejections to json response that API clients expect (I can show it if you like).

expert
  • 29,290
  • 30
  • 110
  • 214