Describe the bug
When making a batch request using the Microsoft Graph Java SDK, the SDK fails to parse the batch response if one of the individual responses contains a plain text body instead of valid JSON.
com.google.gson.JsonSyntaxException: com.google.gson.stream.MalformedJsonException: Unterminated object at line 1 column 149 path $.responses[1].body
See https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json
at com.google.gson.internal.Streams.parse(Streams.java:58) ~[gson-2.11.0.jar!/:na]
at com.google.gson.JsonParser.parseReader(JsonParser.java:146) ~[gson-2.11.0.jar!/:na]
at com.google.gson.JsonParser.parseReader(JsonParser.java:110) ~[gson-2.11.0.jar!/:na]
at com.microsoft.graph.core.content.BatchResponseContent.getBatchResponseContent(BatchResponseContent.java:168) ~[microsoft-graph-core-3.6.1.jar!/:na]
...
Caused by: com.google.gson.stream.MalformedJsonException: Unterminated object at line 1 column 149 path $.responses[1].body
The raw batch response returned by the API looks like this (simplified for clarity):
{
"responses": [
{
"id": "a063d4a4-XXXX-4433-8704-6b1252f7e864",
"status": 200,
"headers": {
"Cache-Control": "private",
"Content-Type": "application/json; odata.metadata=minimal; odata.streaming=true; IEEE754Compatible=false; charset=utf-8"
},
"body": {
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('user%40domain.com')/events(id)",
"value": [
{
"@odata.etag": "W/\"a+n3FpwGr06xxxxxxxxxjmCBjQ==\"",
"id": "AAMkAGVlYjJjNGxxxxxxxABGAAAAAAAkmk-HOxjKRZmNlLfGqYjaBwBr6fcWnAavTpXG0aD="
}
]
}
},
{
"id": "5f956687-XXXX-4aa4-a726-dc6b9810db1e",
"status": 503,
"headers": {
"Cache-Control": "private"
},
"body": Authentication Concurrency Limit Reached
}
]
}
As you can see, the second response has a body field that is not valid JSON (Authentication Concurrency Limit Reached), which causes the SDK to throw a JsonSyntaxException when parsing.
There is also another variant that falls into the same error:
{
"responses": [
{
"id": "6ea85c49-XXXX-4116-9844-2b0cbbf52ea3",
"status": 503,
"headers": {
"Content-Type": "text/html; charset=us-ascii"
},
"body":<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Service Unavailable</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Application Request Queue Full</h2>
<hr><p>HTTP Error 503. The application request queue is full.</p>
</BODY></HTML>
}
]
}
Expected behavior
The SDK should be able to handle non-JSON response bodies gracefully in batch responses. For example:
- Return the raw string body when the response is not valid JSON.
- Or provide a way to detect and handle such cases without throwing a parsing exception.
How to reproduce
- Use the
GraphServiceClient to send multiple requests (20 request per batch) in parallel (+4 in parallel) using GraphServiceClient().getBatchRequestBuilder().post()
- When the API is under load, one of the requests may return a 503 Authentication Concurrency Limit Reached error.
- The batch response will then contain a body field with plain text instead of valid JSON.
- Attempting to parse the batch response with BatchResponseContent.getResponseById(...) will throw a JsonSyntaxException.
To see the raw API response add the following interceptor to the OkHttpClient
@Slf4j
public class RawResponseLoggingInterceptor implements Interceptor {
@NotNull
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
ResponseBody responseBody = response.body();
if (responseBody != null) {
try {
MediaType contentType = responseBody.contentType();
String rawJson = responseBody.string();
log.info("[API] Raw JSON Response for URL [{}], Status [{}]: {}", request.url(), response.code(), rawJson);
ResponseBody newResponseBody = ResponseBody.create(rawJson, contentType);
return response.newBuilder().body(newResponseBody).build();
} catch (Exception e) {
log.error("[API] Error reading raw response body for URL [{}]", request.url(), e);
}
}
return response;
}
}
RawResponseLoggingInterceptor loggingInterceptor = new RawResponseLoggingInterceptor();
OkHttpClient customOkHttpClient = new OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.build();
GraphServiceClient client = new GraphServiceClient(authProvider, customOkHttpClient);
SDK Version
6.51.0
Latest version known to work for scenario above?
No response
Known Workarounds
No response
Debug output
Click to expand log
```
com.google.gson.JsonSyntaxException: com.google.gson.stream.MalformedJsonException: Unterminated object at line 1 column 149 path $.responses[0].body
See https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json
at com.google.gson.internal.Streams.parse(Streams.java:58) ~[gson-2.11.0.jar!/:na]
at com.google.gson.JsonParser.parseReader(JsonParser.java:146) ~[gson-2.11.0.jar!/:na]
at com.google.gson.JsonParser.parseReader(JsonParser.java:110) ~[gson-2.11.0.jar!/:na]
at com.microsoft.graph.core.content.BatchResponseContent.getBatchResponseContent(BatchResponseContent.java:168) ~[microsoft-graph-core-3.6.1.jar!/:na]
at com.microsoft.graph.core.content.BatchResponseContent.getResponseById(BatchResponseContent.java:94) ~[microsoft-graph-core-3.6.1.jar!/:na]
at java.base/java.util.HashMap.forEach(Unknown Source) ~[na:na]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Unknown Source) ~[na:na]
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359) ~[spring-aop-6.2.6.jar!/:6.2.6]
at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196) ~[spring-aop-6.2.6.jar!/:6.2.6]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-6.2.6.jar!/:6.2.6]
at org.springframework.aop.interceptor.AsyncExecutionInterceptor.lambda$invoke$0(AsyncExecutionInterceptor.java:114) ~[spring-aop-6.2.6.jar!/:6.2.6]
at java.base/java.util.concurrent.FutureTask.run(Unknown Source) ~[na:na]
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source) ~[na:na]
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source) ~[na:na]
at java.base/java.lang.Thread.run(Unknown Source) ~[na:na]
Caused by: com.google.gson.stream.MalformedJsonException: Unterminated object at line 1 column 149 path $.responses[0].body
See https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json
at com.google.gson.stream.JsonReader.syntaxError(JsonReader.java:1754) ~[gson-2.11.0.jar!/:na]
at com.google.gson.stream.JsonReader.doPeek(JsonReader.java:574) ~[gson-2.11.0.jar!/:na]
at com.google.gson.stream.JsonReader.hasNext(JsonReader.java:498) ~[gson-2.11.0.jar!/:na]
at com.google.gson.internal.bind.TypeAdapters$28.read(TypeAdapters.java:875) ~[gson-2.11.0.jar!/:na]
at com.google.gson.internal.bind.TypeAdapters$28.read(TypeAdapters.java:820) ~[gson-2.11.0.jar!/:na]
at com.google.gson.internal.Streams.parse(Streams.java:46) ~[gson-2.11.0.jar!/:na]
... 18 common frames omitted
</details>
### Configuration
- OS: Windows 11 Pro 24H2
- Architecture: x64
- JDK: OpenJDK Runtime Environment Temurin-21.0.3+9 (build 21.0.3+9-LTS)
### Other information
_No response_
Describe the bug
When making a batch request using the Microsoft Graph Java SDK, the SDK fails to parse the batch response if one of the individual responses contains a plain text body instead of valid JSON.
The raw batch response returned by the API looks like this (simplified for clarity):
{ "responses": [ { "id": "a063d4a4-XXXX-4433-8704-6b1252f7e864", "status": 200, "headers": { "Cache-Control": "private", "Content-Type": "application/json; odata.metadata=minimal; odata.streaming=true; IEEE754Compatible=false; charset=utf-8" }, "body": { "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('user%40domain.com')/events(id)", "value": [ { "@odata.etag": "W/\"a+n3FpwGr06xxxxxxxxxjmCBjQ==\"", "id": "AAMkAGVlYjJjNGxxxxxxxABGAAAAAAAkmk-HOxjKRZmNlLfGqYjaBwBr6fcWnAavTpXG0aD=" } ] } }, { "id": "5f956687-XXXX-4aa4-a726-dc6b9810db1e", "status": 503, "headers": { "Cache-Control": "private" }, "body": Authentication Concurrency Limit Reached } ] }As you can see, the second response has a body field that is not valid JSON (Authentication Concurrency Limit Reached), which causes the SDK to throw a
JsonSyntaxExceptionwhen parsing.There is also another variant that falls into the same error:
{ "responses": [ { "id": "6ea85c49-XXXX-4116-9844-2b0cbbf52ea3", "status": 503, "headers": { "Content-Type": "text/html; charset=us-ascii" }, "body":<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd"> <HTML><HEAD><TITLE>Service Unavailable</TITLE> <META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD> <BODY><h2>Application Request Queue Full</h2> <hr><p>HTTP Error 503. The application request queue is full.</p> </BODY></HTML> } ] }Expected behavior
The SDK should be able to handle non-JSON response bodies gracefully in batch responses. For example:
How to reproduce
GraphServiceClientto send multiple requests (20 request per batch) in parallel (+4 in parallel) usingGraphServiceClient().getBatchRequestBuilder().post()To see the raw API response add the following interceptor to the
OkHttpClientSDK Version
6.51.0
Latest version known to work for scenario above?
No response
Known Workarounds
No response
Debug output
Click to expand log
```com.google.gson.JsonSyntaxException: com.google.gson.stream.MalformedJsonException: Unterminated object at line 1 column 149 path $.responses[0].body
See https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json
at com.google.gson.internal.Streams.parse(Streams.java:58) ~[gson-2.11.0.jar!/:na]
at com.google.gson.JsonParser.parseReader(JsonParser.java:146) ~[gson-2.11.0.jar!/:na]
at com.google.gson.JsonParser.parseReader(JsonParser.java:110) ~[gson-2.11.0.jar!/:na]
at com.microsoft.graph.core.content.BatchResponseContent.getBatchResponseContent(BatchResponseContent.java:168) ~[microsoft-graph-core-3.6.1.jar!/:na]
at com.microsoft.graph.core.content.BatchResponseContent.getResponseById(BatchResponseContent.java:94) ~[microsoft-graph-core-3.6.1.jar!/:na]
at java.base/java.util.HashMap.forEach(Unknown Source) ~[na:na]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Unknown Source) ~[na:na]
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359) ~[spring-aop-6.2.6.jar!/:6.2.6]
at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196) ~[spring-aop-6.2.6.jar!/:6.2.6]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-6.2.6.jar!/:6.2.6]
at org.springframework.aop.interceptor.AsyncExecutionInterceptor.lambda$invoke$0(AsyncExecutionInterceptor.java:114) ~[spring-aop-6.2.6.jar!/:6.2.6]
at java.base/java.util.concurrent.FutureTask.run(Unknown Source) ~[na:na]
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source) ~[na:na]
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source) ~[na:na]
at java.base/java.lang.Thread.run(Unknown Source) ~[na:na]
Caused by: com.google.gson.stream.MalformedJsonException: Unterminated object at line 1 column 149 path $.responses[0].body
See https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json
at com.google.gson.stream.JsonReader.syntaxError(JsonReader.java:1754) ~[gson-2.11.0.jar!/:na]
at com.google.gson.stream.JsonReader.doPeek(JsonReader.java:574) ~[gson-2.11.0.jar!/:na]
at com.google.gson.stream.JsonReader.hasNext(JsonReader.java:498) ~[gson-2.11.0.jar!/:na]
at com.google.gson.internal.bind.TypeAdapters$28.read(TypeAdapters.java:875) ~[gson-2.11.0.jar!/:na]
at com.google.gson.internal.bind.TypeAdapters$28.read(TypeAdapters.java:820) ~[gson-2.11.0.jar!/:na]
at com.google.gson.internal.Streams.parse(Streams.java:46) ~[gson-2.11.0.jar!/:na]
... 18 common frames omitted