From 4f1c279154ef6929a36107f0dc3375df0eac34f1 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Tue, 29 Jul 2025 01:51:49 +0300 Subject: [PATCH 1/2] impl: relax json syntax rules for deserialization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For some clients, workspace polling fails due to the following error: ``` com.squareup.moshi.JsonEncodingException: Use JsonReader.setLenient(true) to accept malformed JSON at path $ ``` I haven’t been able to reproduce this issue, even using the same version deployed at the client (2.20.2). This change is an attempt to relax the JSON parsing rules by enabling lenient mode, in the hope that it will resolve the issue on the client side. --- CHANGELOG.md | 4 ++++ src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cab6bf..3bc6c1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Changed + +- the http client has relaxed syntax rules when deserializing JSON responses + ## 0.6.0 - 2025-07-25 ### Changed diff --git a/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt b/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt index 1a0f18e..fecbed5 100644 --- a/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt +++ b/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt @@ -74,10 +74,10 @@ open class CoderRestClient( var builder = OkHttpClient.Builder() if (context.proxySettings.getProxy() != null) { - context.logger.debug("proxy: ${context.proxySettings.getProxy()}") + context.logger.info("proxy: ${context.proxySettings.getProxy()}") builder.proxy(context.proxySettings.getProxy()) } else if (context.proxySettings.getProxySelector() != null) { - context.logger.debug("proxy selector: ${context.proxySettings.getProxySelector()}") + context.logger.info("proxy selector: ${context.proxySettings.getProxySelector()}") builder.proxySelector(context.proxySettings.getProxySelector()!!) } @@ -133,7 +133,7 @@ open class CoderRestClient( retroRestClient = Retrofit.Builder().baseUrl(url.toString()).client(httpClient) - .addConverterFactory(MoshiConverterFactory.create(moshi)) + .addConverterFactory(MoshiConverterFactory.create(moshi).asLenient()) .build().create(CoderV2RestFacade::class.java) } From 7d4a8ad00e887367e6613c03fa7a3e745d1eb879 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Fri, 1 Aug 2025 01:18:55 +0300 Subject: [PATCH 2/2] 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. --- .../com/coder/toolbox/sdk/CoderRestClient.kt | 8 ++- .../sdk/convertors/LoggingConverterFactory.kt | 53 +++++++++++++++++++ .../sdk/convertors/LoggingMoshiConverter.kt | 34 ++++++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/com/coder/toolbox/sdk/convertors/LoggingConverterFactory.kt create mode 100644 src/main/kotlin/com/coder/toolbox/sdk/convertors/LoggingMoshiConverter.kt diff --git a/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt b/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt index fecbed5..fca46a3 100644 --- a/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt +++ b/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt @@ -3,6 +3,7 @@ package com.coder.toolbox.sdk import com.coder.toolbox.CoderToolboxContext import com.coder.toolbox.sdk.convertors.ArchConverter import com.coder.toolbox.sdk.convertors.InstantConverter +import com.coder.toolbox.sdk.convertors.LoggingConverterFactory import com.coder.toolbox.sdk.convertors.OSConverter import com.coder.toolbox.sdk.convertors.UUIDConverter import com.coder.toolbox.sdk.ex.APIResponseException @@ -133,7 +134,12 @@ open class CoderRestClient( retroRestClient = Retrofit.Builder().baseUrl(url.toString()).client(httpClient) - .addConverterFactory(MoshiConverterFactory.create(moshi).asLenient()) + .addConverterFactory( + LoggingConverterFactory.wrap( + context, + MoshiConverterFactory.create(moshi).asLenient() + ) + ) .build().create(CoderV2RestFacade::class.java) } diff --git a/src/main/kotlin/com/coder/toolbox/sdk/convertors/LoggingConverterFactory.kt b/src/main/kotlin/com/coder/toolbox/sdk/convertors/LoggingConverterFactory.kt new file mode 100644 index 0000000..839d753 --- /dev/null +++ b/src/main/kotlin/com/coder/toolbox/sdk/convertors/LoggingConverterFactory.kt @@ -0,0 +1,53 @@ +package com.coder.toolbox.sdk.convertors + +import com.coder.toolbox.CoderToolboxContext +import okhttp3.RequestBody +import okhttp3.ResponseBody +import retrofit2.Converter +import retrofit2.Retrofit +import java.lang.reflect.Type + +class LoggingConverterFactory private constructor( + private val context: CoderToolboxContext, + private val delegate: Converter.Factory, +) : Converter.Factory() { + + override fun responseBodyConverter( + type: Type, + annotations: Array, + retrofit: Retrofit + ): Converter? { + // Get the delegate converter + val delegateConverter = delegate.responseBodyConverter(type, annotations, retrofit) + ?: return null + + @Suppress("UNCHECKED_CAST") + return LoggingMoshiConverter(context, delegateConverter as Converter) + } + + override fun requestBodyConverter( + type: Type, + parameterAnnotations: Array, + methodAnnotations: Array, + retrofit: Retrofit + ): Converter<*, RequestBody>? { + return delegate.requestBodyConverter(type, parameterAnnotations, methodAnnotations, retrofit) + } + + override fun stringConverter( + type: Type, + annotations: Array, + retrofit: Retrofit + ): Converter<*, String>? { + return delegate.stringConverter(type, annotations, retrofit) + } + + companion object { + fun wrap( + context: CoderToolboxContext, + delegate: Converter.Factory, + ): LoggingConverterFactory { + return LoggingConverterFactory(context, delegate) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/coder/toolbox/sdk/convertors/LoggingMoshiConverter.kt b/src/main/kotlin/com/coder/toolbox/sdk/convertors/LoggingMoshiConverter.kt new file mode 100644 index 0000000..9cc548a --- /dev/null +++ b/src/main/kotlin/com/coder/toolbox/sdk/convertors/LoggingMoshiConverter.kt @@ -0,0 +1,34 @@ +package com.coder.toolbox.sdk.convertors + +import com.coder.toolbox.CoderToolboxContext +import okhttp3.ResponseBody +import okhttp3.ResponseBody.Companion.toResponseBody +import retrofit2.Converter + +class LoggingMoshiConverter( + private val context: CoderToolboxContext, + private val delegate: Converter +) : Converter { + + override fun convert(value: ResponseBody): Any? { + val bodyString = value.string() + + return try { + // Parse with Moshi + delegate.convert(bodyString.toResponseBody(value.contentType())) + } catch (e: Exception) { + // Log the raw content that failed to parse + context.logger.error( + """ + |Moshi parsing failed: + |Content-Type: ${value.contentType()} + |Content: $bodyString + |Error: ${e.message} + """.trimMargin() + ) + + // Re-throw so the onFailure callback still gets called + throw e + } + } +} \ No newline at end of file