Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.runnect.runnect.data.network.calladapter

import com.google.gson.Gson
import com.runnect.runnect.data.dto.response.base.ErrorResponse
import com.runnect.runnect.domain.common.RunnectException
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
Expand All @@ -11,7 +13,7 @@ import okio.Timeout

class ResultCall<T>(private val call: Call<T>) : Call<Result<T>> {

private val gson = Gson()
private val json = Json { ignoreUnknownKeys = true }

override fun execute(): Response<Result<T>> {
throw UnsupportedOperationException("ResultCall doesn't support execute")
Expand Down Expand Up @@ -46,23 +48,17 @@ class ResultCall<T>(private val call: Call<T>) : Call<Result<T>> {
}

private fun parseErrorResponse(response: Response<*>): RunnectException {
val errorJson = response.errorBody()?.string()
val errorString = response.errorBody()?.string()

return runCatching {
val errorBody = gson.fromJson(errorJson, ErrorResponse::class.java)
val message = errorBody?.run {
message ?: error ?: ERROR_MSG_COMMON
}

RunnectException(
code = errorBody.status,
message = message
)
val jsonObject = json.parseToJsonElement(errorString.orEmpty()).jsonObject
val status = jsonObject["status"]?.jsonPrimitive?.int ?: response.code()
val message = jsonObject["message"]?.jsonPrimitive?.content
?: jsonObject["error"]?.jsonPrimitive?.content
?: ERROR_MSG_COMMON
RunnectException(code = status, message = message)
Comment on lines +54 to +59
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

에러 필드 추출을 조금 더 견고하게 처리해주세요.

Line 55-58에서 status/message 파싱 중 하나만 어긋나도 전체가 getOrElse로 빠져 실제 서버 메시지를 잃을 수 있습니다. "null"/빈 문자열도 그대로 메시지로 노출될 수 있습니다.

🔧 Proposed fix
-            val status = jsonObject["status"]?.jsonPrimitive?.int ?: response.code()
-            val message = jsonObject["message"]?.jsonPrimitive?.content
-                ?: jsonObject["error"]?.jsonPrimitive?.content
-                ?: ERROR_MSG_COMMON
+            val status = jsonObject["status"]?.jsonPrimitive?.content
+                ?.toIntOrNull()
+                ?: response.code()
+            val message = sequenceOf(
+                jsonObject["message"]?.jsonPrimitive?.content,
+                jsonObject["error"]?.jsonPrimitive?.content
+            ).firstOrNull { !it.isNullOrBlank() && it != "null" }
+                ?: ERROR_MSG_COMMON
             RunnectException(code = status, message = message)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
val jsonObject = json.parseToJsonElement(errorString.orEmpty()).jsonObject
val status = jsonObject["status"]?.jsonPrimitive?.int ?: response.code()
val message = jsonObject["message"]?.jsonPrimitive?.content
?: jsonObject["error"]?.jsonPrimitive?.content
?: ERROR_MSG_COMMON
RunnectException(code = status, message = message)
val status = jsonObject["status"]?.jsonPrimitive?.content
?.toIntOrNull()
?: response.code()
val message = sequenceOf(
jsonObject["message"]?.jsonPrimitive?.content,
jsonObject["error"]?.jsonPrimitive?.content
).firstOrNull { !it.isNullOrBlank() && it != "null" }
?: ERROR_MSG_COMMON
RunnectException(code = status, message = message)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/runnect/runnect/data/network/calladapter/ResultCall.kt`
around lines 54 - 59, The current parsing of errorString via
json.parseToJsonElement and subsequent extraction of status/message is brittle:
any parsing error or a single malformed field causes the whole getOrElse to
swallow server details; also values like "null" or empty strings can be returned
as real messages. Update the block around
json.parseToJsonElement(errorString.orEmpty())/jsonObject and the status/message
extraction so you first try-catch JSON parsing, verify the element is a
JsonObject, then individually extract status by checking the presence of
"status" and using jsonPrimitive.intOrNull or toIntOrNull with a fallback to
response.code(), and extract message by checking "message" then "error",
ensuring the jsonPrimitive is a non-empty, non-"null" string before using it; if
parsing fails or values are invalid, fall back to ERROR_MSG_COMMON, and
construct RunnectException(code = status, message = message) with those robustly
derived values.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 코드 유지

1번 코멘트와 동일한 사유입니다. 기존 Gson 코드의 에러 파싱 동작을 그대로 유지하는 것이 이 PR의 목적이며, 정규화 로직 추가는 범위 밖입니다.

status 파싱에 대해: jsonPrimitive.int는 JSON 값이 숫자가 아닐 때 예외를 던지고, runCatching이 이를 잡아서 response.code()로 폴백합니다. 기존 Gson 코드에서 errorBody.status가 실패할 때 getOrElse로 빠지는 것과 동일한 동작입니다.

}.getOrElse {
RunnectException(
code = response.code(),
message = ERROR_MSG_COMMON
)
RunnectException(code = response.code(), message = ERROR_MSG_COMMON)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package com.runnect.runnect.data.network.calladapter.flow

import com.google.gson.Gson
import com.runnect.runnect.data.dto.response.base.ErrorResponse
import com.runnect.runnect.domain.common.RunnectException
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import retrofit2.Call
import retrofit2.CallAdapter
import retrofit2.Callback
Expand All @@ -18,7 +19,7 @@ class FlowCallAdapter<T>(
private val responseType: Type
) : CallAdapter<T, Flow<Result<T>>> {

private val gson = Gson()
private val json = Json { ignoreUnknownKeys = true }
override fun responseType() = responseType

// Retrofit의 Call을 Result<>로 변환
Expand Down Expand Up @@ -58,15 +59,18 @@ class FlowCallAdapter<T>(
} ?: Result.failure(nullBodyException)
}

// Response에서 오류를 파싱하여 RunnectException 객체를 생성
private fun parseErrorResponse(response: Response<*>): RunnectException {
val errorBodyString = response.errorBody()?.string()
val errorResponse = errorBodyString?.let {
gson.fromJson(it, ErrorResponse::class.java)
}

val errorMessage = errorResponse?.message ?: errorResponse?.error ?: ERROR_MSG_COMMON
return RunnectException(response.code(), errorMessage)
return runCatching {
val jsonObject = json.parseToJsonElement(errorBodyString.orEmpty()).jsonObject
val message = jsonObject["message"]?.jsonPrimitive?.content
?: jsonObject["error"]?.jsonPrimitive?.content
?: ERROR_MSG_COMMON
Comment thread
coderabbitai[bot] marked this conversation as resolved.
RunnectException(response.code(), message)
}.getOrElse {
RunnectException(response.code(), ERROR_MSG_COMMON)
}
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
package com.runnect.runnect.data.network.interceptor

import com.google.gson.Gson
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonParseException
import com.google.gson.JsonSyntaxException
import com.runnect.runnect.data.dto.response.base.BaseResponse
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonObject
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Response
Expand All @@ -18,14 +15,14 @@ import timber.log.Timber
*/
class ResponseInterceptor : Interceptor {

private val gson = Gson()
private val json = Json { ignoreUnknownKeys = true }

override fun intercept(chain: Interceptor.Chain): Response {
val originalResponse = chain.proceed(chain.request())
if (!originalResponse.isSuccessful) return originalResponse

val bodyString = originalResponse.peekBody(Long.MAX_VALUE).string()
val newResponseBodyString = jsonToBaseResponse(bodyString)?.let {
val newResponseBodyString = extractData(bodyString)?.let {
it.toResponseBody("application/json".toMediaTypeOrNull())
} ?: return originalResponse

Expand All @@ -42,26 +39,18 @@ class ResponseInterceptor : Interceptor {
}
}

private fun jsonToBaseResponse(body: String): String? {
private fun extractData(body: String): String? {
return try {
val jsonElement: JsonElement = gson.fromJson(body, JsonElement::class.java)
if (!isBaseResponse(jsonElement.asJsonObject)) {
return null
}

val baseResponse = gson.fromJson(body, BaseResponse::class.java)
gson.toJson(baseResponse.data)
} catch (e: JsonSyntaxException) {
null // JSON 구문 분석 오류 발생 시 원래 형식을 반환
} catch (e: JsonParseException) {
null // JSON 파싱 오류 발생 시 원래 형식을 반환
val jsonObject = json.parseToJsonElement(body).jsonObject
if (!isBaseResponse(jsonObject)) return null
jsonObject["data"].toString()
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} catch (e: Exception) {
null // 기타 예외 발생 시 원래 형식을 반환
null
}
}

private fun isBaseResponse(jsonObject: JsonObject): Boolean {
val requiredFields = listOf("status", "success", "message", "data")
return requiredFields.all { jsonObject.has(it) }
return requiredFields.all { it in jsonObject }
}
}
Loading