Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
kekolab opened this issue Feb 21, 2024 · 1 comment · Fixed by #1518
Closed

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

kekolab opened this issue Feb 21, 2024 · 1 comment · Fixed by #1518
Assignees
Labels
bug Something isn't working help wanted Extra attention is needed

Comments

@kekolab
Copy link
Contributor

kekolab commented Feb 21, 2024

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)

@kekolab
Copy link
Contributor Author

kekolab commented Feb 21, 2024

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
Labels
bug Something isn't working help wanted Extra attention is needed
Projects
None yet
2 participants