Skip to content

LargeFileUploadTask does not add header Content-Length (always?) #1520

@kekolab

Description

@kekolab

I couldn't manage to have the LargeFileUploadTask to work.
While debugging I noticed that LargeFileUploadTask.upload() does not add the Content-Length header to the request, as specified here

Expected behavior

Uploading a file

Actual behavior

Exception

Steps to reproduce the behavior

This is the code I use:

public class LargeUploadFileTaskTest {
    @Test
    public void test() throws IOException, IllegalAccessException, InvocationTargetException, NoSuchMethodException,
            InterruptedException {
        OkHttpClient.Builder clientBuilder = new OkHttpClient().newBuilder();
        String name = "theName";
        byte[] content = new byte[5];
        new Random().nextBytes(content);
        InputStream stream = new ByteArrayInputStream(content);
        long length = stream.available();
        ApacheHttpTransport transport = new ApacheHttpTransport(
                ApacheHttpTransport.newDefaultHttpClientBuilder().build());
        Credential credential = new Credential.Builder(BearerToken.authorizationHeaderAccessMethod())
                .setClientAuthentication(new ClientParametersAuthentication(CLIENT_ID, null))
                .setJsonFactory(new GsonFactory())
                .setTokenServerEncodedUrl(TOKEN_URL)
                .setTransport(transport)
                .build()
                .setAccessToken(ACCESS_TOKEN)
                .setExpirationTimeMilliseconds(EXPIRATION_MILLIS)
                .setRefreshToken(REFRESH_TOKEN);
        AuthenticationProvider authenticationProvider = new GoogleCredentialAuthenticationProvider(credential);
        GraphServiceClient client = new GraphServiceClient(authenticationProvider, clientBuilder.build());

        DriveItemUploadableProperties uploadableProperties = new DriveItemUploadableProperties();
        uploadableProperties.setName(name);
        uploadableProperties.setFileSize(length);
        uploadableProperties.getAdditionalData().put("@microsoft.graph.conflictBehavior", "fail");

        CreateUploadSessionPostRequestBody uploadSessionPostRequestBody = new CreateUploadSessionPostRequestBody();
        uploadSessionPostRequestBody.setItem(uploadableProperties);
        String driveId = client.drives().get().getValue().get(0).getId();
        UploadSession uploadSession = client.drives().byDriveId(driveId).items()
                .byDriveItemId("root:/" + name + ":")
                .createUploadSession()
                .post(uploadSessionPostRequestBody);

        LargeFileUploadTask<DriveItem> largeFileUploadTask = new LargeFileUploadTask<>(
                client.getRequestAdapter(), uploadSession,
                stream, length, DriveItem::createFromDiscriminatorValue);
        UploadResult<DriveItem> uploadResult = largeFileUploadTask.upload();
        client.drives().byDriveId(driveId).items().byDriveItemId(uploadResult.itemResponse.getId()).delete();
    }

    public static class GoogleCredentialAuthenticationProvider implements AuthenticationProvider {
        private Credential credential;

        public GoogleCredentialAuthenticationProvider(Credential credential) {
            this.credential = credential;
        }

        @Override
        public void authenticateRequest(RequestInformation request,
                Map<String, Object> additionalAuthenticationContext) {
            if (Instant.ofEpochMilli(this.credential.getExpirationTimeMilliseconds())
                    .isBefore(Instant.now().plusMillis(60000))) {
                try {
                    credential.refreshToken();
                } catch (IOException e) {
                    throw new RuntimeException("Cannot refresh token", e);
                }
            }
            request.headers.add("Authorization", "Bearer " + credential.getAccessToken());
        }
    }
}

receiving this exception:

com.microsoft.kiota.ApiException: generalException
        at com.microsoft.kiota.ApiExceptionBuilder.withMessage(ApiExceptionBuilder.java:45)
        at com.microsoft.graph.core.requests.upload.UploadResponseHandler.handleResponse(UploadResponseHandler.java:61)
        at com.microsoft.graph.core.requests.upload.UploadSliceRequestBuilder.put(UploadSliceRequestBuilder.java:69)
        at com.microsoft.graph.core.tasks.LargeFileUploadTask.uploadSlice(LargeFileUploadTask.java:207)
        at com.microsoft.graph.core.tasks.LargeFileUploadTask.upload(LargeFileUploadTask.java:131)
        at com.microsoft.graph.core.tasks.LargeFileUploadTask.upload(LargeFileUploadTask.java:111)
