-
Notifications
You must be signed in to change notification settings - Fork 146
Description
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) usingGraphServiceClient().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_