From 612c090f16b425359f1e3521ad1c77e86fecbb81 Mon Sep 17 00:00:00 2001 From: Javier Aliaga Date: Tue, 12 May 2026 15:48:07 +0200 Subject: [PATCH 1/3] chore: rewrite invoke/http example to use native HttpClient MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The DaprClient.invokeMethod wrappers were deprecated by #1666. Rewrite the invoke/http sample to use java.net.http.HttpClient through the Dapr sidecar, demonstrating both URL forms accepted by the sidecar — the dapr-app-id header against the sidecar base URL, and the explicit /v1.0/invoke//method/ path. Update the matching README snippet and reduce expected_stdout_lines to 'Done' — the previous expected lines never matched what DemoService returns (a timestamp). Signed-off-by: Javier Aliaga --- .../examples/invoke/http/InvokeClient.java | 63 ++++++++++++---- .../io/dapr/examples/invoke/http/README.md | 74 ++++++++++++------- 2 files changed, 97 insertions(+), 40 deletions(-) diff --git a/examples/src/main/java/io/dapr/examples/invoke/http/InvokeClient.java b/examples/src/main/java/io/dapr/examples/invoke/http/InvokeClient.java index 3f00cb680c..b8773e8f95 100644 --- a/examples/src/main/java/io/dapr/examples/invoke/http/InvokeClient.java +++ b/examples/src/main/java/io/dapr/examples/invoke/http/InvokeClient.java @@ -13,9 +13,12 @@ package io.dapr.examples.invoke.http; -import io.dapr.client.DaprClient; -import io.dapr.client.DaprClientBuilder; -import io.dapr.client.domain.HttpExtension; +import io.dapr.config.Properties; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; /** * 1. Build and install jars: @@ -24,6 +27,15 @@ * 3. Send messages to the server: * dapr run -- java -jar target/dapr-java-sdk-examples-exec.jar \ * io.dapr.examples.invoke.http.InvokeClient 'message one' 'message two' + * + *

This example demonstrates calling another Dapr-enabled application using a + * native HTTP client. Two equivalent URL forms are supported by the Dapr sidecar: + *

    + *
  1. Sending the request to the sidecar's base URL with a {@code dapr-app-id} + * header that identifies the target app.
  2. + *
  3. Sending the request to the sidecar's {@code /v1.0/invoke/<app-id>/method/<method>} + * path, with no extra header.
  4. + *