[...]

To understand why, I wrote this Interceptor to log the requests and responses:

public static class OkHttpRequestResponseLogger implements Interceptor {
    private boolean logResponse;
    private boolean logRequest;

    public OkHttpRequestResponseLogger(boolean logRequest, boolean logResponse) {
        this.logRequest = logRequest;
        this.logResponse = logResponse;
    }

    private void logHeaders(StringBuilder logMessage, Headers headers) {
        for (String name : headers.names())
            logMessage.append(name + " : " + headers.get(name) + System.lineSeparator());
    }

    private boolean isContentTypeText(MediaType contentType) {
        if (contentType == null)
            return false;
        String ct = contentType.toString();
        if (ct == null)
            return false;

            ct = ct.toLowerCase();
            return ct.contains("json") || ct.contains("text") || ct.contains("xml");
        }

        private byte[] logRequestBody(StringBuilder logMessage, RequestBody body) throws IOException {
            if (body == null)
                return null;
            Buffer sink = new Buffer();
            body.writeTo(sink);
            byte[] bytes = sink.readByteArray();
            logByteArray(logMessage, bytes, body.contentType());
            return bytes;
        }

        private void logByteArray(StringBuilder logMessage, byte[] bytes, MediaType contentType) {
            if (isContentTypeText(contentType)) {
                logMessage.append(new String(bytes));
            } else {
                logMessage.append(Hex.encodeHexString(bytes));
            }
            logMessage.append(System.lineSeparator());
        }

        private Request logRequest(StringBuilder logMessage, Request request) throws IOException {
            logMessage.append(request.method() + " " + request.url() + System.lineSeparator());
            logHeaders(logMessage, request.headers());
            RequestBody requestBody = request.body();
            byte[] body = logRequestBody(logMessage, requestBody);

            requestBody = requestBody != null ? RequestBody.create(body, requestBody.contentType()) : null;
            Request.Builder requestBuilder = new Request.Builder()
                    .cacheControl(request.cacheControl())
                    .headers(request.headers())
                    .method(request.method(), requestBody)
                    .url(request.url())
                    .removeHeader("Accept-Encoding"); // To avoid gzip encoding in the response
            return requestBuilder.build();
        }

        private Response logResponse(StringBuilder logMessage, Response response) throws IOException {
            logMessage.append(response.code() + " " + response.message() + System.lineSeparator());
            logHeaders(logMessage, response.headers());
            ResponseBody responseBody = response.body();
            byte[] body = logResponseBody(logMessage, responseBody);

            responseBody = responseBody != null ? ResponseBody.create(body, responseBody.contentType()) : null;
            return new Response.Builder()
                    .body(responseBody)
                    .code(response.code())
                    .handshake(response.handshake())
                    .headers(response.headers())
                    .message(response.message())
                    .networkResponse(response.networkResponse())
                    .priorResponse(response.priorResponse())
                    .protocol(response.protocol())
                    .receivedResponseAtMillis(response.receivedResponseAtMillis())
                    .request(response.request())
                    .sentRequestAtMillis(response.sentRequestAtMillis())
                    .build();
        }

        private byte[] logResponseBody(StringBuilder logMessage, ResponseBody body) throws IOException {
            if (body == null)
                return null;
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[] chunk = new byte[4096];
            int bytesRead;
            InputStream bodyStream = body.byteStream();
            while ((bytesRead = bodyStream.read(chunk)) != -1)
                baos.write(chunk, 0, bytesRead);
            byte[] bytes = baos.toByteArray();
            logByteArray(logMessage, bytes, body.contentType());
            return bytes;
        }

        @Override
        public Response intercept(Chain chain) throws IOException {
            StringBuilder logMessage = new StringBuilder();
            Request request = chain.request();
            if (logRequest) {
                logMessage.append("--- REQUEST ---" + System.lineSeparator());
                request = logRequest(logMessage, chain.request());
                logMessage.append(System.lineSeparator());
            }
            Response response = chain.proceed(request);
            if (logResponse) {
                logMessage.append("--- RESPONSE ---" + System.lineSeparator());
                response = logResponse(logMessage, response);
                logMessage.append(System.lineSeparator());
            }
            if (!logMessage.isEmpty()) {
                logMessage.append(System.lineSeparator());
                System.out.println(logMessage.toString());
            }
            return response;
        }
    }

and attached it to the OkHttpClientBuilder as a Network Interceptor:

