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)