From 1a7caef6ccd016a29206ef8b4b6d54dd2a6f6306 Mon Sep 17 00:00:00 2001 From: Javier Aliaga Date: Thu, 28 May 2026 19:08:39 +0200 Subject: [PATCH] feat: attach SDK User-Agent in DaprInvokeHttpClient and clarify DaprBodyPublishers.json scope (#1758) newRequestBuilder now sets the User-Agent header to the SDK version, matching what the deprecated invokeMethod path emitted via DaprHttp. This is the only default the SDK can supply that callers cannot, and it is useful for runtime side triage. Other implicit invokeMethod behaviors (Content-Type default, request-id, error mapping, trace propagation) are intentionally left to the caller, since exposing the native HttpClient is the point of the migration. Also document on DaprBodyPublishers.json that the helper uses the SDK's default object serializer and is not a wrapper around a configured custom DaprObjectSerializer. Its only contribution over a plain ofByteArray call is guaranteeing a length-known BodyPublisher, which keeps the JDK on Content-Length framing and sidesteps the Transfer-Encoding: chunked race reported under load. Signed-off-by: Javier Aliaga (cherry picked from commit 43cf7c651ea01eafd1ac61ae744a617227a1bc9e) --- .../main/java/io/dapr/client/DaprBodyPublishers.java | 12 ++++++++++++ .../java/io/dapr/client/DaprInvokeHttpClient.java | 11 ++++++++--- .../io/dapr/client/DaprInvokeHttpClientTest.java | 11 +++++++++++ 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/sdk/src/main/java/io/dapr/client/DaprBodyPublishers.java b/sdk/src/main/java/io/dapr/client/DaprBodyPublishers.java index 37651e30b..4c48bef91 100644 --- a/sdk/src/main/java/io/dapr/client/DaprBodyPublishers.java +++ b/sdk/src/main/java/io/dapr/client/DaprBodyPublishers.java @@ -59,6 +59,18 @@ private DaprBodyPublishers() { *

Callers are still responsible for setting an appropriate * {@code Content-Type} header (typically {@code application/json}). * + *

This helper is a convenience for the default-serializer case. It does + * not honor a custom {@link io.dapr.serializer.DaprObjectSerializer} + * configured on the {@link DaprClientBuilder}. Callers with a custom serializer + * should serialize the value themselves and wrap the resulting bytes: + *

{@code
+   * byte[] bytes = mySerializer.serialize(value);
+   * BodyPublisher body = HttpRequest.BodyPublishers.ofByteArray(bytes);
+   * }
+ * The only behavior this helper adds over a direct {@code ofByteArray} call is + * choosing a length-known {@link BodyPublisher} so the JDK emits + * {@code Content-Length} rather than {@code Transfer-Encoding: chunked}. + * * @param value object to serialize; {@code null} yields an empty body. * @return a body publisher carrying the JSON-encoded bytes. * @throws UncheckedIOException if serialization fails. diff --git a/sdk/src/main/java/io/dapr/client/DaprInvokeHttpClient.java b/sdk/src/main/java/io/dapr/client/DaprInvokeHttpClient.java index 34564dcbb..f13edb6cf 100644 --- a/sdk/src/main/java/io/dapr/client/DaprInvokeHttpClient.java +++ b/sdk/src/main/java/io/dapr/client/DaprInvokeHttpClient.java @@ -13,6 +13,8 @@ package io.dapr.client; +import io.dapr.utils.Version; + import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; @@ -100,8 +102,9 @@ public URI baseUri() { /** * Creates an {@link HttpRequest.Builder} pre-bound to the Dapr invoke URL for the - * configured app id, with the {@code dapr-api-token} header attached (when one is - * configured) and the SDK's HTTP read timeout applied. + * configured app id, with the SDK {@code User-Agent} header attached, the + * {@code dapr-api-token} header attached (when one is configured) and the SDK's + * HTTP read timeout applied. * *

The {@code relativePath} is resolved against {@link #baseUri()} via * {@link URI#resolve(String)}. Per {@link URI#resolve(String)} semantics, a leading @@ -113,7 +116,9 @@ public URI baseUri() { */ public HttpRequest.Builder newRequestBuilder(String relativePath) { Objects.requireNonNull(relativePath, "relativePath"); - HttpRequest.Builder builder = HttpRequest.newBuilder().uri(baseUri.resolve(relativePath)); + HttpRequest.Builder builder = HttpRequest.newBuilder() + .uri(baseUri.resolve(relativePath)) + .header(Headers.DAPR_USER_AGENT, Version.getSdkVersion()); if (daprApiToken != null && !daprApiToken.isEmpty()) { builder.header(Headers.DAPR_API_TOKEN, daprApiToken); } diff --git a/sdk/src/test/java/io/dapr/client/DaprInvokeHttpClientTest.java b/sdk/src/test/java/io/dapr/client/DaprInvokeHttpClientTest.java index 826f9583f..c3c0514a0 100644 --- a/sdk/src/test/java/io/dapr/client/DaprInvokeHttpClientTest.java +++ b/sdk/src/test/java/io/dapr/client/DaprInvokeHttpClientTest.java @@ -13,6 +13,7 @@ package io.dapr.client; +import io.dapr.utils.Version; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -80,6 +81,16 @@ public void newRequestBuilder_resolvesRelativePathAgainstBaseUri() { request.uri().toString()); } + @Test + public void newRequestBuilder_attachesSdkUserAgentHeader() { + DaprInvokeHttpClient invoker = new DaprInvokeHttpClient(httpClient, BASE_URI, null, null); + + HttpRequest request = invoker.newRequestBuilder("orders").GET().build(); + + assertEquals(Version.getSdkVersion(), + request.headers().firstValue(Headers.DAPR_USER_AGENT).orElse(null)); + } + @Test public void newRequestBuilder_attachesApiTokenHeaderWhenConfigured() { DaprInvokeHttpClient invoker = new DaprInvokeHttpClient(httpClient, BASE_URI, "xyz", null);