OkHttpClient.Builder clientBuilder = new OkHttpClient().newBuilder().addNetworkInterceptor(new OkHttpRequestResponseLogger(true, true));

thus obtaining the following log. Please note that:

  • the log is created before removing the Accept-Encoding header; that's why it appears in the log of the Request;
  • the body of the request are the 5 bytes, hex-encoded
--- REQUEST ---
PUT https://api.onedrive.com/rup/83a259e7e5f58e69/eyJSZXNvdXJjZUlEIjoiODNBMjU5RTdFNUY1OEU2OSExMDYiLCJSZWxhdGlvbnNoaXBOYW1lIjoidGhlTmFtZSJ9/4mmUna845TQ8AIQV60JuGUz85GIKZqgXPfYDiL7D0iapzCnn2QB-YMxwHYOFDv1QVUXFsKdVlVyE5xXj-IYsVbOfj6S5D1tmV9qZd3MW5uFwE/eyJuYW1lIjoidGhlTmFtZSIsImZpbGVTaXplIjo1LCJAbmFtZS5jb25mbGljdEJlaGF2aW9yIjoiZmFpbCJ9/4wK3xIQUXIHrtW7IxFslvprEAtnAPaebYdqlIn3scsCAjqBSdIybJdRQYdchnG_ClfCWoqm5odGae1AZ07i3YlZsp8u9wwq2ARxBK8lcExWtjas0mi83cqw9v1TsNZRfvNpzmYOgNQZVHcx0YtypCYDG3GErD5TQGaXStuoYnzXMr32lRBq2lckM20KSVIm05nmxLWSEOguUwe6KqqU5wZgEjm_Wk-GFY3nmTNh1pFIC9kjc-7ZgD08VDfXWzi02Sciejxo1of7_RL6slEjanaFDf6TiwJ9zCA8IkXKqz8AWGEN1a8nF1jNrwfAzD2X5HdyqrnvX4T4OzaVh2K0QkF3q5IAvQxdZLH-UjhOBvYLV6M_d1Ds5GjLDQemXSMbdxqABmnTPE-0MSbaJYzBM7NiSzeH5f_M0REgBATIHwdDvRXIgEx_qiQfwvF5OPhEU82irTNjZ30_3gBOHAg1ARdAEBD7s9eBO9uKwkYQebFAOO3SMDMyoAUpEeFglE1rbxFom8OuzJhTpv36k8Qpk6sjUR-LSfXN9pB9GNIic2loKmO5cTbVOPSXD0pZ_HhMBVU
Accept-Encoding : gzip
authorization : Bearer [...]
Connection : Keep-Alive
content-range : bytes 0-4/5
Content-Type : application/octet-stream
Host : api.onedrive.com
Transfer-Encoding : chunked
User-Agent : okhttp/4.12.0
2cad62d665

--- RESPONSE ---
400 
content-length : 116
content-type : application/json; charset=utf-8
date : Wed, 21 Feb 2024 21:58:43 GMT
ms-cv : NEC5dq6oIUC8MbCCa1aR2Q.0
p3p : CP="BUS CUR CONo FIN IVDo ONL OUR PHY SAMo TELo"
strict-transport-security : max-age=31536000; includeSubDomains
www-authenticate : Bearer realm="OneDriveAPI", error="invalid_token", error_description="Invalid auth token"
x-asmversion : UNKNOWN; 19.1338.129.2007
x-cache : CONFIG_NOCACHE
x-msedge-ref : Ref A: 208B0365004E4A08AE53C2BE5A3790C1 Ref B: MIL30EDGE1120 Ref C: 2024-02-21T21:58:44Z
x-msnserver : AMS0PF70708741F
x-qosstats : {"ApiId":0,"ResultType":2,"SourcePropertyId":0,"TargetPropertyId":42}
x-throwsite : 7b59.16f2
{"error":{"code":"invalidRequest","message":"Declared fragment length does not match the provided number of bytes"}}

The mistery is that if I attach the Interceptor as an Application Interceptor instead of an Network Interceptor (see here):

OkHttpClient.Builder clientBuilder = new OkHttpClient().newBuilder().addInterceptor(new OkHttpRequestResponseLogger(true, true));

