Skip to content

Commit 0f88740

Browse files
authored
Merge pull request #1548 from microsoftgraph/fix/lfu
Fix large file upload
2 parents 64b1e20 + c13f9d6 commit 0f88740

File tree

7 files changed

+99
-51
lines changed

7 files changed

+99
-51
lines changed

CHANGELOG.md

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
### Changed
1313

14+
## [3.1.8]
15+
16+
### Added
17+
18+
### Changed
19+
- Changed chunkInputStream method in LargeFileUploadTask to resolve IndexOutOfBoundsException when uploading large files
20+
- Fix Large File Upload bug where exception was thrown for completed successful uploads
21+
1422
## [3.1.7] - 2024-03-28
1523

1624
### Changed
@@ -60,13 +68,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6068
### Changed
6169

6270
- Version bump for Java SDK GA release.
63-
- Bumps Kiota-Java abstractions, authentication, http, and serialization components for Java SDK 6.1.0 release.
71+
- Bumps Kiota-Java abstractions, authentication, http, and serialization components for Java SDK 6.1.0 release.
6472

6573
## [3.0.12] - 2023-12-15
6674

67-
### Fixed
75+
### Fixed
6876

69-
- Fixes a bug where a null collection for allowedHosts would result in failure to initialize client. [#1411](https://github.com/microsoftgraph/msgraph-sdk-java-core/pull/1411)
77+
- Fixes a bug where a null collection for allowedHosts would result in failure to initialize client. [#1411](https://github.com/microsoftgraph/msgraph-sdk-java-core/pull/1411)
7078

7179
## [3.0.11] - 2023-12-08
7280

@@ -77,7 +85,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7785

7886
## [3.0.10] - 2023-11-27
7987

80-
### Changed
88+
### Changed
8189

8290
- Removed the usage of reflection for enum deserialization and reordered RequestAdapter method arguments. [Kiota-Java #840](https://github.com/microsoft/kiota-java/issues/840)
8391

@@ -100,13 +108,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
100108

101109
### Removed
102110

103-
- Removes 'SuppressFBWarnings' annotations and dependency.
111+
- Removes 'SuppressFBWarnings' annotations and dependency.
104112

105113
## [3.0.7] - 2023-07-20
106114

107115
### Added
108116

109-
- Adds graph-java-sdk implementation of the `UrlReplaceHandler` middleware including default replacement pairs.
117+
- Adds graph-java-sdk implementation of the `UrlReplaceHandler` middleware including default replacement pairs.
110118
- Default replacement pair: '/users/TokenToReplace' -> '/me'
111119

112120
## [3.0.6] - 2023-07-11
@@ -123,11 +131,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
123131

124132
## [3.0.4] - 2023-05-03
125133

126-
### Added
127-
128-
- Added LargeFileUploadTask functionality for kiota generated service libraries.
134+
### Added
135+
136+
- Added LargeFileUploadTask functionality for kiota generated service libraries.
129137

130-
### Fixed
138+
### Fixed
131139

132140
- Fixes formatting used in the headers added by the telemetry handler to align with the [msGraph sdk spec.](https://github.com/microsoftgraph/msgraph-sdk-design/blob/master/middleware/TelemetryHandler.md)
133141

@@ -171,7 +179,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
171179
private OkHttpClient createClient(@Nonnull final IAuthenticationProvider auth) {
172180
return HttpClients.createDefault(auth);
173181
}
174-
182+
175183
// then create the GraphServiceClient
176184
IAuthenticationProvider authenticationProvider = ...;
177185
GraphServiceClient
@@ -183,16 +191,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
183191

184192
## [2.0.20] - 2023-10-23
185193

186-
### Changed
194+
### Changed
187195

188-
- Updates Okhttp3 to avoid transient vulnerabilty. [#1038](https://github.com/microsoftgraph/msgraph-sdk-java-core/issues/1038)
196+
- Updates Okhttp3 to avoid transient vulnerabilty. [#1038](https://github.com/microsoftgraph/msgraph-sdk-java-core/issues/1038)
189197

190198
## [2.0.19] - 2023-06-20
191199

192200
### Changed
193201

194202
- Remove explicit logging of GraphServiceException in the CoreHttpProvider class. [#885](https://github.com/microsoftgraph/msgraph-sdk-java-core/issues/885)
195-
- Thank you to @MaHa6543 for the contribution.
203+
- Thank you to @MaHa6543 for the contribution.
196204

197205
## [2.0.18] - 2023-04-06
198206

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ mavenGroupId = com.microsoft.graph
2525
mavenArtifactId = microsoft-graph-core
2626
mavenMajorVersion = 3
2727
mavenMinorVersion = 1
28-
mavenPatchVersion = 7
28+
mavenPatchVersion = 8
2929
mavenArtifactSuffix =
3030

3131
#These values are used to run functional tests

gradle/dependencies.gradle

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ dependencies {
77
testImplementation 'io.opentelemetry:opentelemetry-api:1.37.0'
88
testImplementation 'io.opentelemetry:opentelemetry-context:1.37.0'
99
testImplementation 'io.github.std-uritemplate:std-uritemplate:0.0.57'
10-
1110
implementation 'com.google.code.gson:gson:2.10.1'
1211

1312
implementation 'jakarta.annotation:jakarta.annotation-api:3.0.0'

src/main/java/com/microsoft/graph/core/CoreConstants.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ private CoreConstants() {}
1414
private static class VersionValues {
1515
private static final int MAJOR = 3;
1616
private static final int MINOR = 1;
17-
private static final int PATCH = 7;
17+
private static final int PATCH = 8;
1818
}
1919

2020
/**

src/main/java/com/microsoft/graph/core/requests/upload/UploadResponseHandler.java

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -52,36 +52,46 @@ public <T extends Parsable> UploadResult<T> handleResponse(@Nonnull final Respon
5252
Objects.requireNonNull(response);
5353
Objects.requireNonNull(factory);
5454
try (final ResponseBody body = response.body()) {
55-
if (Objects.isNull(body)) {
55+
UploadResult<T> uploadResult = new UploadResult<>();
56+
String contentLengthHeader = response.headers().get("content-length");
57+
// rely on content-type OR content-length headers to determine if response body is empty.
58+
// Response body() may be non-null despite being empty in raw response https://square.github.io/okhttp/3.x/okhttp/okhttp3/Response.html#body--
59+
// content-length header is not always present in Graph responses. Content-type is more reliable
60+
if (Objects.isNull(body)
61+
|| Objects.isNull(body.contentType())
62+
|| (!Objects.isNull(contentLengthHeader) && Integer.parseInt(contentLengthHeader) == 0)
63+
) {
64+
if (response.code() == HttpURLConnection.HTTP_CREATED) {
65+
final String location = response.headers().get("location");
66+
if(!Objects.isNull(location) && !location.isEmpty()) {
67+
uploadResult.location = new URI(location);
68+
return uploadResult;
69+
}
70+
}
5671
throw new ApiException(ErrorConstants.Messages.NO_RESPONSE_FOR_UPLOAD);
5772
}
5873
try(final InputStream in = body.byteStream()){
59-
final String contentType = body.contentType().toString().split(";")[0]; //contentType.toString() returns in format <mediaType>;<charset>, we only want the mediaType.
6074
if(!response.isSuccessful()) {
6175
throw new ApiExceptionBuilder()
6276
.withMessage(ErrorConstants.Codes.GENERAL_EXCEPTION)
6377
.withResponseStatusCode(response.code())
6478
.withResponseHeaders(HeadersCompatibility.getResponseHeaders(response.headers()))
6579
.build();
6680
}
67-
UploadResult<T> uploadResult = new UploadResult<>();
68-
if (response.code() == HttpURLConnection.HTTP_CREATED) {
69-
if (body.contentLength() > 0) {
70-
final ParseNode uploadTypeParseNode = parseNodeFactory.getParseNode(contentType, in);
71-
uploadResult.itemResponse = uploadTypeParseNode.getObjectValue(factory);
72-
}
73-
final String location = response.headers().get("location");
74-
if(!Objects.isNull(location) && !location.isEmpty()) {
75-
uploadResult.location = new URI(location);
76-
}
77-
} else {
81+
boolean canBeParsed = (!Objects.isNull(contentLengthHeader) && Integer.parseInt(contentLengthHeader) > 0) || !Objects.isNull(body.contentType());
82+
String contentType = canBeParsed ? body.contentType().toString().split(";")[0] : null; //contentType.toString() returns in format <mediaType>;<charset>, we only want the mediaType.
83+
if (canBeParsed) {
7884
final ParseNode parseNode = parseNodeFactory.getParseNode(contentType, in);
79-
final UploadSession uploadSession = parseNode.getObjectValue(UploadSession::createFromDiscriminatorValue);
80-
final List<String> nextExpectedRanges = uploadSession.getNextExpectedRanges();
81-
if (!(nextExpectedRanges == null || nextExpectedRanges.isEmpty())) {
82-
uploadResult.uploadSession = uploadSession;
83-
} else {
85+
if (response.code() == HttpURLConnection.HTTP_CREATED) {
8486
uploadResult.itemResponse = parseNode.getObjectValue(factory);
87+
} else {
88+
final UploadSession uploadSession = parseNode.getObjectValue(UploadSession::createFromDiscriminatorValue);
89+
final List<String> nextExpectedRanges = uploadSession.getNextExpectedRanges();
90+
if (!(nextExpectedRanges == null || nextExpectedRanges.isEmpty())) {
91+
uploadResult.uploadSession = uploadSession;
92+
} else {
93+
uploadResult.itemResponse = parseNode.getObjectValue(factory);
94+
}
8595
}
8696
}
8797
return uploadResult;
@@ -91,6 +101,7 @@ public <T extends Parsable> UploadResult<T> handleResponse(@Nonnull final Respon
91101
throw new RuntimeException(ex);
92102
}
93103
}
104+
94105
}
95106

96107

src/main/java/com/microsoft/graph/core/tasks/LargeFileUploadTask.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ public IUploadSession updateSessionStatus() {
201201
return session;
202202
}
203203
private UploadResult<T> uploadSlice(UploadSliceRequestBuilder<T> uploadSliceRequestBuilder, ArrayList<Throwable> exceptionsList) throws IOException {
204-
byte[] buffer = chunkInputStream(uploadStream,(int) uploadSliceRequestBuilder.getRangeBegin(), (int)uploadSliceRequestBuilder.getRangeLength());
204+
byte[] buffer = chunkInputStream(uploadStream, (int)uploadSliceRequestBuilder.getRangeLength());
205205
ByteArrayInputStream chunkStream = new ByteArrayInputStream(buffer);
206206
try {
207207
return uploadSliceRequestBuilder.put(chunkStream);
@@ -275,9 +275,9 @@ private long nextSliceSize(long rangeBegin, long rangeEnd) {
275275
long size = rangeEnd - rangeBegin + 1;
276276
return Math.min(size, this.maxSliceSize);
277277
}
278-
private byte[] chunkInputStream(InputStream stream, int begin, int length) throws IOException {
278+
private byte[] chunkInputStream(InputStream stream, int length) throws IOException {
279279
byte[] buffer = new byte[length];
280-
int lengthAssert = stream.read(buffer, begin, length);
280+
int lengthAssert = stream.read(buffer);
281281
assert lengthAssert == length;
282282
return buffer;
283283
}

src/test/java/com/microsoft/graph/core/requests/upload/UploadResponseHandlerTest.java

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,16 @@
1111
import okhttp3.*;
1212
import org.junit.jupiter.api.Assertions;
1313
import org.junit.jupiter.api.Test;
14+
import org.mockito.stubbing.Answer;
1415

1516
import static org.junit.jupiter.api.Assertions.*;
17+
import static org.mockito.ArgumentMatchers.any;
18+
import static org.mockito.Mockito.doAnswer;
1619
import static org.mockito.Mockito.mock;
20+
import static org.mockito.Mockito.when;
1721
import static com.microsoft.kiota.serialization.ParseNodeFactoryRegistry.defaultInstance;
1822

23+
import java.io.IOException;
1924
import java.net.HttpURLConnection;
2025
import java.time.OffsetDateTime;
2126

@@ -24,7 +29,7 @@ class UploadResponseHandlerTest {
2429
ParseNodeFactoryRegistry registry = defaultInstance;
2530

2631
@Test
27-
void GetUploadItemOnCompletedUpload() {
32+
void GetUploadItemOnCompletedUpload() throws IOException {
2833
registry.contentTypeAssociatedFactories.put(CoreConstants.MimeTypeNames.APPLICATION_JSON, new JsonParseNodeFactory());
2934

3035
UploadResponseHandler responseHandler = new UploadResponseHandler(null);
@@ -41,8 +46,9 @@ void GetUploadItemOnCompletedUpload() {
4146
.body(body)
4247
.code(HttpURLConnection.HTTP_CREATED)
4348
.build();
49+
OkHttpClient mockHttpClient = getMockClient(response);
4450
UploadResult<TestDriveItem> result = responseHandler
45-
.handleResponse(response, TestDriveItem::createFromDiscriminatorValue);
51+
.handleResponse(mockHttpClient.newCall(mock(Request.class)).execute(), TestDriveItem::createFromDiscriminatorValue);
4652
responseHandler.handleResponse(response, parseNode -> {return new TestDriveItem();});
4753
TestDriveItem item = result.itemResponse;
4854
assertTrue(result.isUploadSuccessful());
@@ -53,7 +59,7 @@ void GetUploadItemOnCompletedUpload() {
5359
}
5460

5561
@Test
56-
void GetUploadItemOnCompletedUpdate() {
62+
void GetUploadItemOnCompletedUpdate() throws IOException {
5763
registry.contentTypeAssociatedFactories.put(CoreConstants.MimeTypeNames.APPLICATION_JSON, new JsonParseNodeFactory());
5864

5965
UploadResponseHandler responseHandler = new UploadResponseHandler(null);
@@ -70,8 +76,9 @@ void GetUploadItemOnCompletedUpdate() {
7076
.code(HttpURLConnection.HTTP_OK)
7177
.message("OK")
7278
.build();
79+
OkHttpClient mockHttpClient = getMockClient(response);
7380
UploadResult<TestDriveItem> result = responseHandler
74-
.handleResponse(response, TestDriveItem::createFromDiscriminatorValue);
81+
.handleResponse(mockHttpClient.newCall(mock(Request.class)).execute(), TestDriveItem::createFromDiscriminatorValue);
7582
responseHandler.handleResponse(response, parseNode -> {return new TestDriveItem();});
7683
TestDriveItem item = result.itemResponse;
7784
assertTrue(result.isUploadSuccessful());
@@ -82,28 +89,29 @@ void GetUploadItemOnCompletedUpdate() {
8289
}
8390

8491
@Test
85-
void getFileAttachmentLocationOnCompletedUpload() {
92+
void getFileAttachmentLocationOnCompletedUpload() throws IOException {
8693
registry.contentTypeAssociatedFactories.put(CoreConstants.MimeTypeNames.APPLICATION_JSON, new JsonParseNodeFactory());
8794

8895
UploadResponseHandler responseHandler = new UploadResponseHandler(null);
8996
Response response = new Response.Builder()
9097
.request(mock(Request.class))
9198
.protocol(mock(Protocol.class))
9299
.message("success")
93-
.body(ResponseBody.create("", MediaType.parse(CoreConstants.MimeTypeNames.APPLICATION_JSON)))
94100
.code(HttpURLConnection.HTTP_CREATED)
95101
.header("location", "http://localhost")
96102
.build();
103+
OkHttpClient mockClient = getMockClient(response);
97104
UploadResult<TestDriveItem> result = responseHandler
98-
.handleResponse(response,TestDriveItem::createFromDiscriminatorValue);
105+
.handleResponse(mockClient.newCall(mock(Request.class)).execute()
106+
,TestDriveItem::createFromDiscriminatorValue);
99107
TestDriveItem item = result.itemResponse;
100108

101109
assertTrue(result.isUploadSuccessful());
102110
assertNull(item);
103111
assertEquals("http://localhost", result.location.toString());
104112
}
105113
@Test
106-
void getUploadSessionOnProgressingUpload() {
114+
void getUploadSessionOnProgressingUpload() throws IOException {
107115
registry.contentTypeAssociatedFactories.put(CoreConstants.MimeTypeNames.APPLICATION_JSON, new JsonParseNodeFactory());
108116

109117
UploadResponseHandler responseHandler = new UploadResponseHandler(null);
@@ -123,8 +131,9 @@ void getUploadSessionOnProgressingUpload() {
123131
.body(body)
124132
.code(HttpURLConnection.HTTP_ACCEPTED)
125133
.build();
134+
OkHttpClient mockHttpClient = getMockClient(response);
126135
UploadResult<TestDriveItem> result = responseHandler
127-
.handleResponse(response, TestDriveItem::createFromDiscriminatorValue);
136+
.handleResponse(mockHttpClient.newCall(mock(Request.class)).execute(), TestDriveItem::createFromDiscriminatorValue);
128137
UploadSession session = (UploadSession) result.uploadSession;
129138

130139
assertFalse(result.isUploadSuccessful());
@@ -137,7 +146,7 @@ void getUploadSessionOnProgressingUpload() {
137146
}
138147

139148
@Test
140-
void throwsServiceExceptionOnErrorResponse() {
149+
void throwsServiceExceptionOnErrorResponse() throws IOException {
141150
UploadResponseHandler responseHandler = new UploadResponseHandler(null);
142151
ResponseBody body = ResponseBody.create("{\n" +
143152
" \"error\": {\n"+
@@ -157,17 +166,18 @@ void throwsServiceExceptionOnErrorResponse() {
157166
.body(body)
158167
.code(HttpURLConnection.HTTP_UNAUTHORIZED)
159168
.build();
169+
OkHttpClient mockHttpClient = getMockClient(response);
160170

161171
try {
162172
responseHandler
163-
.handleResponse(response, TestDriveItem::createFromDiscriminatorValue);
173+
.handleResponse(mockHttpClient.newCall(mock(Request.class)).execute(), TestDriveItem::createFromDiscriminatorValue);
164174
} catch (ApiException ex) {
165175
Assertions.assertEquals(ErrorConstants.Codes.GENERAL_EXCEPTION, ex.getMessage());
166176
assertEquals(HttpURLConnection.HTTP_UNAUTHORIZED, ex.getResponseStatusCode());
167177
}
168178
}
169179
@Test
170-
void throwsSerializationErrorOnInvalidJson() {
180+
void throwsSerializationErrorOnInvalidJson() throws IOException {
171181
UploadResponseHandler responseHandler = new UploadResponseHandler(null);
172182
String malformedResponse =
173183
" \"error\": {\n"+
@@ -188,11 +198,31 @@ void throwsSerializationErrorOnInvalidJson() {
188198
.body(body)
189199
.code(HttpURLConnection.HTTP_UNAUTHORIZED)
190200
.build();
201+
OkHttpClient mockHttpClient = getMockClient(response);
191202
try {
192203
responseHandler
193-
.handleResponse(response, TestDriveItem::createFromDiscriminatorValue);
204+
.handleResponse(mockHttpClient.newCall(mock(Request.class)).execute(), TestDriveItem::createFromDiscriminatorValue);
194205
} catch (ApiException ex) {
195206
assertEquals(ErrorConstants.Codes.GENERAL_EXCEPTION, ex.getMessage());
196207
}
197208
}
209+
210+
public static OkHttpClient getMockClient(final Response response) throws IOException {
211+
final OkHttpClient mockClient = mock(OkHttpClient.class);
212+
final Call remoteCall = mock(Call.class);
213+
final Dispatcher dispatcher = new Dispatcher();
214+
when(remoteCall.execute()).thenReturn(response);
215+
doAnswer(
216+
(Answer<Void>)
217+
invocation -> {
218+
Callback callback = invocation.getArgument(0);
219+
callback.onResponse(null, response);
220+
return null;
221+
})
222+
.when(remoteCall)
223+
.enqueue(any(Callback.class));
224+
when(mockClient.dispatcher()).thenReturn(dispatcher);
225+
when(mockClient.newCall(any())).thenReturn(remoteCall);
226+
return mockClient;
227+
}
198228
}

0 commit comments

Comments
 (0)