libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

ApiError.kt (4820B)


      1 /*
      2  * This file is part of LibEuFin.
      3  * Copyright (C) 2024-2025 Taler Systems S.A.
      4 
      5  * LibEuFin is free software; you can redistribute it and/or modify
      6  * it under the terms of the GNU Affero General Public License as
      7  * published by the Free Software Foundation; either version 3, or
      8  * (at your option) any later version.
      9 
     10  * LibEuFin is distributed in the hope that it will be useful, but
     11  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
     12  * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
     13  * Public License for more details.
     14 
     15  * You should have received a copy of the GNU Affero General Public
     16  * License along with LibEuFin; see the file COPYING.  If not, see
     17  * <http://www.gnu.org/licenses/>
     18  */
     19 package tech.libeufin.common
     20 
     21 import io.ktor.http.*
     22 import io.ktor.server.application.*
     23 import io.ktor.server.response.*
     24 import io.ktor.util.*
     25 import kotlinx.serialization.Serializable
     26 
     27 /**
     28  * Convenience type to throw errors along the API activity
     29  * and that is meant to be caught by Ktor and responded to the
     30  * client.
     31  */
     32 class ApiException(
     33     // Status code that Ktor will set for the response.
     34     val httpStatus: HttpStatusCode,
     35     // Error detail object, after Taler API.
     36     val talerError: TalerError
     37 ) : Exception(talerError.hint)
     38 
     39 /**
     40  * Error object to respond to the client.  The
     41  * 'code' field takes values from the GANA gnu-taler-error-code
     42  * specification.  'hint' is a human-readable description
     43  * of the error.
     44  */
     45 @Serializable
     46 data class TalerError(
     47     @kotlinx.serialization.Transient val err: TalerErrorCode = TalerErrorCode.END,
     48     val code: Int,
     49     val hint: String? = null,
     50     val detail: String? = null
     51 )
     52 
     53 private val LOG_MSG = AttributeKey<String>("log_msg")
     54 
     55 fun ApplicationCall.logMsg(): String? = attributes.getOrNull(LOG_MSG)
     56 
     57 suspend fun ApplicationCall.err(
     58     status: HttpStatusCode,
     59     hint: String?,
     60     error: TalerErrorCode,
     61     cause: Exception?
     62 ) {
     63     err(
     64         ApiException(
     65             httpStatus = status, talerError = TalerError(
     66                 code = error.code, err = error, hint = hint
     67             )
     68         ),
     69         cause
     70     )
     71 }
     72 
     73 suspend fun ApplicationCall.err(
     74     err: ApiException,
     75     cause: Exception?
     76 ) {
     77     val fmt = buildString {
     78         append(err.talerError.err.name)
     79         append(" ")
     80         append(err.talerError.hint)
     81         val msg = cause?.message
     82         if (msg != null) {
     83             append("- ")
     84             append(msg)
     85         }
     86     }
     87     attributes.put(LOG_MSG, fmt)
     88     respond(
     89         status = err.httpStatus,
     90         message = err.talerError
     91     )
     92 }
     93 
     94 
     95 fun apiError(
     96     status: HttpStatusCode,
     97     hint: String?,
     98     error: TalerErrorCode,
     99     detail: String? = null
    100 ): ApiException = ApiException(
    101     httpStatus = status, talerError = TalerError(
    102         code = error.code, err = error, hint = hint, detail = detail
    103     )
    104 )
    105 
    106 /* ----- HTTP error ----- */
    107 
    108 fun forbidden(
    109     hint: String,
    110     error: TalerErrorCode = TalerErrorCode.GENERIC_FORBIDDEN
    111 ): ApiException = apiError(HttpStatusCode.Forbidden, hint, error)
    112 
    113 fun unauthorized(
    114     hint: String,
    115     error: TalerErrorCode = TalerErrorCode.GENERIC_UNAUTHORIZED
    116 ): ApiException = apiError(HttpStatusCode.Unauthorized, hint, error)
    117 
    118 fun internalServerError(hint: String?): ApiException 
    119     = apiError(HttpStatusCode.InternalServerError, hint, TalerErrorCode.GENERIC_INTERNAL_INVARIANT_FAILURE)
    120 
    121 fun paramsMalformed(hint: String): ApiException
    122     = badRequest(hint, TalerErrorCode.GENERIC_PARAMETER_MALFORMED)
    123 
    124 fun notFound(
    125     hint: String,
    126     error: TalerErrorCode
    127 ): ApiException = apiError(HttpStatusCode.NotFound, hint, error)
    128 
    129 fun conflict(
    130     hint: String, error: TalerErrorCode
    131 ): ApiException = apiError(HttpStatusCode.Conflict, hint, error)
    132 
    133 fun tooManyRequests(
    134     hint: String, error: TalerErrorCode
    135 ): ApiException = apiError(HttpStatusCode.TooManyRequests, hint, error)
    136 
    137 fun badRequest(
    138     hint: String? = null, 
    139     error: TalerErrorCode = TalerErrorCode.GENERIC_JSON_INVALID,
    140     detail: String? = null
    141 ): ApiException = apiError(HttpStatusCode.BadRequest, hint, error, detail)
    142 
    143 fun unsupportedMediaType(
    144     hint: String, 
    145     error: TalerErrorCode = TalerErrorCode.END,
    146 ): ApiException = apiError(HttpStatusCode.UnsupportedMediaType, hint, error)
    147 
    148 fun notImplemented(
    149     hint: String = "API not implemented",
    150     error: TalerErrorCode = TalerErrorCode.END,
    151 ): ApiException = apiError(HttpStatusCode.NotImplemented, hint, error)
    152 
    153 fun bodyOverflow(
    154     hint: String, 
    155     error: TalerErrorCode = TalerErrorCode.GENERIC_UPLOAD_EXCEEDS_LIMIT,
    156 ): ApiException = apiError(HttpStatusCode.PayloadTooLarge, hint, error)
    157 
    158 fun badGateway(
    159     hint: String, 
    160     error: TalerErrorCode = TalerErrorCode.GENERIC_UPLOAD_EXCEEDS_LIMIT,
    161 ): ApiException = apiError(HttpStatusCode.PayloadTooLarge, hint, error)