Skip to content

Commit cc962a6

Browse files
adinauerclaude
andcommitted
fix(sentry): Track consumed values during JSON recovery
Treat recovery state as consumed when the current value has already been read, including skipValue()-driven failures. This prevents the fallback recovery step from skipping the next valid sibling after a deserializer consumes a value and then throws. Add regressions for direct list recovery and nested map-of-list recovery so consumed-value failures keep later valid entries. Refs GH-5278 Co-Authored-By: Claude <noreply@anthropic.com>
1 parent d6f8299 commit cc962a6

2 files changed

Lines changed: 72 additions & 33 deletions

File tree

sentry/src/main/java/io/sentry/JsonObjectReader.java

Lines changed: 34 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -30,60 +30,61 @@ public JsonObjectReader(Reader in) {
3030
@Override
3131
public @Nullable String nextStringOrNull() throws IOException {
3232
if (jsonReader.peek() == JsonToken.NULL) {
33-
jsonReader.nextNull();
33+
nextNull();
3434
return null;
3535
}
36-
return jsonReader.nextString();
36+
return nextString();
3737
}
3838

3939
@Override
4040
public @Nullable Double nextDoubleOrNull() throws IOException {
4141
if (jsonReader.peek() == JsonToken.NULL) {
42-
jsonReader.nextNull();
42+
nextNull();
4343
return null;
4444
}
45-
return jsonReader.nextDouble();
45+
return nextDouble();
4646
}
4747

4848
@Override
4949
public @Nullable Float nextFloatOrNull() throws IOException {
5050
if (jsonReader.peek() == JsonToken.NULL) {
51-
jsonReader.nextNull();
51+
nextNull();
5252
return null;
5353
}
5454
return nextFloat();
5555
}
5656

5757
@Override
5858
public float nextFloat() throws IOException {
59+
markValueConsumed();
5960
return (float) jsonReader.nextDouble();
6061
}
6162

6263
@Override
6364
public @Nullable Long nextLongOrNull() throws IOException {
6465
if (jsonReader.peek() == JsonToken.NULL) {
65-
jsonReader.nextNull();
66+
nextNull();
6667
return null;
6768
}
68-
return jsonReader.nextLong();
69+
return nextLong();
6970
}
7071

7172
@Override
7273
public @Nullable Integer nextIntegerOrNull() throws IOException {
7374
if (jsonReader.peek() == JsonToken.NULL) {
74-
jsonReader.nextNull();
75+
nextNull();
7576
return null;
7677
}
77-
return jsonReader.nextInt();
78+
return nextInt();
7879
}
7980

8081
@Override
8182
public @Nullable Boolean nextBooleanOrNull() throws IOException {
8283
if (jsonReader.peek() == JsonToken.NULL) {
83-
jsonReader.nextNull();
84+
nextNull();
8485
return null;
8586
}
86-
return jsonReader.nextBoolean();
87+
return nextBoolean();
8788
}
8889

8990
@Override
@@ -110,7 +111,7 @@ public void nextUnknown(ILogger logger, Map<String, Object> unknown, String name
110111
public <T> @Nullable List<T> nextListOrNull(
111112
@NotNull ILogger logger, @NotNull JsonDeserializer<T> deserializer) throws IOException {
112113
if (jsonReader.peek() == JsonToken.NULL) {
113-
jsonReader.nextNull();
114+
nextNull();
114115
return null;
115116
}
116117
beginArray();
@@ -142,7 +143,7 @@ public void nextUnknown(ILogger logger, Map<String, Object> unknown, String name
142143
public <T> @Nullable Map<String, T> nextMapOrNull(
143144
@NotNull ILogger logger, @NotNull JsonDeserializer<T> deserializer) throws IOException {
144145
if (jsonReader.peek() == JsonToken.NULL) {
145-
jsonReader.nextNull();
146+
nextNull();
146147
return null;
147148
}
148149
beginObject();
@@ -215,7 +216,7 @@ public void nextUnknown(ILogger logger, Map<String, Object> unknown, String name
215216
public <T> @Nullable T nextOrNull(
216217
@NotNull ILogger logger, @NotNull JsonDeserializer<T> deserializer) throws Exception {
217218
if (jsonReader.peek() == JsonToken.NULL) {
218-
jsonReader.nextNull();
219+
nextNull();
219220
return null;
220221
}
221222
return deserializer.deserialize(this, logger);
@@ -224,20 +225,20 @@ public void nextUnknown(ILogger logger, Map<String, Object> unknown, String name
224225
@Override
225226
public @Nullable Date nextDateOrNull(ILogger logger) throws IOException {
226227
if (jsonReader.peek() == JsonToken.NULL) {
227-
jsonReader.nextNull();
228+
nextNull();
228229
return null;
229230
}
230-
return ObjectReader.dateOrNull(jsonReader.nextString(), logger);
231+
return ObjectReader.dateOrNull(nextString(), logger);
231232
}
232233

233234
@Override
234235
public @Nullable TimeZone nextTimeZoneOrNull(ILogger logger) throws IOException {
235236
if (jsonReader.peek() == JsonToken.NULL) {
236-
jsonReader.nextNull();
237+
nextNull();
237238
return null;
238239
}
239240
try {
240-
return TimeZone.getTimeZone(jsonReader.nextString());
241+
return TimeZone.getTimeZone(nextString());
241242
} catch (Exception e) {
242243
logger.log(SentryLevel.ERROR, "Error when deserializing TimeZone", e);
243244
}
@@ -268,8 +269,8 @@ public void nextUnknown(ILogger logger, Map<String, Object> unknown, String name
268269
@Override
269270
public void beginObject() throws IOException {
270271
jsonReader.beginObject();
272+
markValueConsumed();
271273
depth++;
272-
markContainerEntered();
273274
}
274275

275276
@Override
@@ -281,8 +282,8 @@ public void endObject() throws IOException {
281282
@Override
282283
public void beginArray() throws IOException {
283284
jsonReader.beginArray();
285+
markValueConsumed();
284286
depth++;
285-
markContainerEntered();
286287
}
287288

288289
@Override
@@ -298,31 +299,37 @@ public boolean hasNext() throws IOException {
298299

299300
@Override
300301
public int nextInt() throws IOException {
302+
markValueConsumed();
301303
return jsonReader.nextInt();
302304
}
303305

304306
@Override
305307
public long nextLong() throws IOException {
308+
markValueConsumed();
306309
return jsonReader.nextLong();
307310
}
308311

309312
@Override
310313
public String nextString() throws IOException {
314+
markValueConsumed();
311315
return jsonReader.nextString();
312316
}
313317

314318
@Override
315319
public boolean nextBoolean() throws IOException {
320+
markValueConsumed();
316321
return jsonReader.nextBoolean();
317322
}
318323

319324
@Override
320325
public double nextDouble() throws IOException {
326+
markValueConsumed();
321327
return jsonReader.nextDouble();
322328
}
323329

324330
@Override
325331
public void nextNull() throws IOException {
332+
markValueConsumed();
326333
jsonReader.nextNull();
327334
}
328335

@@ -333,6 +340,7 @@ public void setLenient(boolean lenient) {
333340

334341
@Override
335342
public void skipValue() throws IOException {
343+
markValueConsumed();
336344
jsonReader.skipValue();
337345
}
338346

@@ -369,13 +377,10 @@ private void endRecovery(final @Nullable RecoveryState recoveryState) {
369377
}
370378
}
371379

372-
private void markContainerEntered() {
373-
for (RecoveryState recoveryState : recoveryStates) {
374-
if (!recoveryState.enteredContainer
375-
&& isContainerStartToken(recoveryState.startToken)
376-
&& depth > recoveryState.startDepth) {
377-
recoveryState.enteredContainer = true;
378-
}
380+
private void markValueConsumed() {
381+
final @Nullable RecoveryState recoveryState = recoveryStates.peekLast();
382+
if (recoveryState != null) {
383+
recoveryState.valueConsumed = true;
379384
}
380385
}
381386

@@ -391,15 +396,11 @@ private void recoverValue(final @NotNull RecoveryState recoveryState) throws IOE
391396
}
392397
}
393398

394-
if (!recoveryState.enteredContainer && peek() == recoveryState.startToken) {
399+
if (!recoveryState.valueConsumed && peek() == recoveryState.startToken) {
395400
skipValue();
396401
}
397402
}
398403

399-
private static boolean isContainerStartToken(final @NotNull JsonToken token) {
400-
return token == JsonToken.BEGIN_OBJECT || token == JsonToken.BEGIN_ARRAY;
401-
}
402-
403404
@Override
404405
public void close() throws IOException {
405406
jsonReader.close();
@@ -408,7 +409,7 @@ public void close() throws IOException {
408409
private static final class RecoveryState {
409410
private final int startDepth;
410411
private final @NotNull JsonToken startToken;
411-
private boolean enteredContainer;
412+
private boolean valueConsumed;
412413

413414
private RecoveryState(final int startDepth, final @NotNull JsonToken startToken) {
414415
this.startDepth = startDepth;

sentry/src/test/java/io/sentry/JsonObjectReaderTest.kt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,25 @@ class JsonObjectReaderTest {
284284
assertEquals(listOf("two"), actual)
285285
}
286286

287+
@Test(timeout = 1000L)
288+
fun `nextListOrNull keeps elements after skipValue consumes a failing element`() {
289+
var callCount = 0
290+
val deserializer =
291+
JsonDeserializer<String> { reader, logger ->
292+
if (callCount++ == 0) {
293+
reader.skipValue()
294+
throw IllegalStateException("intentional")
295+
}
296+
throwingValueDeserializer.deserialize(reader, logger)
297+
}
298+
299+
val actual =
300+
getValuesReader("[{\"value\": \"ignored\"}, {\"value\": \"two\"}]")
301+
.nextListOrNull(fixture.logger, deserializer)
302+
303+
assertEquals(listOf("two"), actual)
304+
}
305+
287306
@Test(timeout = 1000L)
288307
fun `nextMapOrNull skips a failing value`() {
289308
val actual =
@@ -338,6 +357,25 @@ class JsonObjectReaderTest {
338357
assertEquals(mapOf("good" to listOf("two")), actual)
339358
}
340359

360+
@Test(timeout = 1000L)
361+
fun `nextMapOfListOrNull keeps nested values after skipValue consumes a failing element`() {
362+
var callCount = 0
363+
val deserializer =
364+
JsonDeserializer<String> { reader, logger ->
365+
if (callCount++ == 0) {
366+
reader.skipValue()
367+
throw IllegalStateException("intentional")
368+
}
369+
throwingValueDeserializer.deserialize(reader, logger)
370+
}
371+
372+
val actual =
373+
getValuesReader("{\"good\": [{\"value\": \"ignored\"}, {\"value\": \"two\"}]}")
374+
.nextMapOfListOrNull(fixture.logger, deserializer)
375+
376+
assertEquals(mapOf("good" to listOf("two")), actual)
377+
}
378+
341379
@Test(timeout = 1000L)
342380
fun `nextListOrNull logs and aborts when recovery fails`() {
343381
assertFailsWith<Exception> {

0 commit comments

Comments
 (0)