Skip to content

Updating a file via LargeFileUploadTask does not work (NullPointerException) #1517

Description

This is a duplication of #microsoftgraph/msgraph-sdk-java#1804.
Initially, I opened the issue on that repo, but it is more correct opening it here

As stated in the object, updating (i.e. modifying) a file via the LargeFileUploadTask does not work and results in a NullPointerException.

Expected behavior

Not receiving a NullPointerException

Actual behavior

Receiving a NullPointerException

API version

implementation 'com.microsoft.graph:microsoft-graph:6.2.0' // Include the sdk as a dependency

Steps to reproduce the behavior

  1. Create a new file
DriveItemUploadableProperties uploadableProperties = new DriveItemUploadableProperties();
uploadableProperties.setName(name);
uploadableProperties.setFileSize(length);
uploadableProperties.getAdditionalData().put("@microsoft.graph.conflictBehavior", "fail");
CreateUploadSessionPostRequestBody uploadSessionPostRequestBody = new CreateUploadSessionPostRequestBody();
uploadSessionPostRequestBody.setItem(uploadableProperties);
UploadSession uploadSession = drive.items().byDriveItemId(parent.getId() + ":/" + name + ":").createUploadSession().post(uploadSessionPostRequestBody);
LargeFileUploadTask<DriveItem> largeFileUploadTask = new  LargeFileUploadTask<>(
client.getRequestAdapter(), uploadSession, content, length, DriveItem::createFromDiscriminatorValue);
UploadResult<DriveItem> uploadResult = largeFileUploadTask.upload();
  1. Update it
    Note the difference in the upload session creation: above (creation) we use the parentId and the file name byDriveItemId(parent.getId() + ":/" + name + ":") while, below (update), we use the file id byDriveItemId(id)
DriveItemUploadableProperties uploadableProperties = new DriveItemUploadableProperties();
uploadableProperties.setName(name);
uploadableProperties.setFileSize(length);
uploadableProperties.getAdditionalData().put("@microsoft.graph.conflictBehavior", "replace");
CreateUploadSessionPostRequestBody uploadSessionPostRequestBody = new CreateUploadSessionPostRequestBody();
uploadSessionPostRequestBody.setItem(uploadableProperties);
UploadSession uploadSession = drive.items().byDriveItemId(id).createUploadSession().post(uploadSessionPostRequestBody);
LargeFileUploadTask<DriveItem> largeFileUploadTask = new  LargeFileUploadTask<>(
client.getRequestAdapter(), uploadSession, content, length, DriveItem::createFromDiscriminatorValue);
UploadResult<DriveItem> uploadResult = largeFileUploadTask.upload();
  1. You have a NullPointerException
