Skip to content

Commit c02ab76

Browse files
[Backport release-1.17] feat: add DaprClient.invokeHttpClient(appId) factory (#1763)
* feat: add DaprClient.invokeHttpClient(appId) factory (#1742) * chore: rewrite invoke/http example to use native HttpClient 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/<app-id>/method/<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 <javier@diagrid.io> * 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 <javier@diagrid.io> * 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 <javier@diagrid.io> --------- Signed-off-by: Javier Aliaga <javier@diagrid.io> (cherry picked from commit 9539d70) * fix: Add missing implementation Signed-off-by: Javier Aliaga <javier@diagrid.io> --------- Signed-off-by: Javier Aliaga <javier@diagrid.io> Co-authored-by: Javier Aliaga <javier@diagrid.io>
1 parent 80687da commit c02ab76

13 files changed

Lines changed: 726 additions & 42 deletions

File tree

dapr-spring/dapr-spring-boot-4-autoconfigure/src/main/java/io/dapr/spring/boot4/autoconfigure/client/ObservationDaprClient.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
package io.dapr.spring.boot4.autoconfigure.client;
1515

1616
import io.dapr.client.DaprClient;
17+
import io.dapr.client.DaprInvokeHttpClient;
1718
import io.dapr.client.domain.BulkPublishRequest;
1819
import io.dapr.client.domain.BulkPublishResponse;
1920
import io.dapr.client.domain.ConfigurationItem;
@@ -299,6 +300,11 @@ public <T> Mono<T> invokeMethod(InvokeMethodRequest invokeMethodRequest, TypeRef
299300
return delegate.invokeMethod(invokeMethodRequest, type);
300301
}
301302

303+
@Override
304+
public DaprInvokeHttpClient invokeHttpClient(String appId) {
305+
return delegate.invokeHttpClient(appId);
306+
}
307+
302308
// -------------------------------------------------------------------------
303309
// Bindings
304310
// -------------------------------------------------------------------------

dapr-spring/dapr-spring-boot-autoconfigure/src/main/java/io/dapr/spring/boot/autoconfigure/client/ObservationDaprClient.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
package io.dapr.spring.boot.autoconfigure.client;
1515

1616
import io.dapr.client.DaprClient;
17+
import io.dapr.client.DaprInvokeHttpClient;
1718
import io.dapr.client.domain.BulkPublishRequest;
1819
import io.dapr.client.domain.BulkPublishResponse;
1920
import io.dapr.client.domain.ConfigurationItem;
@@ -299,6 +300,11 @@ public <T> Mono<T> invokeMethod(InvokeMethodRequest invokeMethodRequest, TypeRef
299300
return delegate.invokeMethod(invokeMethodRequest, type);
300301
}
301302

303+
@Override
304+
public DaprInvokeHttpClient invokeHttpClient(String appId) {
305+
return delegate.invokeHttpClient(appId);
306+
}
307+
302308
// -------------------------------------------------------------------------
303309
// Bindings
304310
// -------------------------------------------------------------------------

dapr-spring/dapr-spring-boot-autoconfigure/src/test/java/io/dapr/spring/boot/autoconfigure/client/ObservationDaprClientTest.java

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,10 @@
1414
package io.dapr.spring.boot.autoconfigure.client;
1515

1616
import io.dapr.client.DaprClient;
17-
import io.dapr.client.domain.DeleteStateRequest;
18-
import io.dapr.client.domain.GetSecretRequest;
19-
import io.dapr.client.domain.GetStateRequest;
17+
import io.dapr.client.DaprInvokeHttpClient;
2018
import io.dapr.client.domain.InvokeBindingRequest;
2119
import io.dapr.client.domain.PublishEventRequest;
2220
import io.dapr.client.domain.ScheduleJobRequest;
23-
import io.dapr.client.domain.State;
2421
import io.micrometer.observation.tck.TestObservationRegistry;
2522
import io.micrometer.observation.tck.TestObservationRegistryAssert;
2623
import org.junit.jupiter.api.BeforeEach;
@@ -33,7 +30,6 @@
3330
import reactor.core.publisher.Mono;
3431

3532
import java.util.List;
36-
import java.util.Map;
3733

3834
import static org.assertj.core.api.Assertions.assertThat;
3935
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -378,4 +374,18 @@ void deprecatedInvokeMethodDoesNotCreateSpan() {
378374
// Registry must be empty — no spans for deprecated methods
379375
TestObservationRegistryAssert.assertThat(registry).doesNotHaveAnyObservation();
380376
}
377+
378+
@Test
379+
@DisplayName("invokeHttpClient delegates without creating a span")
380+
void invokeHttpClientDelegatesWithoutSpan() {
381+
DaprInvokeHttpClient stub = org.mockito.Mockito.mock(DaprInvokeHttpClient.class);
382+
when(delegate.invokeHttpClient("orderprocessor")).thenReturn(stub);
383+
384+
DaprInvokeHttpClient result = client.invokeHttpClient("orderprocessor");
385+
386+
assertThat(result).isSameAs(stub);
387+
verify(delegate).invokeHttpClient("orderprocessor");
388+
// Synchronous factory — no Mono/Flux to observe, so no span is expected.
389+
TestObservationRegistryAssert.assertThat(registry).doesNotHaveAnyObservation();
390+
}
381391
}

examples/src/main/java/io/dapr/examples/invoke/http/InvokeClient.java

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@
1515

1616
import io.dapr.client.DaprClient;
1717
import io.dapr.client.DaprClientBuilder;
18-
import io.dapr.client.domain.HttpExtension;
18+
import io.dapr.client.DaprInvokeHttpClient;
19+
import io.dapr.config.Properties;
20+
21+
import java.net.URI;
22+
import java.net.http.HttpClient;
23+
import java.net.http.HttpRequest;
24+
import java.net.http.HttpResponse;
1925

2026
/**
2127
* 1. Build and install jars:
@@ -24,6 +30,16 @@
2430
* 3. Send messages to the server:
2531
* dapr run -- java -jar target/dapr-java-sdk-examples-exec.jar \
2632
* io.dapr.examples.invoke.http.InvokeClient 'message one' 'message two'
33+
*
34+
* <p>This example demonstrates calling another Dapr-enabled application over HTTP.
35+
* Two equivalent approaches are shown:
36+
* <ol>
37+
* <li>{@link DaprClient#invokeHttpClient(String)} — an SDK-provided {@link java.net.http.HttpClient}
38+
* wrapper pre-bound to the sidecar's {@code /v1.0/invoke/&lt;app-id&gt;/method/} prefix,
39+
* with the {@code dapr-api-token} header attached when configured.</li>
40+
* <li>A raw {@link java.net.http.HttpClient} sending the request to the sidecar's base URL
41+
* with a {@code dapr-app-id} header identifying the target app — no SDK helper required.</li>
42+
* </ol>
2743
*/
2844
public class InvokeClient {
2945

@@ -32,22 +48,49 @@ public class InvokeClient {
3248
*/
3349
private static final String SERVICE_APP_ID = "invokedemo";
3450

51+
/**
52+
* Method on the target service to invoke.
53+
*/
54+
private static final String METHOD = "say";
55+
3556
/**
3657
* Starts the invoke client.
3758
*
3859
* @param args Messages to be sent as request for the invoke API.
3960
*/
4061
public static void main(String[] args) throws Exception {
41-
try (DaprClient client = (new DaprClientBuilder()).build()) {
62+
try (DaprClient daprClient = new DaprClientBuilder().build()) {
63+
DaprInvokeHttpClient invoker = daprClient.invokeHttpClient(SERVICE_APP_ID);
64+
65+
int port = Properties.HTTP_PORT.get();
66+
String sidecarBase = "http://localhost:" + port;
67+
HttpClient rawHttpClient = HttpClient.newHttpClient();
68+
4269
for (String message : args) {
43-
byte[] response = client.invokeMethod(SERVICE_APP_ID, "say", message, HttpExtension.POST, null,
44-
byte[].class).block();
45-
System.out.println(new String(response));
46-
}
70+
// Form 1: SDK helper — paths resolve against /v1.0/invoke/<app-id>/method/.
71+
HttpRequest sdkRequest = invoker.newRequestBuilder(METHOD)
72+
.header("Content-Type", "application/json")
73+
.POST(HttpRequest.BodyPublishers.ofString(message))
74+
.build();
75+
HttpResponse<byte[]> sdkResponse =
76+
invoker.send(sdkRequest, HttpResponse.BodyHandlers.ofByteArray());
77+
System.out.println(new String(sdkResponse.body()));
4778

48-
// This is an example, so for simplicity we are just exiting here.
49-
// Normally a dapr app would be a web service and not exit main.
50-
System.out.println("Done");
79+
// Form 2: raw HttpClient + dapr-app-id header against the sidecar's base URL.
80+
HttpRequest headerRequest = HttpRequest.newBuilder()
81+
.uri(URI.create(sidecarBase + "/" + METHOD))
82+
.header("Content-Type", "application/json")
83+
.header("dapr-app-id", SERVICE_APP_ID)
84+
.POST(HttpRequest.BodyPublishers.ofString(message))
85+
.build();
86+
HttpResponse<byte[]> headerResponse =
87+
rawHttpClient.send(headerRequest, HttpResponse.BodyHandlers.ofByteArray());
88+
System.out.println(new String(headerResponse.body()));
89+
}
5190
}
91+
92+
// This is an example, so for simplicity we are just exiting here.
93+
// Normally a dapr app would be a web service and not exit main.
94+
System.out.println("Done");
5295
}
5396
}

examples/src/main/java/io/dapr/examples/invoke/http/README.md

Lines changed: 64 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,23 @@ This sample includes:
88

99
Visit [this](https://docs.dapr.io/developing-applications/building-blocks/service-invocation/service-invocation-overview/) link for more information about Dapr and service invocation.
1010

11-
## Remote invocation using the Java-SDK
11+
## Remote invocation using a native HTTP client
1212

13-
This sample uses the Client provided in Dapr Java SDK invoking a remote method.
13+
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.
14+
15+
Two equivalent approaches are demonstrated:
16+
17+
1. `DaprClient.invokeHttpClient(appId)` — an SDK-provided wrapper that returns a pre-configured `HttpClient` bound to the sidecar's `/v1.0/invoke/<app-id>/method/` prefix, with the `dapr-api-token` header attached when configured.
18+
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.
19+
20+
> **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)`:
21+
>
22+
> ```java
23+
> HttpRequest request = invoker.newRequestBuilder("orders")
24+
> .header("Content-Type", "application/json")
25+
> .POST(DaprBodyPublishers.json(order))
26+
> .build();
27+
> ```
1428
1529
## Pre-requisites
1630
@@ -104,8 +118,8 @@ Use the following command to execute the demo service example:
104118
<!-- STEP
105119
name: Run demo service
106120
expected_stdout_lines:
107-
- 'Server: "message one"'
108-
- 'Server: "message two"'
121+
- 'Server: message one'
122+
- 'Server: message two'
109123
background: true
110124
sleep: 5
111125
-->
@@ -121,39 +135,62 @@ Once running, the ExposerService is now ready to be invoked by Dapr.
121135
122136
### Running the InvokeClient sample
123137
124-
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:
138+
The Invoke client sample calls the remote method through the Dapr sidecar using two equivalent approaches:
139+
140+
1. `DaprClient.invokeHttpClient(appId)` — an SDK-provided wrapper around `java.net.http.HttpClient` pre-bound to `/v1.0/invoke/<app-id>/method/`.
141+
2. A raw `java.net.http.HttpClient` against the sidecar's base URL with a `dapr-app-id` header.
142+
143+
In `InvokeClient.java` file, you will find the `InvokeClient` class and the `main` method. See the code snippet below:
125144
126145
```java
127146
public class InvokeClient {
128147
129-
private static final String SERVICE_APP_ID = "invokedemo";
130-
///...
131-
public static void main(String[] args) throws Exception {
132-
try (DaprClient client = (new DaprClientBuilder()).build()) {
148+
private static final String SERVICE_APP_ID = "invokedemo";
149+
private static final String METHOD = "say";
150+
151+
public static void main(String[] args) throws Exception {
152+
try (DaprClient daprClient = new DaprClientBuilder().build()) {
153+
DaprInvokeHttpClient invoker = daprClient.invokeHttpClient(SERVICE_APP_ID);
154+
155+
int port = Properties.HTTP_PORT.get();
156+
String sidecarBase = "http://localhost:" + port;
157+
HttpClient rawHttpClient = HttpClient.newHttpClient();
158+
133159
for (String message : args) {
134-
byte[] response = client.invokeMethod(SERVICE_APP_ID, "say", message, HttpExtension.POST, null,
135-
byte[].class).block();
136-
System.out.println(new String(response));
160+
// Form 1: SDK helper — paths resolve against /v1.0/invoke/<app-id>/method/.
161+
HttpRequest sdkRequest = invoker.newRequestBuilder(METHOD)
162+
.header("Content-Type", "application/json")
163+
.POST(HttpRequest.BodyPublishers.ofString(message))
164+
.build();
165+
HttpResponse<byte[]> sdkResponse =
166+
invoker.send(sdkRequest, HttpResponse.BodyHandlers.ofByteArray());
167+
System.out.println(new String(sdkResponse.body()));
168+
169+
// Form 2: raw HttpClient + dapr-app-id header against the sidecar's base URL.
170+
HttpRequest headerRequest = HttpRequest.newBuilder()
171+
.uri(URI.create(sidecarBase + "/" + METHOD))
172+
.header("Content-Type", "application/json")
173+
.header("dapr-app-id", SERVICE_APP_ID)
174+
.POST(HttpRequest.BodyPublishers.ofString(message))
175+
.build();
176+
HttpResponse<byte[]> headerResponse =
177+
rawHttpClient.send(headerRequest, HttpResponse.BodyHandlers.ofByteArray());
178+
System.out.println(new String(headerResponse.body()));
137179
}
138-
139-
// This is an example, so for simplicity we are just exiting here.
140-
// Normally a dapr app would be a web service and not exit main.
141-
System.out.println("Done");
142180
}
181+
182+
System.out.println("Done");
143183
}
144-
///...
145184
}
146185
```
147186
148-
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.
149-
187+
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.
188+
150189
Execute the follow script in order to run the InvokeClient example, passing two messages for the remote method:
151190
152191
<!-- STEP
153192
name: Run demo client
154-
expected_stdout_lines:
155-
- '"message one" received'
156-
- '"message two" received'
193+
expected_stdout_lines:
157194
- 'Done'
158195
background: true
159196
sleep: 5
@@ -165,15 +202,14 @@ dapr run --app-id invokeclient -- java -jar target/dapr-java-sdk-examples-exec.j
165202
166203
<!-- END_STEP -->
167204
168-
Finally, the console for `invokeclient` should output:
205+
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:
169206
170207
```text
171-
"message one" received
172-
173-
"message two" received
174-
208+
2026-05-12 13:45:00.123
209+
2026-05-12 13:45:00.456
210+
2026-05-12 13:45:00.789
211+
2026-05-12 13:45:01.012
175212
Done
176-
177213
```
178214
179215
For more details on Dapr Spring Boot integration, please refer to [Dapr Spring Boot](../../DaprApplication.java) Application implementation.
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright 2026 The Dapr Authors
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
package io.dapr.client;
15+
16+
import io.dapr.serializer.DefaultObjectSerializer;
17+
18+
import java.io.IOException;
19+
import java.io.UncheckedIOException;
20+
import java.net.http.HttpRequest.BodyPublisher;
21+
import java.net.http.HttpRequest.BodyPublishers;
22+
23+
/**
24+
* Convenience {@link BodyPublisher} factories for use with {@link DaprInvokeHttpClient}
25+
* and the standard {@link java.net.http.HttpClient}.
26+
*
27+
* <p>{@link DaprClient#invokeHttpClient(String)} intentionally does <em>not</em>
28+
* serialize request bodies for you — callers pass raw {@link BodyPublisher}s
29+
* exactly as with any {@link java.net.http.HttpClient}. This class provides an
30+
* opt-in helper that matches the JSON encoding the deprecated
31+
* {@code DaprClient.invokeMethod} APIs applied internally, easing migration.
32+
*
33+
* <p>Example:
34+
* <pre>{@code
35+
* record Order(String id, int qty) {}
36+
*
37+
* HttpRequest request = invoker.newRequestBuilder("orders")
38+
* .header("Content-Type", "application/json")
39+
* .POST(DaprBodyPublishers.json(new Order("o-1", 3)))
40+
* .build();
41+
* }</pre>
42+
*/
43+
public final class DaprBodyPublishers {
44+
45+
private static final DefaultObjectSerializer SERIALIZER = new DefaultObjectSerializer();
46+
47+
private DaprBodyPublishers() {
48+
}
49+
50+
/**
51+
* Serializes the given value as JSON using the SDK's default object serializer
52+
* (Jackson) and returns a {@link BodyPublisher} carrying the resulting bytes.
53+
*
54+
* <p>This matches the wire encoding the deprecated
55+
* {@code DaprClient.invokeMethod} APIs applied to request payloads: e.g.
56+
* {@code json("hello")} emits {@code "hello"} (a JSON string) and
57+
* {@code json(null)} emits an empty body.
58+
*
59+
* <p>Callers are still responsible for setting an appropriate
60+
* {@code Content-Type} header (typically {@code application/json}).
61+
*
62+
* @param value object to serialize; {@code null} yields an empty body.
63+
* @return a body publisher carrying the JSON-encoded bytes.
64+
* @throws UncheckedIOException if serialization fails.
65+
*/
66+
public static BodyPublisher json(Object value) {
67+
try {
68+
byte[] bytes = SERIALIZER.serialize(value);
69+
return bytes == null ? BodyPublishers.noBody() : BodyPublishers.ofByteArray(bytes);
70+
} catch (IOException e) {
71+
throw new UncheckedIOException("Failed to JSON-serialize request body", e);
72+
}
73+
}
74+
}

0 commit comments

Comments
 (0)