the content-length header magically appears (I'd think it is added when I rebuild the request) and the response is a 201/Created:

--- REQUEST ---
PUT https://api.onedrive.com/rup/83a259e7e5f58e69/eyJSZXNvdXJjZUlEIjoiODNBMjU5RTdFNUY1OEU2OSExMDYiLCJSZWxhdGlvbnNoaXBOYW1lIjoidGhlTmFtZSJ9/4mK4M4DSp7mPr5dU_-DCeMF6SA7G6ouclrdte55WKjYIkNPDuMD8rNNnhhOGDvGlQAYi6rVIaupnGxLyewAg7t3I4TFkcaEVEmgnj8nm3V3SE/eyJuYW1lIjoidGhlTmFtZSIsImZpbGVTaXplIjo1LCJAbmFtZS5jb25mbGljdEJlaGF2aW9yIjoiZmFpbCJ9/4wEdbotO1H-HwjM1PtejvTZjnCKaMmLMZXRg-apkzp_eOA2VmfLYquwCLjhVJ2MbXHsi5g6GrK1uDSlTHsPT--cFF3SHDcCPxqAQqTgJgH6MwE9Drcm-2pmxVb9eCcXtqlhiJk7qOPzWixG8RUuKkr_KGeOwo93I4JSrO71SoQWLBzqDoNpCK-NwQ_f6M3Uuat6E9aPtBfGijlEMLlJeyGq_V3E7EFE_ORVEX11mLIQSrzqZjv6f9n_SKlWsz4qIetSBUpEjQQkTPal3mThnOVDtrksBYfH5iiWcEoZbOWs7DOM4Re6IOsCBfb5WdwYnGyHfReiDKDd2t1puWBE4NqjF8aACUCzbAyqntVz6DvYA_VAuEwTfW6HMAFzzYyXHAuWi2toceziOzxsUyR9B9E6h3twTKFJzJbOiZvcdSpCdfQ1yJmTTlrQW55Dqz4xOXDmtNc6v4e13z4-SlxAFTL7f1bSxCXa0fo5hzWCvFzQBz38PZIIfuWZ02IfFleDt0lyn-NAZgv8XutecyRgVndhxdARn9ht6_vOYjnpdb8RgxH4wcxZikZA22OiGamh7Eb
authorization : Bearer 
content-length : 5
content-range : bytes 0-4/5
content-type : application/octet-stream
14af14b7f5

--- RESPONSE ---
201 
content-type : application/json; charset=utf-8
date : Wed, 21 Feb 2024 22:54:00 GMT
ms-cv : hhMVBRuH00+EX0dNqNlZsA.0
p3p : CP="BUS CUR CONo FIN IVDo ONL OUR PHY SAMo TELo"
strict-transport-security : max-age=31536000; includeSubDomains
www-authenticate : Bearer realm="OneDriveAPI", error="invalid_token", error_description="Invalid auth token"
x-asmversion : UNKNOWN; 19.1338.129.2007
x-cache : CONFIG_NOCACHE
x-msedge-ref : Ref A: 48BD001FEEB6405EBCD806A8492DD553 Ref B: MIL30EDGE1310 Ref C: 2024-02-21T22:54:00Z
x-msnserver : AMS0PF48CFBC45A
{"createdBy":{"application":{"id":"4c3cb947"},"user":{"id":"83a259e7e5f58e69"}},"createdDateTime":"2024-02-21T22:54:00.85Z","cTag":"aYzo4M0EyNTlFN0U1RjU4RTY5ITE3NjQyOC4yNTc","eTag":"aODNBMjU5RTdFNUY1OEU2OSExNzY0MjguMA","id":"83A259E7E5F58E69!176428","lastModifiedBy":{"application":{"id":"4c3cb947"},"user":{"id":"83a259e7e5f58e69"}},"lastModifiedDateTime":"2024-02-21T22:54:00.85Z","name":"theName","parentReference":{"driveId":"83a259e7e5f58e69","driveType":"personal","id":"83A259E7E5F58E69!106","path":"/drive/root:"},"size":5,"webUrl":"https://1drv.ms/u/s!AGmO9eXnWaKDiuIs","items":[],"file":{"hashes":{"quickXorHash":"FHgFBW5RDwAAAAAABQAAAAAAAAA=","sha1Hash":"EB7056C190724D15F3AD39CCE2AE90D9ABBEB609","sha256Hash":"243550BE6C4662D9D105238A5AAF2E5FE24C024E38F0EFBE6F2885B30C057C25"},"mimeType":"application/octet-stream"},"fileSystemInfo":{"createdDateTime":"2024-02-21T22:54:00.85Z","lastModifiedDateTime":"2024-02-21T22:54:00.85Z"},"reactions":{"commentCount":0},"tags":[],"lenses":[]}

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions