Skip to content

Commit 7d4a8ad

Browse files
committed
impl: add error logging for malformed JSON responses in Coder REST API calls
Wraps Moshi converter to log raw response body when JSON parsing fails. It helps debug malformed JSON during workspace polling by logging full response content, content type, and error details when a exception during marshalling occurs. A couple of approaches were tried, unfortunately by the time the exception is raised the response body has already been consumed by Moshi's converter, so you can't read it again. The interceptors are also not really a viable option, they are called before the converters which means: - we don't know if the response body is in the correct form or not. This is problematic because it means for every rest call we have to read the body twice (interceptor and by moshi converter) - we also have to save the intercepted body and store it until we have an exception from moshi, in which case it will have to be logged. This approach only logs on conversion failures, and the only performance impact on successful responses is the fact that we convert the response body byte stream to a string representation that can later be printed, and then again back to a byte stream by the moshi converter.
1 parent 50da220 commit 7d4a8ad

File tree

3 files changed

+94
-1
lines changed

3 files changed

+94
-1
lines changed

src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.coder.toolbox.sdk
33
import com.coder.toolbox.CoderToolboxContext
44
import com.coder.toolbox.sdk.convertors.ArchConverter
55
import com.coder.toolbox.sdk.convertors.InstantConverter
6+
import com.coder.toolbox.sdk.convertors.LoggingConverterFactory
67
import com.coder.toolbox.sdk.convertors.OSConverter
78
import com.coder.toolbox.sdk.convertors.UUIDConverter
89
import com.coder.toolbox.sdk.ex.APIResponseException
@@ -133,7 +134,12 @@ open class CoderRestClient(
133134

134135
retroRestClient =
135136
Retrofit.Builder().baseUrl(url.toString()).client(httpClient)
136-
.addConverterFactory(MoshiConverterFactory.create(moshi).asLenient())
137+
.addConverterFactory(
138+
LoggingConverterFactory.wrap(
139+
context,
140+
MoshiConverterFactory.create(moshi).asLenient()
141+
)
142+
)
137143
.build().create(CoderV2RestFacade::class.java)
138144
}
139145

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.coder.toolbox.sdk.convertors
2+
3+
import com.coder.toolbox.CoderToolboxContext
4+
import okhttp3.RequestBody
5+
import okhttp3.ResponseBody
6+
import retrofit2.Converter
7+
import retrofit2.Retrofit
8+
import java.lang.reflect.Type
9+
10+
class LoggingConverterFactory private constructor(
11+
private val context: CoderToolboxContext,
12+
private val delegate: Converter.Factory,
13+
) : Converter.Factory() {
14+
15+
override fun responseBodyConverter(
16+
type: Type,
17+
annotations: Array<Annotation>,
18+
retrofit: Retrofit
19+
): Converter<ResponseBody, *>? {
20+
// Get the delegate converter
21+
val delegateConverter = delegate.responseBodyConverter(type, annotations, retrofit)
22+
?: return null
23+
24+
@Suppress("UNCHECKED_CAST")
25+
return LoggingMoshiConverter(context, delegateConverter as Converter<ResponseBody, Any?>)
26+
}
27+
28+
override fun requestBodyConverter(
29+
type: Type,
30+
parameterAnnotations: Array<Annotation>,
31+
methodAnnotations: Array<Annotation>,
32+
retrofit: Retrofit
33+
): Converter<*, RequestBody>? {
34+
return delegate.requestBodyConverter(type, parameterAnnotations, methodAnnotations, retrofit)
35+
}
36+
37+
override fun stringConverter(
38+
type: Type,
39+
annotations: Array<Annotation>,
40+
retrofit: Retrofit
41+
): Converter<*, String>? {
42+
return delegate.stringConverter(type, annotations, retrofit)
43+
}
44+
45+
companion object {
46+
fun wrap(
47+
context: CoderToolboxContext,
48+
delegate: Converter.Factory,
49+
): LoggingConverterFactory {
50+
return LoggingConverterFactory(context, delegate)
51+
}
52+
}
53+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.coder.toolbox.sdk.convertors
2+
3+
import com.coder.toolbox.CoderToolboxContext
4+
import okhttp3.ResponseBody
5+
import okhttp3.ResponseBody.Companion.toResponseBody
6+
import retrofit2.Converter
7+
8+
class LoggingMoshiConverter(
9+
private val context: CoderToolboxContext,
10+
private val delegate: Converter<ResponseBody, Any?>
11+
) : Converter<ResponseBody, Any> {
12+
13+
override fun convert(value: ResponseBody): Any? {
14+
val bodyString = value.string()
15+
16+
return try {
17+
// Parse with Moshi
18+
delegate.convert(bodyString.toResponseBody(value.contentType()))
19+
} catch (e: Exception) {
20+
// Log the raw content that failed to parse
21+
context.logger.error(
22+
"""
23+
|Moshi parsing failed:
24+
|Content-Type: ${value.contentType()}
25+
|Content: $bodyString
26+
|Error: ${e.message}
27+
""".trimMargin()
28+
)
29+
30+
// Re-throw so the onFailure callback still gets called
31+
throw e
32+
}
33+
}
34+
}

0 commit comments

Comments
 (0)