@@ -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