Skip to content

Commit 43e259d

Browse files
committed
Improve JSON null handling and truncation detection
1 parent a71499a commit 43e259d

File tree

4 files changed

+169
-188
lines changed

4 files changed

+169
-188
lines changed

sentry-android-replay/src/main/java/io/sentry/android/replay/DefaultReplayBreadcrumbConverter.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,9 @@ public open class DefaultReplayBreadcrumbConverter() : ReplayBreadcrumbConverter
243243
request.size?.let { requestData["size"] = it }
244244
request.body?.let {
245245
requestData["body"] = it.body
246-
requestData["warnings"] = it.warnings?.map { w -> w.value }
246+
it.warnings?.let { warnings ->
247+
requestData["warnings"] = warnings.map { warning -> warning.value }
248+
}
247249
}
248250

249251
if (request.headers.isNotEmpty()) {
@@ -260,7 +262,9 @@ public open class DefaultReplayBreadcrumbConverter() : ReplayBreadcrumbConverter
260262
response.size?.let { responseData["size"] = it }
261263
response.body?.let {
262264
responseData["body"] = it.body
263-
responseData["warnings"] = it.warnings?.map { w -> w.value }
265+
it.warnings?.let { warnings ->
266+
responseData["warnings"] = warnings.map { warning -> warning.value }
267+
}
264268
}
265269

266270
if (response.headers.isNotEmpty()) {

sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpInterceptor.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,9 @@ public open class SentryOkHttpInterceptor(
318318
val maxBodySize = SentryReplayOptions.MAX_NETWORK_BODY_SIZE
319319

320320
// Peek at the body (doesn't consume it)
321-
val peekBody = peekBody(maxBodySize.toLong())
321+
// We +1 here in order to properly truncate within NetworkBodyParser.fromBytes
322+
// and be able to distinguish from an oversized request and a request matching maxBodySize
323+
val peekBody = peekBody(maxBodySize.toLong() + 1)
322324
val bodyBytes = peekBody.bytes()
323325

324326
val charset = contentType?.charset(Charsets.UTF_8)?.name() ?: "UTF-8"

sentry/src/main/java/io/sentry/util/network/NetworkBodyParser.java

Lines changed: 88 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ private NetworkBodyParser() {}
2727
* Creates a NetworkBody from raw bytes with content type information. This is useful for handling
2828
* binary or unknown content types.
2929
*
30-
* @param bytes The raw bytes of the body
30+
* @param bytes The raw bytes of the body, may be truncated
3131
* @param contentType Optional content type hint to help with parsing
3232
* @param charset Optional charset to use for text conversion (defaults to UTF-8)
3333
* @param maxSizeBytes Maximum size to process
@@ -51,12 +51,12 @@ private NetworkBodyParser() {}
5151
"[Binary data, " + bytes.length + " bytes, type: " + contentType + "]");
5252
}
5353

54-
final boolean isPartial = bytes.length >= maxSizeBytes;
55-
5654
// Convert to string and parse
5755
try {
5856
final String effectiveCharset = charset != null ? charset : "UTF-8";
59-
final String content = new String(bytes, effectiveCharset);
57+
final int size = Math.min(bytes.length, maxSizeBytes);
58+
final boolean isPartial = bytes.length > maxSizeBytes;
59+
final String content = new String(bytes, 0, size, effectiveCharset);
6060
return parse(content, contentType, isPartial, logger);
6161
} catch (UnsupportedEncodingException e) {
6262
logger.log(SentryLevel.WARNING, "Failed to decode bytes: " + e.getMessage());
@@ -86,7 +86,7 @@ private NetworkBodyParser() {}
8686
}
8787
}
8888

