Skip to content

Commit ac7a09a

Browse files
faraz152claude
andauthored
Fix NPE in Gson when serializing request headers on cold start (#1603)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 15a624a commit ac7a09a

3 files changed

Lines changed: 96 additions & 2 deletions

File tree

library/src/main/kotlin/com/chuckerteam/chucker/internal/data/entity/HttpTransaction.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import androidx.room.Ignore
1313
import androidx.room.PrimaryKey
1414
import com.chuckerteam.chucker.internal.support.FormatUtils
1515
import com.chuckerteam.chucker.internal.support.FormattedUrl
16+
import com.chuckerteam.chucker.internal.support.HttpHeaderSerializer
1617
import com.chuckerteam.chucker.internal.support.JsonConverter
1718
import com.chuckerteam.chucker.internal.support.SpanTextUtil
1819
import com.google.gson.reflect.TypeToken
@@ -161,7 +162,7 @@ internal class HttpTransaction(
161162
}
162163

163164
fun setRequestHeaders(headers: List<HttpHeader>) {
164-
requestHeaders = JsonConverter.instance.toJson(headers)
165+
requestHeaders = HttpHeaderSerializer.toJson(headers)
165166
}
166167

167168
fun setGraphQlOperationName(headers: Headers) {
@@ -194,7 +195,7 @@ internal class HttpTransaction(
194195
}
195196

196197
fun setResponseHeaders(headers: List<HttpHeader>) {
197-
responseHeaders = JsonConverter.instance.toJson(headers)
198+
responseHeaders = HttpHeaderSerializer.toJson(headers)
198199
}
199200

200201
fun getResponseHeadersString(withMarkup: Boolean): String =
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.chuckerteam.chucker.internal.support
2+
3+
import com.chuckerteam.chucker.internal.data.entity.HttpHeader
4+
import com.google.gson.JsonArray
5+
import com.google.gson.JsonObject
6+
7+
internal object HttpHeaderSerializer {
8+
fun toJson(headers: List<HttpHeader>): String =
9+
try {
10+
JsonConverter.instance.toJson(headers)
11+
} catch (
12+
@Suppress("TooGenericExceptionCaught", "SwallowedException") e: Exception,
13+
) {
14+
// Gson's default path resolves the List<HttpHeader> element type
15+
// reflectively, which occasionally throws NPE on cold start (issue
16+
// #1602). Build the JSON tree explicitly so callers still get a
17+
// valid string that fromJson<List<HttpHeader>> can round-trip.
18+
buildHeadersJson(headers)
19+
}
20+
21+
private fun buildHeadersJson(headers: List<HttpHeader>): String {
22+
val array = JsonArray(headers.size)
23+
headers.forEach { header ->
24+
val obj = JsonObject()
25+
obj.addProperty("name", header.name)
26+
obj.addProperty("value", header.value)
27+
array.add(obj)
28+
}
29+
return array.toString()
30+
}
31+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package com.chuckerteam.chucker.internal.support
2+
3+
import com.chuckerteam.chucker.internal.data.entity.HttpHeader
4+
import com.google.common.truth.Truth.assertThat
5+
import com.google.gson.reflect.TypeToken
6+
import org.junit.jupiter.api.Test
7+
8+
internal class HttpHeaderSerializerTest {
9+
private val sampleHeaders =
10+
listOf(
11+
HttpHeader("Content-Type", "application/json"),
12+
HttpHeader("X-Trace", "abc\"123\n"),
13+
)
14+
15+
private val headerListType =
16+
TypeToken.getParameterized(List::class.java, HttpHeader::class.java).type
17+
18+
@Test
19+
fun `empty list serializes to an empty JSON array`() {
20+
val json = HttpHeaderSerializer.toJson(emptyList())
21+
22+
val parsed: List<HttpHeader> = JsonConverter.instance.fromJson(json, headerListType)
23+
assertThat(parsed).isEmpty()
24+
}
25+
26+
@Test
27+
fun `round-trips headers through the default Gson path`() {
28+
val json = HttpHeaderSerializer.toJson(sampleHeaders)
29+
30+
val parsed: List<HttpHeader> = JsonConverter.instance.fromJson(json, headerListType)
31+
assertThat(parsed).containsExactlyElementsIn(sampleHeaders).inOrder()
32+
}
33+
34+
@Test
35+
fun `fallback path produces JSON that round-trips back to the same headers`() {
36+
val fallbackJson = invokePrivateFallback(sampleHeaders)
37+
38+
val parsed: List<HttpHeader> = JsonConverter.instance.fromJson(fallbackJson, headerListType)
39+
assertThat(parsed).containsExactlyElementsIn(sampleHeaders).inOrder()
40+
}
41+
42+
@Test
43+
fun `fallback escapes control characters and quotes into valid JSON`() {
44+
val awkward =
45+
listOf(
46+
HttpHeader("k\"ey", "line1\nline2\tend"),
47+
)
48+
49+
val fallbackJson = invokePrivateFallback(awkward)
50+
51+
val parsed: List<HttpHeader> = JsonConverter.instance.fromJson(fallbackJson, headerListType)
52+
assertThat(parsed).containsExactlyElementsIn(awkward).inOrder()
53+
}
54+
55+
private fun invokePrivateFallback(headers: List<HttpHeader>): String {
56+
val method =
57+
HttpHeaderSerializer::class.java.getDeclaredMethod("buildHeadersJson", List::class.java).apply {
58+
isAccessible = true
59+
}
60+
return method.invoke(HttpHeaderSerializer, headers) as String
61+
}
62+
}

0 commit comments

Comments
 (0)