From ec97be30cb69b30ee1b2611509c19594ea1717cb Mon Sep 17 00:00:00 2001 From: Javier Aliaga Date: Tue, 12 May 2026 15:48:07 +0200 Subject: [PATCH 1/2] 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 297a3a3d106b0785e68a3623f9263fd57cda3862 Mon Sep 17 00:00:00 2001 From: Javier Aliaga Date: Thu, 14 May 2026 10:36:14 +0200 Subject: [PATCH 2/2] 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 e5c95ec0d7..71842cee6c 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; @@ -656,6 +657,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()); + } +}