*/ public class InvokeClient { @@ -32,22 +44,47 @@ public class InvokeClient { */ private static final String SERVICE_APP_ID = "invokedemo"; + /** + * Method on the target service to invoke. + */ + private static final String METHOD = "say"; + /** * Starts the invoke client. * * @param args Messages to be sent as request for the invoke API. */ public static void main(String[] args) throws Exception { - try (DaprClient client = (new DaprClientBuilder()).build()) { - for (String message : args) { - byte[] response = client.invokeMethod(SERVICE_APP_ID, "say", message, HttpExtension.POST, null, - byte[].class).block(); - System.out.println(new String(response)); - } - - // This is an example, so for simplicity we are just exiting here. - // Normally a dapr app would be a web service and not exit main. - System.out.println("Done"); + int port = Properties.HTTP_PORT.get(); + String sidecarBase = "http://localhost:" + port; + + HttpClient httpClient = HttpClient.newHttpClient(); + + for (String message : args) { + // Form 1: dapr-app-id header against the sidecar's base URL. + HttpRequest headerRequest = HttpRequest.newBuilder() + .uri(URI.create(sidecarBase + "/" + METHOD)) + .header("Content-Type", "application/json") + .header("dapr-app-id", SERVICE_APP_ID) + .POST(HttpRequest.BodyPublishers.ofString(message)) + .build(); + HttpResponse headerResponse = + httpClient.send(headerRequest, HttpResponse.BodyHandlers.ofByteArray()); + System.out.println(new String(headerResponse.body())); + + // Form 2: sidecar invoke path. + HttpRequest pathRequest = HttpRequest.newBuilder() + .uri(URI.create(sidecarBase + "/v1.0/invoke/" + SERVICE_APP_ID + "/method/" + METHOD)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(message)) + .build(); + HttpResponse pathResponse = + httpClient.send(pathRequest, HttpResponse.BodyHandlers.ofByteArray()); + System.out.println(new String(pathResponse.body())); } + + // This is an example, so for simplicity we are just exiting here. + // Normally a dapr app would be a web service and not exit main. + System.out.println("Done"); } } diff --git a/examples/src/main/java/io/dapr/examples/invoke/http/README.md b/examples/src/main/java/io/dapr/examples/invoke/http/README.md index 5636656ca6..5ba4772b5f 100644 --- a/examples/src/main/java/io/dapr/examples/invoke/http/README.md +++ b/examples/src/main/java/io/dapr/examples/invoke/http/README.md @@ -8,9 +8,9 @@ This sample includes: Visit [this](https://docs.dapr.io/developing-applications/building-blocks/service-invocation/service-invocation-overview/) link for more information about Dapr and service invocation. -## Remote invocation using the Java-SDK +## Remote invocation using a native HTTP client -This sample uses the Client provided in Dapr Java SDK invoking a remote method. +This sample uses a native Java `HttpClient` to invoke a method on another Dapr-enabled application via the Dapr sidecar. The previous SDK-provided `DaprClient.invokeMethod` wrappers are deprecated; calling the sidecar directly is the recommended approach. ## Pre-requisites @@ -121,39 +121,60 @@ Once running, the ExposerService is now ready to be invoked by Dapr. ### Running the InvokeClient sample -The Invoke client sample uses the Dapr SDK for invoking the remote method. The main method declares a Dapr Client using the `DaprClientBuilder` class. Notice that [DaprClientBuilder](https://github.com/dapr/java-sdk/blob/master/sdk/src/main/java/io/dapr/client/DaprClientBuilder.java) can receive two optional serializers: `withObjectSerializer()` is for Dapr's sent and received objects, and `withStateSerializer()` is for objects to be persisted. It needs to know the method name to invoke as well as the application id for the remote application. This example, we stick to the [default serializer](https://github.com/dapr/java-sdk/blob/master/sdk/src/main/java/io/dapr/serializer/DefaultObjectSerializer.java). In `InvokeClient.java` file, you will find the `InvokeClient` class and the `main` method. See the code snippet below: +The Invoke client sample uses a native Java `HttpClient` to call the remote method through the Dapr sidecar. The Dapr sidecar accepts two equivalent URL forms for service invocation; this sample demonstrates both for each message: + +1. Sending the request to the sidecar's base URL with a `dapr-app-id` header identifying the target app. +2. Sending the request to the sidecar's `/v1.0/invoke//method/` path. + +In `InvokeClient.java` file, you will find the `InvokeClient` class and the `main` method. See the code snippet below: ```java public class InvokeClient { -private static final String SERVICE_APP_ID = "invokedemo"; -///... -public static void main(String[] args) throws Exception { - try (DaprClient client = (new DaprClientBuilder()).build()) { - for (String message : args) { - byte[] response = client.invokeMethod(SERVICE_APP_ID, "say", message, HttpExtension.POST, null, - byte[].class).block(); - System.out.println(new String(response)); - } + private static final String SERVICE_APP_ID = "invokedemo"; + private static final String METHOD = "say"; - // This is an example, so for simplicity we are just exiting here. - // Normally a dapr app would be a web service and not exit main. - System.out.println("Done"); + public static void main(String[] args) throws Exception { + int port = Properties.HTTP_PORT.get(); + String sidecarBase = "http://localhost:" + port; + + HttpClient httpClient = HttpClient.newHttpClient(); + + for (String message : args) { + // Form 1: dapr-app-id header against the sidecar's base URL. + HttpRequest headerRequest = HttpRequest.newBuilder() + .uri(URI.create(sidecarBase + "/" + METHOD)) + .header("Content-Type", "application/json") + .header("dapr-app-id", SERVICE_APP_ID) + .POST(HttpRequest.BodyPublishers.ofString(message)) + .build(); + HttpResponse headerResponse = + httpClient.send(headerRequest, HttpResponse.BodyHandlers.ofByteArray()); + System.out.println(new String(headerResponse.body())); + + // Form 2: sidecar invoke path. + HttpRequest pathRequest = HttpRequest.newBuilder() + .uri(URI.create(sidecarBase + "/v1.0/invoke/" + SERVICE_APP_ID + "/method/" + METHOD)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(message)) + .build(); + HttpResponse pathResponse = + httpClient.send(pathRequest, HttpResponse.BodyHandlers.ofByteArray()); + System.out.println(new String(pathResponse.body())); } + + System.out.println("Done"); } -///... } ``` -The class knows the app id for the remote application. It uses the the static `Dapr.getInstance().invokeMethod` method to invoke the remote method defining the parameters: The verb, application id, method name, and proper data and metadata, as well as the type of the expected return type. The returned payload for this method invocation is plain text and not a [JSON String](https://www.w3schools.com/js/js_json_datatypes.asp), so we expect `byte[]` to get the raw response and not try to deserialize it. - +The `dapr-app-id` header (Form 1) routes the request to the target app named by `SERVICE_APP_ID`. The `/v1.0/invoke//method/` path (Form 2) achieves the same routing through the sidecar API directly. Both forms call the remote `say` method and print its response. + Execute the follow script in order to run the InvokeClient example, passing two messages for the remote method: -Finally, the console for `invokeclient` should output: +Finally, the console for `invokeclient` should output two timestamps per message — one from each URL form — followed by `Done`. The exact timestamps come from the `say` method on `DemoService`. For example: ```text -"message one" received - -"message two" received - +2026-05-12 13:45:00.123 +2026-05-12 13:45:00.456 +2026-05-12 13:45:00.789 +2026-05-12 13:45:01.012 Done - ``` For more details on Dapr Spring Boot integration, please refer to [Dapr Spring Boot](../../DaprApplication.java) Application implementation. From 82ba7fcc4f72d07029b0c32cf13b9374dc8d8cc7 Mon Sep 17 00:00:00 2001 From: Javier Aliaga Date: Thu, 14 May 2026 10:36:14 +0200 Subject: [PATCH 2/3] feat: add DaprClient.invokeHttpClient(appId) factory Provides an SDK-native successor to the invokeMethod APIs deprecated by #1666. DaprClient.invokeHttpClient(appId) returns a DaprInvokeHttpClient pre-bound to {daprHttpEndpoint}/v1.0/invoke/{appId}/method/ that reuses the SDK's shared java.net.http.HttpClient and attaches the dapr-api-token header when configured. Update the invoke/http example and README to demonstrate the new helper alongside the raw dapr-app-id header form. Signed-off-by: Javier Aliaga --- .../client/ObservationDaprClient.java | 6 + .../client/ObservationDaprClientTest.java | 20 +- .../examples/invoke/http/InvokeClient.java | 64 ++++--- .../io/dapr/examples/invoke/http/README.md | 69 +++---- .../main/java/io/dapr/client/DaprClient.java | 13 ++ .../java/io/dapr/client/DaprClientImpl.java | 15 ++ .../main/java/io/dapr/client/DaprHttp.java | 16 ++ .../io/dapr/client/DaprInvokeHttpClient.java | 140 ++++++++++++++ .../io/dapr/client/DaprClientHttpTest.java | 42 +++++ .../dapr/client/DaprInvokeHttpClientTest.java | 171 ++++++++++++++++++ 10 files changed, 491 insertions(+), 65 deletions(-) create mode 100644 sdk/src/main/java/io/dapr/client/DaprInvokeHttpClient.java create mode 100644 sdk/src/test/java/io/dapr/client/DaprInvokeHttpClientTest.java diff --git a/dapr-spring/dapr-spring-boot-observation/src/main/java/io/dapr/spring/observation/client/ObservationDaprClient.java b/dapr-spring/dapr-spring-boot-observation/src/main/java/io/dapr/spring/observation/client/ObservationDaprClient.java index 575a1887fc..5fe10a3a05 100644 --- a/dapr-spring/dapr-spring-boot-observation/src/main/java/io/dapr/spring/observation/client/ObservationDaprClient.java +++ b/dapr-spring/dapr-spring-boot-observation/src/main/java/io/dapr/spring/observation/client/ObservationDaprClient.java @@ -14,6 +14,7 @@ package io.dapr.spring.observation.client; import io.dapr.client.DaprClient; +import io.dapr.client.DaprInvokeHttpClient; import io.dapr.client.domain.BulkPublishRequest; import io.dapr.client.domain.BulkPublishResponse; import io.dapr.client.domain.ConfigurationItem; @@ -334,6 +335,11 @@ public Mono invokeMethod(InvokeMethodRequest invokeMethodRequest, TypeRef return delegate.invokeMethod(invokeMethodRequest, type); } + @Override + public DaprInvokeHttpClient invokeHttpClient(String appId) { + return delegate.invokeHttpClient(appId); + } + // ------------------------------------------------------------------------- // Bindings // ------------------------------------------------------------------------- diff --git a/dapr-spring/dapr-spring-boot-observation/src/test/java/io/dapr/spring/observation/client/ObservationDaprClientTest.java b/dapr-spring/dapr-spring-boot-observation/src/test/java/io/dapr/spring/observation/client/ObservationDaprClientTest.java index e37a332df2..b711f5026c 100644 --- a/dapr-spring/dapr-spring-boot-observation/src/test/java/io/dapr/spring/observation/client/ObservationDaprClientTest.java +++ b/dapr-spring/dapr-spring-boot-observation/src/test/java/io/dapr/spring/observation/client/ObservationDaprClientTest.java @@ -14,13 +14,10 @@ package io.dapr.spring.observation.client; import io.dapr.client.DaprClient; -import io.dapr.client.domain.DeleteStateRequest; -import io.dapr.client.domain.GetSecretRequest; -import io.dapr.client.domain.GetStateRequest; +import io.dapr.client.DaprInvokeHttpClient; import io.dapr.client.domain.InvokeBindingRequest; import io.dapr.client.domain.PublishEventRequest; import io.dapr.client.domain.ScheduleJobRequest; -import io.dapr.client.domain.State; import io.micrometer.observation.tck.TestObservationRegistry; import io.micrometer.observation.tck.TestObservationRegistryAssert; import org.junit.jupiter.api.BeforeEach; @@ -33,7 +30,6 @@ import reactor.core.publisher.Mono; import java.util.List; -import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -378,4 +374,18 @@ void deprecatedInvokeMethodDoesNotCreateSpan() { // Registry must be empty — no spans for deprecated methods TestObservationRegistryAssert.assertThat(registry).doesNotHaveAnyObservation(); } + + @Test + @DisplayName("invokeHttpClient delegates without creating a span") + void invokeHttpClientDelegatesWithoutSpan() { + DaprInvokeHttpClient stub = org.mockito.Mockito.mock(DaprInvokeHttpClient.class); + when(delegate.invokeHttpClient("orderprocessor")).thenReturn(stub); + + DaprInvokeHttpClient result = client.invokeHttpClient("orderprocessor"); + + assertThat(result).isSameAs(stub); + verify(delegate).invokeHttpClient("orderprocessor"); + // Synchronous factory — no Mono/Flux to observe, so no span is expected. + TestObservationRegistryAssert.assertThat(registry).doesNotHaveAnyObservation(); + } } diff --git a/examples/src/main/java/io/dapr/examples/invoke/http/InvokeClient.java b/examples/src/main/java/io/dapr/examples/invoke/http/InvokeClient.java index b8773e8f95..b97d24312c 100644 --- a/examples/src/main/java/io/dapr/examples/invoke/http/InvokeClient.java +++ b/examples/src/main/java/io/dapr/examples/invoke/http/InvokeClient.java @@ -13,6 +13,9 @@ package io.dapr.examples.invoke.http; +import io.dapr.client.DaprClient; +import io.dapr.client.DaprClientBuilder; +import io.dapr.client.DaprInvokeHttpClient; import io.dapr.config.Properties; import java.net.URI; @@ -28,13 +31,14 @@ * dapr run -- java -jar target/dapr-java-sdk-examples-exec.jar \ * io.dapr.examples.invoke.http.InvokeClient 'message one' 'message two' * - *

This example demonstrates calling another Dapr-enabled application using a - * native HTTP client. Two equivalent URL forms are supported by the Dapr sidecar: + *

This example demonstrates calling another Dapr-enabled application over HTTP. + * Two equivalent approaches are shown: *

    - *
  1. Sending the request to the sidecar's base URL with a {@code dapr-app-id} - * header that identifies the target app.
  2. - *
  3. Sending the request to the sidecar's {@code /v1.0/invoke/<app-id>/method/<method>} - * path, with no extra header.
  4. + *
  5. {@link DaprClient#invokeHttpClient(String)} — an SDK-provided {@link java.net.http.HttpClient} + * wrapper pre-bound to the sidecar's {@code /v1.0/invoke/<app-id>/method/} prefix, + * with the {@code dapr-api-token} header attached when configured.
  6. + *
  7. A raw {@link java.net.http.HttpClient} sending the request to the sidecar's base URL + * with a {@code dapr-app-id} header identifying the target app — no SDK helper required.
  8. *
*/ public class InvokeClient { @@ -55,32 +59,34 @@ public class InvokeClient { * @param args Messages to be sent as request for the invoke API. */ public static void main(String[] args) throws Exception { - int port = Properties.HTTP_PORT.get(); - String sidecarBase = "http://localhost:" + port; + try (DaprClient daprClient = new DaprClientBuilder().build()) { + DaprInvokeHttpClient invoker = daprClient.invokeHttpClient(SERVICE_APP_ID); - HttpClient httpClient = HttpClient.newHttpClient(); + int port = Properties.HTTP_PORT.get(); + String sidecarBase = "http://localhost:" + port; + HttpClient rawHttpClient = HttpClient.newHttpClient(); - for (String message : args) { - // Form 1: dapr-app-id header against the sidecar's base URL. - HttpRequest headerRequest = HttpRequest.newBuilder() - .uri(URI.create(sidecarBase + "/" + METHOD)) - .header("Content-Type", "application/json") - .header("dapr-app-id", SERVICE_APP_ID) - .POST(HttpRequest.BodyPublishers.ofString(message)) - .build(); - HttpResponse headerResponse = - httpClient.send(headerRequest, HttpResponse.BodyHandlers.ofByteArray()); - System.out.println(new String(headerResponse.body())); + for (String message : args) { + // Form 1: SDK helper — paths resolve against /v1.0/invoke//method/. + HttpRequest sdkRequest = invoker.newRequestBuilder(METHOD) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(message)) + .build(); + HttpResponse sdkResponse = + invoker.send(sdkRequest, HttpResponse.BodyHandlers.ofByteArray()); + System.out.println(new String(sdkResponse.body())); - // Form 2: sidecar invoke path. - HttpRequest pathRequest = HttpRequest.newBuilder() - .uri(URI.create(sidecarBase + "/v1.0/invoke/" + SERVICE_APP_ID + "/method/" + METHOD)) - .header("Content-Type", "application/json") - .POST(HttpRequest.BodyPublishers.ofString(message)) - .build(); - HttpResponse pathResponse = - httpClient.send(pathRequest, HttpResponse.BodyHandlers.ofByteArray()); - System.out.println(new String(pathResponse.body())); + // Form 2: raw HttpClient + dapr-app-id header against the sidecar's base URL. + HttpRequest headerRequest = HttpRequest.newBuilder() + .uri(URI.create(sidecarBase + "/" + METHOD)) + .header("Content-Type", "application/json") + .header("dapr-app-id", SERVICE_APP_ID) + .POST(HttpRequest.BodyPublishers.ofString(message)) + .build(); + HttpResponse headerResponse = + rawHttpClient.send(headerRequest, HttpResponse.BodyHandlers.ofByteArray()); + System.out.println(new String(headerResponse.body())); + } } // This is an example, so for simplicity we are just exiting here. diff --git a/examples/src/main/java/io/dapr/examples/invoke/http/README.md b/examples/src/main/java/io/dapr/examples/invoke/http/README.md index 5ba4772b5f..dd99ae59e7 100644 --- a/examples/src/main/java/io/dapr/examples/invoke/http/README.md +++ b/examples/src/main/java/io/dapr/examples/invoke/http/README.md @@ -10,7 +10,12 @@ Visit [this](https://docs.dapr.io/developing-applications/building-blocks/servic ## Remote invocation using a native HTTP client -This sample uses a native Java `HttpClient` to invoke a method on another Dapr-enabled application via the Dapr sidecar. The previous SDK-provided `DaprClient.invokeMethod` wrappers are deprecated; calling the sidecar directly is the recommended approach. +This sample invokes a method on another Dapr-enabled application via the Dapr sidecar using `java.net.http.HttpClient`. The previous SDK-provided `DaprClient.invokeMethod` wrappers are deprecated; calling the sidecar directly is the recommended approach. + +Two equivalent approaches are demonstrated: + +1. `DaprClient.invokeHttpClient(appId)` — an SDK-provided wrapper that returns a pre-configured `HttpClient` bound to the sidecar's `/v1.0/invoke//method/` prefix, with the `dapr-api-token` header attached when configured. +2. A raw `java.net.http.HttpClient` sending the request to the sidecar's base URL with a `dapr-app-id` header identifying the target app — no SDK helper required. ## Pre-requisites @@ -121,10 +126,10 @@ Once running, the ExposerService is now ready to be invoked by Dapr. ### Running the InvokeClient sample -The Invoke client sample uses a native Java `HttpClient` to call the remote method through the Dapr sidecar. The Dapr sidecar accepts two equivalent URL forms for service invocation; this sample demonstrates both for each message: +The Invoke client sample calls the remote method through the Dapr sidecar using two equivalent approaches: -1. Sending the request to the sidecar's base URL with a `dapr-app-id` header identifying the target app. -2. Sending the request to the sidecar's `/v1.0/invoke//method/` path. +1. `DaprClient.invokeHttpClient(appId)` — an SDK-provided wrapper around `java.net.http.HttpClient` pre-bound to `/v1.0/invoke//method/`. +2. A raw `java.net.http.HttpClient` against the sidecar's base URL with a `dapr-app-id` header. In `InvokeClient.java` file, you will find the `InvokeClient` class and the `main` method. See the code snippet below: @@ -135,32 +140,34 @@ public class InvokeClient { private static final String METHOD = "say"; public static void main(String[] args) throws Exception { - int port = Properties.HTTP_PORT.get(); - String sidecarBase = "http://localhost:" + port; - - HttpClient httpClient = HttpClient.newHttpClient(); - - for (String message : args) { - // Form 1: dapr-app-id header against the sidecar's base URL. - HttpRequest headerRequest = HttpRequest.newBuilder() - .uri(URI.create(sidecarBase + "/" + METHOD)) - .header("Content-Type", "application/json") - .header("dapr-app-id", SERVICE_APP_ID) - .POST(HttpRequest.BodyPublishers.ofString(message)) - .build(); - HttpResponse headerResponse = - httpClient.send(headerRequest, HttpResponse.BodyHandlers.ofByteArray()); - System.out.println(new String(headerResponse.body())); - - // Form 2: sidecar invoke path. - HttpRequest pathRequest = HttpRequest.newBuilder() - .uri(URI.create(sidecarBase + "/v1.0/invoke/" + SERVICE_APP_ID + "/method/" + METHOD)) - .header("Content-Type", "application/json") - .POST(HttpRequest.BodyPublishers.ofString(message)) - .build(); - HttpResponse pathResponse = - httpClient.send(pathRequest, HttpResponse.BodyHandlers.ofByteArray()); - System.out.println(new String(pathResponse.body())); + try (DaprClient daprClient = new DaprClientBuilder().build()) { + DaprInvokeHttpClient invoker = daprClient.invokeHttpClient(SERVICE_APP_ID); + + int port = Properties.HTTP_PORT.get(); + String sidecarBase = "http://localhost:" + port; + HttpClient rawHttpClient = HttpClient.newHttpClient(); + + for (String message : args) { + // Form 1: SDK helper — paths resolve against /v1.0/invoke//method/. + HttpRequest sdkRequest = invoker.newRequestBuilder(METHOD) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(message)) + .build(); + HttpResponse sdkResponse = + invoker.send(sdkRequest, HttpResponse.BodyHandlers.ofByteArray()); + System.out.println(new String(sdkResponse.body())); + + // Form 2: raw HttpClient + dapr-app-id header against the sidecar's base URL. + HttpRequest headerRequest = HttpRequest.newBuilder() + .uri(URI.create(sidecarBase + "/" + METHOD)) + .header("Content-Type", "application/json") + .header("dapr-app-id", SERVICE_APP_ID) + .POST(HttpRequest.BodyPublishers.ofString(message)) + .build(); + HttpResponse headerResponse = + rawHttpClient.send(headerRequest, HttpResponse.BodyHandlers.ofByteArray()); + System.out.println(new String(headerResponse.body())); + } } System.out.println("Done"); @@ -168,7 +175,7 @@ public class InvokeClient { } ``` -The `dapr-app-id` header (Form 1) routes the request to the target app named by `SERVICE_APP_ID`. The `/v1.0/invoke//method/` path (Form 2) achieves the same routing through the sidecar API directly. Both forms call the remote `say` method and print its response. +Form 1 uses `DaprClient.invokeHttpClient(SERVICE_APP_ID)` to obtain an HTTP client whose base URI already targets the desired app via the sidecar's invoke API. Form 2 sends the request directly to the sidecar's base URL and uses the `dapr-app-id` header to identify the target app. Both forms call the remote `say` method and print its response. Execute the follow script in order to run the InvokeClient example, passing two messages for the remote method: diff --git a/sdk/src/main/java/io/dapr/client/DaprClient.java b/sdk/src/main/java/io/dapr/client/DaprClient.java index 00acb18971..d57780ed23 100644 --- a/sdk/src/main/java/io/dapr/client/DaprClient.java +++ b/sdk/src/main/java/io/dapr/client/DaprClient.java @@ -353,6 +353,19 @@ Mono invokeMethod(String appId, String methodName, byte[] request, HttpE @Deprecated Mono invokeMethod(InvokeMethodRequest invokeMethodRequest, TypeRef type); + /** + * Creates an HTTP client pre-configured for Dapr service invocation against the given app id. + * + *

The returned client resolves relative paths against + * {@code {daprHttpEndpoint}/v1.0/invoke/{appId}/method/} and automatically attaches the + * {@code dapr-api-token} header when one is configured. It reuses the SDK's shared + * {@link java.net.http.HttpClient} instance. + * + * @param appId the application id to invoke. + * @return a {@link DaprInvokeHttpClient} bound to {@code appId}. + */ + DaprInvokeHttpClient invokeHttpClient(String appId); + /** * Invokes a Binding operation. * diff --git a/sdk/src/main/java/io/dapr/client/DaprClientImpl.java b/sdk/src/main/java/io/dapr/client/DaprClientImpl.java index 3adb3bc37f..a6b6bbbc31 100644 --- a/sdk/src/main/java/io/dapr/client/DaprClientImpl.java +++ b/sdk/src/main/java/io/dapr/client/DaprClientImpl.java @@ -130,6 +130,7 @@ import javax.annotation.Nonnull; import java.io.IOException; +import java.net.URI; import java.time.Duration; import java.time.Instant; import java.time.ZoneOffset; @@ -696,6 +697,20 @@ public Mono invokeMethod(InvokeMethodRequest invokeMethodRequest, TypeRef } } + @Override + public DaprInvokeHttpClient invokeHttpClient(String appId) { + if (appId == null || appId.trim().isEmpty()) { + throw new IllegalArgumentException("App Id cannot be null or empty."); + } + URI invokeBase = this.httpClient.getBaseUri() + .resolve("/" + DaprHttp.API_VERSION + "/invoke/" + appId + "/method/"); + return new DaprInvokeHttpClient( + this.httpClient.getHttpClient(), + invokeBase, + this.httpClient.getDaprApiToken(), + this.httpClient.getReadTimeout()); + } + private Mono getMonoForHttpResponse(TypeRef type, DaprHttp.Response r) { try { if (type == null) { diff --git a/sdk/src/main/java/io/dapr/client/DaprHttp.java b/sdk/src/main/java/io/dapr/client/DaprHttp.java index fd7accd998..e091a6587a 100644 --- a/sdk/src/main/java/io/dapr/client/DaprHttp.java +++ b/sdk/src/main/java/io/dapr/client/DaprHttp.java @@ -185,6 +185,22 @@ public int getStatusCode() { this.httpClient = httpClient; } + URI getBaseUri() { + return uri; + } + + String getDaprApiToken() { + return daprApiToken; + } + + Duration getReadTimeout() { + return readTimeout; + } + + HttpClient getHttpClient() { + return httpClient; + } + /** * Invokes an API asynchronously without payload that returns a text payload. * diff --git a/sdk/src/main/java/io/dapr/client/DaprInvokeHttpClient.java b/sdk/src/main/java/io/dapr/client/DaprInvokeHttpClient.java new file mode 100644 index 0000000000..171f49fe19 --- /dev/null +++ b/sdk/src/main/java/io/dapr/client/DaprInvokeHttpClient.java @@ -0,0 +1,140 @@ +/* + * Copyright 2026 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandler; +import java.time.Duration; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +/** + * An HTTP client pre-configured to invoke a specific Dapr application via the + * service invocation API. + * + *

Obtained via {@link DaprClient#invokeHttpClient(String)}. Relative paths + * passed to {@link #newRequestBuilder(String)} resolve against + * {@code {daprHttpEndpoint}/v1.0/invoke/{appId}/method/} and the configured + * {@code dapr-api-token} header is attached automatically when present. + * + *

Example: + *

{@code
+ * DaprInvokeHttpClient invoker = daprClient.invokeHttpClient("orderprocessor");
+ *
+ * HttpRequest request = invoker.newRequestBuilder("orders")
+ *     .header("Content-Type", "application/json")
+ *     .POST(HttpRequest.BodyPublishers.ofString(json))
+ *     .build();
+ *
+ * HttpResponse response = invoker.send(request, HttpResponse.BodyHandlers.ofString());
+ * }
+ * + *

This class is not {@link AutoCloseable}: the underlying {@link HttpClient} is + * managed by the SDK and shared across all clients created from a single + * {@link DaprClientBuilder}; closing the owning {@link DaprClient} releases it. + */ +public class DaprInvokeHttpClient { + + private final HttpClient httpClient; + private final URI baseUri; + private final String daprApiToken; + private final Duration readTimeout; + + DaprInvokeHttpClient(HttpClient httpClient, URI baseUri, String daprApiToken, Duration readTimeout) { + this.httpClient = Objects.requireNonNull(httpClient, "httpClient"); + this.baseUri = Objects.requireNonNull(baseUri, "baseUri"); + this.daprApiToken = daprApiToken; + this.readTimeout = readTimeout; + } + + /** + * Returns the underlying JDK {@link HttpClient}. Useful as an escape hatch when + * callers need full control over a request (for example to bypass the configured + * base URI for a one-off call). + * + * @return the shared underlying HTTP client. + */ + public HttpClient httpClient() { + return httpClient; + } + + /** + * Returns the base URI against which {@link #newRequestBuilder(String)} resolves + * relative paths. Always ends with a trailing slash, e.g. + * {@code http://localhost:3500/v1.0/invoke/orderprocessor/method/}. + * + * @return the resolved invoke base URI. + */ + public URI baseUri() { + return 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. + * + *

The {@code relativePath} is resolved against {@link #baseUri()} via + * {@link URI#resolve(String)}. Per {@link URI#resolve(String)} semantics, a leading + * slash replaces the entire path, so callers should typically pass a path + * without a leading slash (e.g. {@code "orders/42"}). + * + * @param relativePath path appended to the invoke prefix. + * @return a request builder ready to be customized and built. + */ + public HttpRequest.Builder newRequestBuilder(String relativePath) { + Objects.requireNonNull(relativePath, "relativePath"); + HttpRequest.Builder builder = HttpRequest.newBuilder().uri(baseUri.resolve(relativePath)); + if (daprApiToken != null && !daprApiToken.isEmpty()) { + builder.header(Headers.DAPR_API_TOKEN, daprApiToken); + } + if (readTimeout != null && !readTimeout.isZero() && !readTimeout.isNegative()) { + builder.timeout(readTimeout); + } + return builder; + } + + /** + * Sends a request synchronously using the underlying HTTP client. + * Equivalent to {@code httpClient().send(request, bodyHandler)}. + * + * @param request the request to send. + * @param bodyHandler handler for the response body. + * @param the response body type. + * @return the HTTP response. + * @throws IOException if an I/O error occurs. + * @throws InterruptedException if the operation is interrupted. + */ + public HttpResponse send(HttpRequest request, BodyHandler bodyHandler) + throws IOException, InterruptedException { + return httpClient.send(request, bodyHandler); + } + + /** + * Sends a request asynchronously using the underlying HTTP client. + * Equivalent to {@code httpClient().sendAsync(request, bodyHandler)}. + * + * @param request the request to send. + * @param bodyHandler handler for the response body. + * @param the response body type. + * @return a future completing with the HTTP response. + */ + public CompletableFuture> sendAsync(HttpRequest request, BodyHandler bodyHandler) { + return httpClient.sendAsync(request, bodyHandler); + } +} diff --git a/sdk/src/test/java/io/dapr/client/DaprClientHttpTest.java b/sdk/src/test/java/io/dapr/client/DaprClientHttpTest.java index af6791d7e1..e6c9f58064 100644 --- a/sdk/src/test/java/io/dapr/client/DaprClientHttpTest.java +++ b/sdk/src/test/java/io/dapr/client/DaprClientHttpTest.java @@ -633,4 +633,46 @@ public void close() throws Exception { daprClientHttp = buildDaprClient(daprHttp); daprClientHttp.close(); } + + @Test + public void invokeHttpClient_rejectsNullAppId() { + assertThrows(IllegalArgumentException.class, () -> daprClientHttp.invokeHttpClient(null)); + } + + @Test + public void invokeHttpClient_rejectsEmptyAppId() { + assertThrows(IllegalArgumentException.class, () -> daprClientHttp.invokeHttpClient("")); + } + + @Test + public void invokeHttpClient_rejectsBlankAppId() { + assertThrows(IllegalArgumentException.class, () -> daprClientHttp.invokeHttpClient(" ")); + } + + @Test + public void invokeHttpClient_resolvesInvokeBaseUriForAppId() { + DaprInvokeHttpClient invoker = daprClientHttp.invokeHttpClient("orderprocessor"); + + assertEquals( + "http://" + sidecarIp + ":3000/v1.0/invoke/orderprocessor/method/", + invoker.baseUri().toString()); + } + + @Test + public void invokeHttpClient_reusesSharedHttpClient() { + DaprInvokeHttpClient invoker = daprClientHttp.invokeHttpClient("orderprocessor"); + + org.junit.jupiter.api.Assertions.assertSame(httpClient, invoker.httpClient()); + } + + @Test + public void invokeHttpClient_propagatesApiTokenAsHeader() { + DaprHttp tokenedDaprHttp = new DaprHttp(sidecarIp, 3000, "xyz", READ_TIMEOUT, httpClient); + DaprClient client = buildDaprClient(tokenedDaprHttp); + + DaprInvokeHttpClient invoker = client.invokeHttpClient("orderprocessor"); + HttpRequest request = invoker.newRequestBuilder("orders").GET().build(); + + assertEquals("xyz", request.headers().firstValue(Headers.DAPR_API_TOKEN).orElse(null)); + } } diff --git a/sdk/src/test/java/io/dapr/client/DaprInvokeHttpClientTest.java b/sdk/src/test/java/io/dapr/client/DaprInvokeHttpClientTest.java new file mode 100644 index 0000000000..826f9583f4 --- /dev/null +++ b/sdk/src/test/java/io/dapr/client/DaprInvokeHttpClientTest.java @@ -0,0 +1,171 @@ +/* + * Copyright 2026 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandler; +import java.net.http.HttpResponse.BodyHandlers; +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class DaprInvokeHttpClientTest { + + private static final URI BASE_URI = URI.create("http://localhost:3500/v1.0/invoke/orderprocessor/method/"); + private static final Duration READ_TIMEOUT = Duration.ofSeconds(60); + + private HttpClient httpClient; + + @BeforeEach + public void setUp() { + httpClient = mock(HttpClient.class); + } + + @Test + public void constructor_rejectsNullHttpClient() { + assertThrows(NullPointerException.class, + () -> new DaprInvokeHttpClient(null, BASE_URI, "token", READ_TIMEOUT)); + } + + @Test + public void constructor_rejectsNullBaseUri() { + assertThrows(NullPointerException.class, + () -> new DaprInvokeHttpClient(httpClient, null, "token", READ_TIMEOUT)); + } + + @Test + public void accessors_returnConfiguredValues() { + DaprInvokeHttpClient invoker = new DaprInvokeHttpClient(httpClient, BASE_URI, "token", READ_TIMEOUT); + + assertSame(httpClient, invoker.httpClient()); + assertEquals(BASE_URI, invoker.baseUri()); + } + + @Test + public void newRequestBuilder_resolvesRelativePathAgainstBaseUri() { + DaprInvokeHttpClient invoker = new DaprInvokeHttpClient(httpClient, BASE_URI, null, null); + + HttpRequest request = invoker.newRequestBuilder("orders/42").GET().build(); + + assertEquals("http://localhost:3500/v1.0/invoke/orderprocessor/method/orders/42", + request.uri().toString()); + } + + @Test + public void newRequestBuilder_attachesApiTokenHeaderWhenConfigured() { + DaprInvokeHttpClient invoker = new DaprInvokeHttpClient(httpClient, BASE_URI, "xyz", null); + + HttpRequest request = invoker.newRequestBuilder("orders").GET().build(); + + assertEquals("xyz", request.headers().firstValue(Headers.DAPR_API_TOKEN).orElse(null)); + } + + @Test + public void newRequestBuilder_omitsApiTokenHeaderWhenTokenNull() { + DaprInvokeHttpClient invoker = new DaprInvokeHttpClient(httpClient, BASE_URI, null, null); + + HttpRequest request = invoker.newRequestBuilder("orders").GET().build(); + + assertFalse(request.headers().map().containsKey(Headers.DAPR_API_TOKEN)); + } + + @Test + public void newRequestBuilder_omitsApiTokenHeaderWhenTokenEmpty() { + DaprInvokeHttpClient invoker = new DaprInvokeHttpClient(httpClient, BASE_URI, "", null); + + HttpRequest request = invoker.newRequestBuilder("orders").GET().build(); + + assertFalse(request.headers().map().containsKey(Headers.DAPR_API_TOKEN)); + } + + @Test + public void newRequestBuilder_appliesReadTimeoutWhenConfigured() { + DaprInvokeHttpClient invoker = new DaprInvokeHttpClient(httpClient, BASE_URI, null, READ_TIMEOUT); + + HttpRequest request = invoker.newRequestBuilder("orders").GET().build(); + + assertEquals(Optional.of(READ_TIMEOUT), request.timeout()); + } + + @Test + public void newRequestBuilder_omitsTimeoutWhenNullOrZeroOrNegative() { + HttpRequest nullTimeoutRequest = new DaprInvokeHttpClient(httpClient, BASE_URI, null, null) + .newRequestBuilder("a").GET().build(); + HttpRequest zeroTimeoutRequest = new DaprInvokeHttpClient(httpClient, BASE_URI, null, Duration.ZERO) + .newRequestBuilder("a").GET().build(); + HttpRequest negativeTimeoutRequest = new DaprInvokeHttpClient( + httpClient, BASE_URI, null, Duration.ofSeconds(-1)) + .newRequestBuilder("a").GET().build(); + + assertTrue(nullTimeoutRequest.timeout().isEmpty()); + assertTrue(zeroTimeoutRequest.timeout().isEmpty()); + assertTrue(negativeTimeoutRequest.timeout().isEmpty()); + } + + @Test + public void newRequestBuilder_rejectsNullRelativePath() { + DaprInvokeHttpClient invoker = new DaprInvokeHttpClient(httpClient, BASE_URI, null, null); + + assertThrows(NullPointerException.class, () -> invoker.newRequestBuilder(null)); + } + + @Test + public void send_delegatesToUnderlyingHttpClient() throws Exception { + DaprInvokeHttpClient invoker = new DaprInvokeHttpClient(httpClient, BASE_URI, null, null); + HttpRequest request = invoker.newRequestBuilder("orders").GET().build(); + @SuppressWarnings("unchecked") + HttpResponse stubbedResponse = mock(HttpResponse.class); + BodyHandler handler = BodyHandlers.ofString(); + doReturn(stubbedResponse).when(httpClient).send(same(request), same(handler)); + + HttpResponse response = invoker.send(request, handler); + + assertSame(stubbedResponse, response); + verify(httpClient).send(same(request), same(handler)); + } + + @Test + public void sendAsync_delegatesToUnderlyingHttpClient() { + DaprInvokeHttpClient invoker = new DaprInvokeHttpClient(httpClient, BASE_URI, null, null); + HttpRequest request = invoker.newRequestBuilder("orders").GET().build(); + @SuppressWarnings("unchecked") + HttpResponse stubbedResponse = mock(HttpResponse.class); + BodyHandler handler = BodyHandlers.ofString(); + CompletableFuture> future = CompletableFuture.completedFuture(stubbedResponse); + when(httpClient.sendAsync(same(request), same(handler))).thenReturn(future); + + CompletableFuture> result = invoker.sendAsync(request, handler); + + assertSame(future, result); + verify(httpClient).sendAsync(same(request), any()); + } +} From 3fcb736ce1c99da72f5c78e0c42b8840cd707892 Mon Sep 17 00:00:00 2001 From: Javier Aliaga Date: Wed, 20 May 2026 13:46:02 +0200 Subject: [PATCH 3/3] feat: add DaprBodyPublishers.json helper and invokeHttpClient migration notes Adds an opt-in DaprBodyPublishers.json(Object) helper backed by the SDK's default Jackson serializer, matching the JSON encoding the deprecated DaprClient.invokeMethod APIs applied internally. Eases migration without re-introducing auto-serialization into the raw HttpClient surface exposed by invokeHttpClient. Also fixes the invoke/http example README expected_stdout_lines to match the new plain-text body sent by the refactored example (Server: message one instead of Server: "message one"), and adds migration notes to the DaprClient.invokeHttpClient and DaprInvokeHttpClient Javadoc as well as the example README. Signed-off-by: Javier Aliaga --- .../io/dapr/examples/invoke/http/README.md | 13 ++- .../io/dapr/client/DaprBodyPublishers.java | 74 +++++++++++++++ .../main/java/io/dapr/client/DaprClient.java | 8 ++ .../io/dapr/client/DaprInvokeHttpClient.java | 14 +++ .../dapr/client/DaprBodyPublishersTest.java | 90 +++++++++++++++++++ 5 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 sdk/src/main/java/io/dapr/client/DaprBodyPublishers.java create mode 100644 sdk/src/test/java/io/dapr/client/DaprBodyPublishersTest.java diff --git a/examples/src/main/java/io/dapr/examples/invoke/http/README.md b/examples/src/main/java/io/dapr/examples/invoke/http/README.md index dd99ae59e7..e86241a10c 100644 --- a/examples/src/main/java/io/dapr/examples/invoke/http/README.md +++ b/examples/src/main/java/io/dapr/examples/invoke/http/README.md @@ -17,6 +17,15 @@ Two equivalent approaches are demonstrated: 1. `DaprClient.invokeHttpClient(appId)` — an SDK-provided wrapper that returns a pre-configured `HttpClient` bound to the sidecar's `/v1.0/invoke//method/` prefix, with the `dapr-api-token` header attached when configured. 2. A raw `java.net.http.HttpClient` sending the request to the sidecar's base URL with a `dapr-app-id` header identifying the target app — no SDK helper required. +> **Migrating from `DaprClient.invokeMethod`:** the deprecated `invokeMethod` APIs serialized request bodies through the configured `DaprObjectSerializer` (JSON by default), so a `String` payload was sent as a JSON string literal — e.g. `"hello"` instead of `hello`. `invokeHttpClient` does **not** serialize bodies: callers supply raw `BodyPublisher`s exactly as with any `java.net.http.HttpClient`. To preserve the previous JSON encoding, use `DaprBodyPublishers.json(Object)`: +> +> ```java +> HttpRequest request = invoker.newRequestBuilder("orders") +> .header("Content-Type", "application/json") +> .POST(DaprBodyPublishers.json(order)) +> .build(); +> ``` + ## Pre-requisites * [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/). @@ -109,8 +118,8 @@ Use the following command to execute the demo service example: diff --git a/sdk/src/main/java/io/dapr/client/DaprBodyPublishers.java b/sdk/src/main/java/io/dapr/client/DaprBodyPublishers.java new file mode 100644 index 0000000000..37651e30b6 --- /dev/null +++ b/sdk/src/main/java/io/dapr/client/DaprBodyPublishers.java @@ -0,0 +1,74 @@ +/* + * Copyright 2026 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client; + +import io.dapr.serializer.DefaultObjectSerializer; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.http.HttpRequest.BodyPublisher; +import java.net.http.HttpRequest.BodyPublishers; + +/** + * Convenience {@link BodyPublisher} factories for use with {@link DaprInvokeHttpClient} + * and the standard {@link java.net.http.HttpClient}. + * + *

{@link DaprClient#invokeHttpClient(String)} intentionally does not + * serialize request bodies for you — callers pass raw {@link BodyPublisher}s + * exactly as with any {@link java.net.http.HttpClient}. This class provides an + * opt-in helper that matches the JSON encoding the deprecated + * {@code DaprClient.invokeMethod} APIs applied internally, easing migration. + * + *

Example: + *

{@code
+ * record Order(String id, int qty) {}
+ *
+ * HttpRequest request = invoker.newRequestBuilder("orders")
+ *     .header("Content-Type", "application/json")
+ *     .POST(DaprBodyPublishers.json(new Order("o-1", 3)))
+ *     .build();
+ * }
+ */ +public final class DaprBodyPublishers { + + private static final DefaultObjectSerializer SERIALIZER = new DefaultObjectSerializer(); + + private DaprBodyPublishers() { + } + + /** + * Serializes the given value as JSON using the SDK's default object serializer + * (Jackson) and returns a {@link BodyPublisher} carrying the resulting bytes. + * + *

This matches the wire encoding the deprecated + * {@code DaprClient.invokeMethod} APIs applied to request payloads: e.g. + * {@code json("hello")} emits {@code "hello"} (a JSON string) and + * {@code json(null)} emits an empty body. + * + *

Callers are still responsible for setting an appropriate + * {@code Content-Type} header (typically {@code application/json}). + * + * @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. + */ + public static BodyPublisher json(Object value) { + try { + byte[] bytes = SERIALIZER.serialize(value); + return bytes == null ? BodyPublishers.noBody() : BodyPublishers.ofByteArray(bytes); + } catch (IOException e) { + throw new UncheckedIOException("Failed to JSON-serialize request body", e); + } + } +} diff --git a/sdk/src/main/java/io/dapr/client/DaprClient.java b/sdk/src/main/java/io/dapr/client/DaprClient.java index d57780ed23..34495110b7 100644 --- a/sdk/src/main/java/io/dapr/client/DaprClient.java +++ b/sdk/src/main/java/io/dapr/client/DaprClient.java @@ -361,6 +361,14 @@ Mono invokeMethod(String appId, String methodName, byte[] request, HttpE * {@code dapr-api-token} header when one is configured. It reuses the SDK's shared * {@link java.net.http.HttpClient} instance. * + *

Migrating from {@code invokeMethod}: the deprecated {@code invokeMethod} + * APIs serialized request bodies through the configured {@link io.dapr.serializer.DaprObjectSerializer} + * (JSON by default), so a {@code String} payload was sent as a JSON string literal + * (e.g. {@code "hello"}). This client does not serialize bodies — callers + * supply raw {@link java.net.http.HttpRequest.BodyPublisher BodyPublisher}s exactly + * as with any {@link java.net.http.HttpClient}. To preserve the previous JSON encoding, + * use {@link DaprBodyPublishers#json(Object)}. + * * @param appId the application id to invoke. * @return a {@link DaprInvokeHttpClient} bound to {@code appId}. */ diff --git a/sdk/src/main/java/io/dapr/client/DaprInvokeHttpClient.java b/sdk/src/main/java/io/dapr/client/DaprInvokeHttpClient.java index 171f49fe19..34564dcbbb 100644 --- a/sdk/src/main/java/io/dapr/client/DaprInvokeHttpClient.java +++ b/sdk/src/main/java/io/dapr/client/DaprInvokeHttpClient.java @@ -44,6 +44,20 @@ * HttpResponse response = invoker.send(request, HttpResponse.BodyHandlers.ofString()); * } * + *

Migrating from {@code DaprClient.invokeMethod}: the deprecated + * {@code invokeMethod} APIs serialized request bodies through the configured + * {@link io.dapr.serializer.DaprObjectSerializer} (JSON by default), so a {@code String} + * payload was sent as a JSON string literal (e.g. {@code "hello"}). This client does + * not serialize bodies — callers supply raw + * {@link HttpRequest.BodyPublisher BodyPublisher}s. To preserve the previous JSON + * encoding, use {@link DaprBodyPublishers#json(Object)}: + *

{@code
+ * HttpRequest request = invoker.newRequestBuilder("orders")
+ *     .header("Content-Type", "application/json")
+ *     .POST(DaprBodyPublishers.json(order))
+ *     .build();
+ * }
+ * *

This class is not {@link AutoCloseable}: the underlying {@link HttpClient} is * managed by the SDK and shared across all clients created from a single * {@link DaprClientBuilder}; closing the owning {@link DaprClient} releases it. diff --git a/sdk/src/test/java/io/dapr/client/DaprBodyPublishersTest.java b/sdk/src/test/java/io/dapr/client/DaprBodyPublishersTest.java new file mode 100644 index 0000000000..500cdd4d1d --- /dev/null +++ b/sdk/src/test/java/io/dapr/client/DaprBodyPublishersTest.java @@ -0,0 +1,90 @@ +/* + * Copyright 2026 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client; + +import org.junit.jupiter.api.Test; + +import java.net.http.HttpRequest.BodyPublisher; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Flow; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class DaprBodyPublishersTest { + + @Test + public void json_stringIsJsonEncodedWithQuotes() throws Exception { + BodyPublisher publisher = DaprBodyPublishers.json("message one"); + assertEquals("\"message one\"", drain(publisher)); + assertEquals("\"message one\"".getBytes().length, publisher.contentLength()); + } + + @Test + public void json_pojoIsSerializedAsJsonObject() throws Exception { + BodyPublisher publisher = DaprBodyPublishers.json(Map.of("id", "o-1", "qty", 3)); + String body = drain(publisher); + // Map ordering is not guaranteed; check both fields are present and shape is an object. + assertTrue(body.startsWith("{") && body.endsWith("}"), () -> "unexpected JSON: " + body); + assertTrue(body.contains("\"id\":\"o-1\""), () -> "missing id field: " + body); + assertTrue(body.contains("\"qty\":3"), () -> "missing qty field: " + body); + } + + @Test + public void json_nullYieldsEmptyBody() { + BodyPublisher publisher = DaprBodyPublishers.json(null); + assertEquals(0L, publisher.contentLength()); + } + + private static String drain(BodyPublisher publisher) throws Exception { + List chunks = new ArrayList<>(); + CountDownLatch done = new CountDownLatch(1); + publisher.subscribe(new Flow.Subscriber() { + @Override + public void onSubscribe(Flow.Subscription subscription) { + subscription.request(Long.MAX_VALUE); + } + + @Override + public void onNext(ByteBuffer item) { + chunks.add(item); + } + + @Override + public void onError(Throwable throwable) { + done.countDown(); + } + + @Override + public void onComplete() { + done.countDown(); + } + }); + assertTrue(done.await(5, TimeUnit.SECONDS)); + int total = chunks.stream().mapToInt(ByteBuffer::remaining).sum(); + byte[] out = new byte[total]; + int offset = 0; + for (ByteBuffer chunk : chunks) { + int len = chunk.remaining(); + chunk.get(out, offset, len); + offset += len; + } + return new String(out); + } +}