89-
// Default to string representation
89+
// Default to string representation, e.g. for XML
9090
final List<NetworkBody.NetworkBodyWarning> warnings =
9191
isPartial ? Collections.singletonList(NetworkBody.NetworkBodyWarning.TEXT_TRUNCATED) : null;
9292
return new NetworkBody(content, warnings);
@@ -96,11 +96,17 @@ private NetworkBodyParser() {}
9696
private static NetworkBody parseJson(
9797
final @NotNull String content, final boolean isPartial, final @Nullable ILogger logger) {
9898
try (final JsonReader reader = new JsonReader(new StringReader(content))) {
99-
final @Nullable Object data = readJsonSafely(reader);
100-
if (data != null) {
99+
final @NotNull SaferJsonParser.Result result = SaferJsonParser.parse(reader);
100+
final @Nullable Object data = result.data;
101+
if (data == null && !isPartial && !result.errored && !result.hitMaxDepth) {
102+
// In case the actual JSON body is simply null, simply return null
103+
return new NetworkBody(null);
104+
} else {
101105
final @Nullable List<NetworkBody.NetworkBodyWarning> warnings;
102-
if (isPartial) {
106+
if (isPartial || result.hitMaxDepth) {
103107
warnings = Collections.singletonList(NetworkBody.NetworkBodyWarning.JSON_TRUNCATED);
108+
} else if (result.errored) {
109+
warnings = Collections.singletonList(NetworkBody.NetworkBodyWarning.INVALID_JSON);
104110
} else {
105111
warnings = null;
106112
}
@@ -166,7 +172,7 @@ private static NetworkBody parseFormUrlEncoded(
166172

167173
/** Checks if the content type is binary and shouldn't be converted to string. */
168174
private static boolean isBinaryContentType(@NotNull final String contentType) {
169-
String lower = contentType.toLowerCase();
175+
final @NotNull String lower = contentType.toLowerCase(Locale.ROOT);
170176
return lower.contains("image/")
171177
|| lower.contains("video/")
172178
|| lower.contains("audio/")
@@ -176,61 +182,88 @@ private static boolean isBinaryContentType(@NotNull final String contentType) {
176182
|| lower.contains("application/gzip");
177183
}
178184

179-
@Nullable
180-
private static Object readJsonSafely(final @NotNull JsonReader reader) {
181-
try {
182-
switch (reader.peek()) {
183-
case BEGIN_OBJECT:
184-
final @NotNull Map<String, Object> map = new LinkedHashMap<>();
185-
reader.beginObject();
186-
try {
187-
while (reader.hasNext()) {
188-
try {
189-
String name = reader.nextName();
190-
map.put(name, readJsonSafely(reader)); // recursive call
191-
} catch (Exception e) {
192-
// ignored
185+
private static class SaferJsonParser {
186+
187+
private static final int MAX_DEPTH = 100;
188+
189+
private static class Result {
190+
private @Nullable Object data;
191+
private boolean hitMaxDepth;
192+
private boolean errored;
193+
}
194+
195+
final Result result = new Result();
196+
197+
private SaferJsonParser() {}
198+
199+
@NotNull
200+
public static SaferJsonParser.Result parse(final @NotNull JsonReader reader) {
201+
final SaferJsonParser parser = new SaferJsonParser();
202+
parser.result.data = parser.parse(reader, 0);
203+
return parser.result;
204+
}
205+
206+
@Nullable
207+
private Object parse(final @NotNull JsonReader reader, final int currentDepth) {
208+
if (result.errored) {
209+
return null;
210+
}
211+
if (currentDepth >= MAX_DEPTH) {
212+
result.hitMaxDepth = true;
213+
return null;
214+
}
215+
try {
216+
switch (reader.peek()) {
217+
case BEGIN_OBJECT:
218+
final @NotNull Map<String, Object> map = new LinkedHashMap<>();
219+
try {
220+
reader.beginObject();
221+
while (reader.hasNext() && !result.errored) {
222+
final String name = reader.nextName();
223+
map.put(name, parse(reader, currentDepth + 1));
193224
}
225+
reader.endObject();
226+
} catch (Exception e) {
227+
result.errored = true;
228+
return map;
194229
}
195-
reader.endObject();
196-
} catch (Exception e) {
197-
// ignored
198-
}
199-
return map;
200-
201-
case BEGIN_ARRAY:
202-
final List<Object> list = new ArrayList<>();
203-
reader.beginArray();
204-
try {
205-
while (reader.hasNext()) {
206-
list.add(readJsonSafely(reader)); // recursive call
207-
}
208-
reader.endArray();
209-
} catch (Exception e) {
210-
// ignored
211-
}
230+
return map;
212231

213-
return list;
232+
case BEGIN_ARRAY:
233+
final @NotNull List<Object> list = new ArrayList<>();
234+
try {
235+
reader.beginArray();
236+
while (reader.hasNext() && !result.errored) {
237+
list.add(parse(reader, currentDepth + 1));
238+
}
239+
reader.endArray();
240+
} catch (Exception e) {
241+
result.errored = true;
242+
return list;
243+
}
244+
return list;
214245

215-
case STRING:
216-
return reader.nextString();
246+
case STRING:
247+
return reader.nextString();
217248

218-
case NUMBER:
219-
// You can customize number handling (int, long, double) here
220-
return reader.nextDouble();
249+
case NUMBER:
250+
return reader.nextDouble();
221251

222-
case BOOLEAN:
223-
return reader.nextBoolean();
252+
case BOOLEAN:
253+
return reader.nextBoolean();
224254

225-
case NULL:
226-
reader.nextNull();
227-
return null;
255+
case NULL:
256+
reader.nextNull();
257+
return null;
228258

229-
default:
230-
throw new IllegalStateException("Unexpected JSON token: " + reader.peek());
259+
default:
260+
result.errored = true;
261+
return null;
262+
}
263+
} catch (final Exception ignored) {
264+
result.errored = true;
265+
return null;
231266
}
232-
} catch (Exception e) {
233-
return null;
234267
}
235268
}
236269
}

0 commit comments

Comments
 (0)