java.lang.NullPointerException: Cannot invoke "java.util.Collection.toArray()" because "c" is null
 at java.base/java.util.ArrayList.([ArrayList.java:181](vscode-file://vscode-app/c:/Users/03200692/AppData/Local/Programs/Microsoft%20VS%20Code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html))
 at com.microsoft.graph.core.models.UploadSession.getNextExpectedRanges([UploadSession.java:61](vscode-file://vscode-app/c:/Users/03200692/AppData/Local/Programs/Microsoft%20VS%20Code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html))
 at com.microsoft.graph.core.requests.upload.UploadResponseHandler.handleResponse([UploadResponseHandler.java:79](vscode-file://vscode-app/c:/Users/03200692/AppData/Local/Programs/Microsoft%20VS%20Code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html))
 at com.microsoft.graph.core.requests.upload.UploadSliceRequestBuilder.put([UploadSliceRequestBuilder.java:69](vscode-file://vscode-app/c:/Users/03200692/AppData/Local/Programs/Microsoft%20VS%20Code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html))
 at com.microsoft.graph.core.tasks.LargeFileUploadTask.uploadSlice([LargeFileUploadTask.java:207](vscode-file://vscode-app/c:/Users/03200692/AppData/Local/Programs/Microsoft%20VS%20Code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html))
 at com.microsoft.graph.core.tasks.LargeFileUploadTask.upload([LargeFileUploadTask.java:131](vscode-file://vscode-app/c:/Users/03200692/AppData/Local/Programs/Microsoft%20VS%20Code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html))
 at com.microsoft.graph.core.tasks.LargeFileUploadTask.upload([LargeFileUploadTask.java:111](vscode-file://vscode-app/c:/Users/03200692/AppData/Local/Programs/Microsoft%20VS%20Code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html))
[...]

Important

The file on the onedrive is correctly updated!!!

Bug hunting

When you create a file, the server returns a 201 Created with the following response: :

{
   "createdBy":{
      "application":{
         "id":"4c3cb947"
      },
      "user":{
         "id":"83a259e7e5f58e69"
      }
   },
   "createdDateTime":"2024-02-21T10:53:40.397Z",
   "cTag":"aYzo4M0EyNTlFN0U1RjU4RTY5ITE3NjM4OS4yNTc",
   "eTag":"aODNBMjU5RTdFNUY1OEU2OSExNzYzODkuMA",
   "id":"83A259E7E5F58E69!176389",
   "lastModifiedBy":{
      "application":{
         "id":"4c3cb947"
      },
      "user":{
         "id":"83a259e7e5f58e69"
      }
   },
   "lastModifiedDateTime":"2024-02-21T10:53:40.397Z",
   "name":"f1",
   "parentReference":{
      "driveId":"83a259e7e5f58e69",
      "driveType":"personal",
      "id":"83A259E7E5F58E69!176388",
      "name":"testFolder1708512810754",
      "path":"/drive/root:/testFolder1708512810754"
   },
   "size":4096,
   "webUrl":"https://1drv.ms/u/s!AGmO9eXnWaKDiuIF",
   "items":[
      
   ],
   "file":{
      "hashes":{
         "quickXorHash":"5gRtDBd9iwj8tz2tb1ViJeM3gX8=",
         "sha1Hash":"3CCFE9D871858B15B3F00FCB0AF47D171D9D9E2F",
         "sha256Hash":"55A65AE013961B1FC704C0371DC4A6936A6251685B09B014AB48BDAC27258AC7"
      },
      "mimeType":"application/octet-stream"
   },
   "fileSystemInfo":{
      "createdDateTime":"2024-02-21T10:53:40.396Z",
      "lastModifiedDateTime":"2024-02-21T10:53:40.396Z"
   },
   "reactions":{
      "commentCount":0
   },
   "tags":[
      
   ],
   "lenses":[
      
   ]
}

while, when you update a file the server returns a 200 OK with the following response:

{
   "createdBy":{
      "application":{
         "id":"4c3cb947"
      },
      "user":{
         "id":"83a259e7e5f58e69"
      }
   },
   "createdDateTime":"2024-02-21T10:53:40.397Z",
   "cTag":"aYzo4M0EyNTlFN0U1RjU4RTY5ITE3NjM4OS4yNTg",
   "eTag":"aODNBMjU5RTdFNUY1OEU2OSExNzYzODkuMw",
   "id":"83A259E7E5F58E69!176389",
   "lastModifiedBy":{
      "application":{
         "id":"4c3cb947"
      },
      "user":{
         "id":"83a259e7e5f58e69"
      }
   },
   "lastModifiedDateTime":"2024-02-21T10:54:04.21Z",
   "name":"f1",
   "parentReference":{
      "driveId":"83a259e7e5f58e69",
      "driveType":"personal",
      "id":"83A259E7E5F58E69!176388",
      "name":"testFolder1708512810754",
      "path":"/drive/root:/testFolder1708512810754"
   },
   "size":4096,
   "webUrl":"https://1drv.ms/u/s!AGmO9eXnWaKDiuIF",
   "items":[
      
   ],
   "file":{
      "hashes":{
         "quickXorHash":"T43AsKcwz5bu2iAV5IcCghvWqRY=",
         "sha1Hash":"2C8C824E1300C1FE6F3A8C919F9ECDD2CD47CF8D",
         "sha256Hash":"7FF463935CD4132EBA2958AB568514A249EFA2A0A7E07CC2916223603162FD65"
      },
      "mimeType":"application/octet-stream"
   },
   "fileSystemInfo":{
      "createdDateTime":"2024-02-21T10:53:40.396Z",
      "lastModifiedDateTime":"2024-02-21T10:54:04.193Z"
   },
   "reactions":{
      "commentCount":0
   },
   "tags":[
      
   ],
   "lenses":[
      
   ]
}

The response is the same (and, actually, it's as documented in the REST reference, but the problem is in the HTTP response code. The response, indeed, is parsed by com.microsoft.graph.core.requests.upload.UploadResponseHandler#handleResponse:

        Objects.requireNonNull(response);
        Objects.requireNonNull(factory);
        try (final ResponseBody body = response.body()) {
            if (Objects.isNull(body)) {
                throw new ApiException(ErrorConstants.Messages.NO_RESPONSE_FOR_UPLOAD);
            }
            try(final InputStream in = body.byteStream()){
                final String contentType = body.contentType().toString().split(";")[0]; //contentType.toString() returns in format <mediaType>;<charset>, we only want the mediaType.
                if(!response.isSuccessful()) {
                    throw new ApiExceptionBuilder()
                            .withMessage(ErrorConstants.Codes.GENERAL_EXCEPTION)
                            .withResponseStatusCode(response.code())
                            .withResponseHeaders(HeadersCompatibility.getResponseHeaders(response.headers()))
                            .build();
                }
                UploadResult<T> uploadResult = new UploadResult<>();
                if (response.code() == HttpURLConnection.HTTP_CREATED) {
                    if (body.contentLength() > 0) {
                        final ParseNode uploadTypeParseNode = parseNodeFactory.getParseNode(contentType, in);
                        uploadResult.itemResponse = uploadTypeParseNode.getObjectValue(factory);
                    }
                    final String location = response.headers().get("location");
                    if(!Objects.isNull(location) && !location.isEmpty()) {
                        uploadResult.location = new URI(location);
                    }
                } else {
                    final ParseNode parseNode = parseNodeFactory.getParseNode(contentType, in);
                    final UploadSession uploadSession = parseNode.getObjectValue(UploadSession::createFromDiscriminatorValue);
                    if (!uploadSession.getNextExpectedRanges().isEmpty()) {
                        uploadResult.uploadSession = uploadSession;
                    } else {
                        uploadResult.itemResponse = parseNode.getObjectValue(factory);
                    }
                }
                return uploadResult;
            }
        }
        catch(IOException | URISyntaxException ex) {
            throw new RuntimeException(ex);
        }

At the 18th line there is the block that manages the creation response.code() == HttpURLConnection.HTTP_CREATED; if you update the file instead of creating it, you receive a 201 and go in the else block which fails at the line if (!uploadSession.getNextExpectedRanges().isEmpty()) because the received JSON (see above) does not have any property called nextExpectedRanges thus uploadSession.getNextExpectedRanges() returns null and invoking isEmpty() causes the NPE.

Possible solution

Replace line 18 of the method:

if (response.code() == HttpURLConnection.HTTP_CREATED) {

with:

int responseCode = response.code();
if (responseCode == HttpURLConnection.HTTP_CREATED || responseCode == HttpURLConnection.HTTP_OK) {

I haven't tried it (not quite at ease with github and pull requests, but that should work)

Activity

  1. kekolab commented on Feb 21, 2024

    @kekolab
    ContributorAuthor

    The proposed solution in #1517 (comment) will break the test com.microsoft.graph.core.requests.upload.UploadResponseHandlerTest#getUploadSessionOnProgressingUpload, but... with respect to this test:

        @Test
        void getUploadSessionOnProgressingUpload() {
            registry.contentTypeAssociatedFactories.put(CoreConstants.MimeTypeNames.APPLICATION_JSON, new JsonParseNodeFactory());
    
            UploadResponseHandler responseHandler = new UploadResponseHandler(null);
            ResponseBody body = ResponseBody.create(
                "{\n" +
                    "   \"expirationDateTime\": \"2015-01-29T09:21:55.523Z\",\n" +
                    "   \"nextExpectedRanges\": [\n" +
                    "   \"12345-55232\",\n" +
                    "   \"77829-99375\"\n" +
                    "   ]" +
                    "}"
                , MediaType.parse(CoreConstants.MimeTypeNames.APPLICATION_JSON));
            Response response = new Response.Builder()
                .request(mock(Request.class))
                .protocol(mock(Protocol.class))
                .message("OK")
                .body(body)
                .code(HttpURLConnection.HTTP_OK)
                .build();
            UploadResult<TestDriveItem> result = responseHandler
                .handleResponse(response, TestDriveItem::createFromDiscriminatorValue);
            UploadSession session = (UploadSession) result.uploadSession;
    
            assertFalse(result.isUploadSuccessful());
            assertNotNull(session);
            assertTrue(session.getUploadUrl().isEmpty());
            assertEquals(OffsetDateTime.parse("2015-01-29T09:21:55.523Z"), session.getExpirationDateTime());
            assertEquals("12345-55232", session.getNextExpectedRanges().get(0));
            assertEquals("77829-99375", session.getNextExpectedRanges().get(1));
            assertEquals(2, session.getNextExpectedRanges().size());
        }
    

    why does it have a 200/OK as a response? In the REST reference it's written that this type of response has a 202/Accepted as a response code and that 200/OK and 201/Created are used when the upload is completed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Standard GitHub labelIssue caused by core project dependency modules or librarybugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions