Skip to content

JsonSyntaxException in BatchResponseContent when batch response contains non-JSON body #2472

@bxacosta

Description

@bxacosta

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

  1. Use the GraphServiceClient to send multiple requests (20 request per batch) in parallel (+4 in parallel) using GraphServiceClient().getBatchRequestBuilder().post()
  2. When the API is under load, one of the requests may return a 503 Authentication Concurrency Limit Reached error.
  3. The batch response will then contain a body field with plain text instead of valid JSON.
  4. 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_

Metadata

Metadata

Assignees

No one assigned

    Labels

    status:waiting-for-triageAn issue that is yet to be reviewed or assignedtype:bugA broken experience

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions