diff --git a/.vscode/settings.json b/.vscode/settings.json index 92a5ea5d0..78348c083 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,4 +5,4 @@ ], "java.configuration.updateBuildConfiguration": "interactive", "java.compile.nullAnalysis.mode": "automatic" -} \ No newline at end of file +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 1459dd08e..6727aa121 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +### Changed + +## [1.1.5] + +### Changed + +- Fixed exception thrown when setting content length on stream request bodies. + ## [1.1.4] - 2024-04-04 ### Added @@ -33,7 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Fixed a bug where not providing scopes to `AzureIdentityAccessTokenProvider` failed with `UnsupportedOperationException` when attempting to fetch the token. [microsoftgraph/msgraph-sdk-java#1882](https://github.com/microsoftgraph/msgraph-sdk-java/issues/1882) +- Fixed a bug where not providing scopes to `AzureIdentityAccessTokenProvider` failed with `UnsupportedOperationException` when attempting to fetch the token. [microsoftgraph/msgraph-sdk-java#1882](https://github.com/microsoftgraph/msgraph-sdk-java/issues/1882) ## [1.1.0] - 2024-02-14 @@ -43,13 +53,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.0.6] - 2023-03-04 -### Changed +### Changed - Fixed a regression with the content length request header from 1.0.5. ## [1.0.5] - 2023-02-28 -### Changed +### Changed - Added contentLength property to RequestInformation to facilitate in setting the content length of the Okhttp3 RequestBody object within the OkhttpRequestAdapter. @@ -210,7 +220,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Add PeriodAndDuration constructor to create new object from a PeriodAndDuration object. +- Add PeriodAndDuration constructor to create new object from a PeriodAndDuration object. ## [0.7.0] - 2023-08-18 @@ -222,7 +232,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Javax annotations replaced in favor of Jakarta annotations. +- Javax annotations replaced in favor of Jakarta annotations. ## [0.5.0] - 2023-07-26 @@ -269,8 +279,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Adds a NativeResponseHandler to abstractions. -- Adds setResponseHandler method to RequestInformation class in abstractions. +- Adds a NativeResponseHandler to abstractions. +- Adds setResponseHandler method to RequestInformation class in abstractions. ## [0.4.1] - 2023-03-29 @@ -373,11 +383,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Added ResponseHandlerOption class. +- Added ResponseHandlerOption class. ### Changed -- Removed responseHandler parameter from RequestAdapter sendAsync methods. +- Removed responseHandler parameter from RequestAdapter sendAsync methods. - Compatibility for Android level 26. ## [0.0.5] - 2022-09-15 diff --git a/components/http/okHttp/src/main/java/com/microsoft/kiota/http/OkHttpRequestAdapter.java b/components/http/okHttp/src/main/java/com/microsoft/kiota/http/OkHttpRequestAdapter.java index dc4c00402..744f73c6c 100644 --- a/components/http/okHttp/src/main/java/com/microsoft/kiota/http/OkHttpRequestAdapter.java +++ b/components/http/okHttp/src/main/java/com/microsoft/kiota/http/OkHttpRequestAdapter.java @@ -37,7 +37,6 @@ import java.io.IOException; import java.io.InputStream; import java.math.BigDecimal; -import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; import java.time.LocalDate; @@ -846,7 +845,7 @@ private void setBaseUrlForRequestInformation(@Nonnull final RequestInformation r try (final Scope scope = span.makeCurrent()) { this.authProvider.authenticateRequest(requestInfo, null); return (T) getRequestFromRequestInformation(requestInfo, span, span); - } catch (URISyntaxException | MalformedURLException ex) { + } catch (URISyntaxException | IOException ex) { span.recordException(ex); throw new RuntimeException(ex); } finally { @@ -862,13 +861,13 @@ private void setBaseUrlForRequestInformation(@Nonnull final RequestInformation r * @param spanForAttributes the span for the attributes. * @return the created request instance. * @throws URISyntaxException if the URI is invalid. - * @throws MalformedURLException if the URL is invalid. + * @throws IOException if the URL is invalid. */ protected @Nonnull Request getRequestFromRequestInformation( @Nonnull final RequestInformation requestInfo, @Nonnull final Span parentSpan, @Nonnull final Span spanForAttributes) - throws URISyntaxException, MalformedURLException { + throws URISyntaxException, IOException { final Span span = GlobalOpenTelemetry.getTracer(obsOptions.getTracerInstrumentationName()) .spanBuilder("getRequestFromRequestInformation") @@ -907,30 +906,23 @@ public MediaType contentType() { @Override public long contentLength() throws IOException { - long length; final Set contentLength = requestInfo.headers.getOrDefault( contentLengthHeaderKey, new HashSet<>()); - if (contentLength.isEmpty() - && requestInfo.content - instanceof ByteArrayInputStream) { + if (!contentLength.isEmpty()) { + return Long.parseLong( + contentLength.toArray(new String[] {})[0]); + } + // super.contentLength() is not relied on since it defaults to + // -1L, causing wrong telemetry added to the attributes. + if (requestInfo.content instanceof ByteArrayInputStream) { final ByteArrayInputStream contentStream = (ByteArrayInputStream) requestInfo.content; - length = contentStream.available(); - } else { - length = - Long.parseLong( - contentLength.toArray(new String[] {})[0]); - } - if (length <= 0) { - length = super.contentLength(); - } - if (length > 0) { - spanForAttributes.setAttribute( - HttpIncubatingAttributes.HTTP_REQUEST_BODY_SIZE, - length); + // using available() on a byte-array backed input stream is + // reliable because array size is defined. + return contentStream.available(); } - return length; + return super.contentLength(); } @Override @@ -967,7 +959,18 @@ public void writeTo(@Nonnull BufferedSink sink) throws IOException { requestBuilder.tag(obsOptions.getType(), obsOptions); } requestBuilder.tag(Span.class, parentSpan); - return requestBuilder.build(); + final Request request = requestBuilder.build(); + if (request != null) { + RequestBody requestBody = request.body(); + if (requestBody != null) { + long contentLength = requestBody.contentLength(); + if (contentLength >= 0) { + spanForAttributes.setAttribute( + HttpIncubatingAttributes.HTTP_REQUEST_BODY_SIZE, contentLength); + } + } + } + return request; } finally { span.end(); } diff --git a/components/http/okHttp/src/test/java/com/microsoft/kiota/http/OkHttpRequestAdapterTest.java b/components/http/okHttp/src/test/java/com/microsoft/kiota/http/OkHttpRequestAdapterTest.java index 5905e2bcf..3a2185915 100644 --- a/components/http/okHttp/src/test/java/com/microsoft/kiota/http/OkHttpRequestAdapterTest.java +++ b/components/http/okHttp/src/test/java/com/microsoft/kiota/http/OkHttpRequestAdapterTest.java @@ -301,22 +301,21 @@ void getRequestFromRequestInformationHasCorrectContentLength_JsonPayload() throw new ByteArrayInputStream( "{\"name\":\"value\",\"array\":[\"1\",\"2\",\"3\"]}" .getBytes(StandardCharsets.UTF_8)); - requestInformation.setStreamContent(content, "application/octet-stream"); + requestInformation.setStreamContent(content, "application/json"); requestInformation.httpMethod = HttpMethod.PUT; - requestInformation.headers.tryAdd("Content-Length", String.valueOf(content.available())); + final var contentLength = content.available(); + requestInformation.headers.tryAdd("Content-Length", String.valueOf(contentLength)); final var adapter = new OkHttpRequestAdapter(authenticationProviderMock); final var request = adapter.getRequestFromRequestInformation( requestInformation, mock(Span.class), mock(Span.class)); - assertEquals( - String.valueOf(requestInformation.content.available()), - request.headers().get("Content-Length")); - assertEquals("application/octet-stream", request.headers().get("Content-Type")); + assertEquals(String.valueOf(contentLength), request.headers().get("Content-Length")); + assertEquals("application/json", request.headers().get("Content-Type")); assertNotNull(request.body()); - assertEquals(request.body().contentLength(), requestInformation.content.available()); - assertEquals(request.body().contentType(), MediaType.parse("application/octet-stream")); + assertEquals(request.body().contentLength(), contentLength); + assertEquals(request.body().contentType(), MediaType.parse("application/json")); } @Test @@ -327,7 +326,8 @@ void getRequestFromRequestInformationIncludesContentLength_FilePayload() throws requestInformation.setUri(new URI("https://localhost")); requestInformation.httpMethod = HttpMethod.PUT; - requestInformation.headers.add("Content-Length", String.valueOf(testFile.length())); + final var contentLength = testFile.length(); + requestInformation.headers.add("Content-Length", String.valueOf(contentLength)); try (FileInputStream content = new FileInputStream(testFile)) { requestInformation.setStreamContent(content, "application/octet-stream"); @@ -336,16 +336,82 @@ void getRequestFromRequestInformationIncludesContentLength_FilePayload() throws adapter.getRequestFromRequestInformation( requestInformation, mock(Span.class), mock(Span.class)); - assertEquals( - String.valueOf(requestInformation.content.available()), - request.headers().get("Content-Length")); + assertEquals(String.valueOf(contentLength), request.headers().get("Content-Length")); assertEquals("application/octet-stream", request.headers().get("Content-Type")); assertNotNull(request.body()); - assertEquals(request.body().contentLength(), requestInformation.content.available()); + assertEquals(request.body().contentLength(), contentLength); assertEquals(request.body().contentType(), MediaType.parse("application/octet-stream")); } } + @Test + void getRequestFromRequestInformationWithoutContentLengthOverrideForStreamBody() + throws Exception { + final var authenticationProviderMock = mock(AuthenticationProvider.class); + final var testFile = new File("./src/test/resources/helloWorld.txt"); + final var requestInformation = new RequestInformation(); + + requestInformation.setUri(new URI("https://localhost")); + requestInformation.httpMethod = HttpMethod.PUT; + try (FileInputStream content = new FileInputStream(testFile)) { + requestInformation.setStreamContent(content, "application/octet-stream"); + + final var adapter = new OkHttpRequestAdapter(authenticationProviderMock); + final var request = + adapter.getRequestFromRequestInformation( + requestInformation, mock(Span.class), mock(Span.class)); + + assertEquals("application/octet-stream", request.headers().get("Content-Type")); + assertNotNull(request.body()); + assertEquals(-1L, request.body().contentLength()); + assertEquals(request.body().contentType(), MediaType.parse("application/octet-stream")); + } + } + + @Test + void getRequestFromRequestInformationWithoutContentLengthOverrideForJsonPayload() + throws Exception { + final var authenticationProviderMock = mock(AuthenticationProvider.class); + final var requestInformation = new RequestInformation(); + requestInformation.setUri(new URI("https://localhost")); + ByteArrayInputStream content = + new ByteArrayInputStream( + "{\"name\":\"value\",\"array\":[\"1\",\"2\",\"3\"]}" + .getBytes(StandardCharsets.UTF_8)); + requestInformation.setStreamContent(content, "application/json"); + requestInformation.httpMethod = HttpMethod.PUT; + final var contentLength = content.available(); + + final var adapter = new OkHttpRequestAdapter(authenticationProviderMock); + final var request = + adapter.getRequestFromRequestInformation( + requestInformation, mock(Span.class), mock(Span.class)); + + assertEquals("application/json", request.headers().get("Content-Type")); + assertNotNull(request.body()); + assertEquals(contentLength, request.body().contentLength()); + assertEquals(MediaType.parse("application/json"), request.body().contentType()); + } + + @Test + void getRequestFromRequestInformationWithoutContentLengthOverrideWithEmptyPayload() + throws Exception { + final var authenticationProviderMock = mock(AuthenticationProvider.class); + final var requestInformation = new RequestInformation(); + requestInformation.setUri(new URI("https://localhost")); + ByteArrayInputStream content = new ByteArrayInputStream(new byte[0]); + requestInformation.httpMethod = HttpMethod.PUT; + requestInformation.content = content; + + final var adapter = new OkHttpRequestAdapter(authenticationProviderMock); + final var request = + adapter.getRequestFromRequestInformation( + requestInformation, mock(Span.class), mock(Span.class)); + + assertNull(request.headers().get("Content-Type")); + assertEquals(0, request.body().contentLength()); + } + public static OkHttpClient getMockClient(final Response response) throws IOException { final OkHttpClient mockClient = mock(OkHttpClient.class); final Call remoteCall = mock(Call.class); diff --git a/gradle.properties b/gradle.properties index 38409fd06..1750affd7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -26,8 +26,8 @@ org.gradle.caching=true mavenGroupId = com.microsoft.kiota mavenMajorVersion = 1 mavenMinorVersion = 1 -mavenPatchVersion = 4 -mavenArtifactSuffix = +mavenPatchVersion = 5 +mavenArtifactSuffix = #These values are used to run functional tests #If you wish to run the functional tests, edit the gradle.properties