Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -334,6 +335,11 @@ public <T> Mono<T> invokeMethod(InvokeMethodRequest invokeMethodRequest, TypeRef
return delegate.invokeMethod(invokeMethodRequest, type);
}

@Override
public DaprInvokeHttpClient invokeHttpClient(String appId) {
return delegate.invokeHttpClient(appId);
}

// -------------------------------------------------------------------------
// Bindings
// -------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@

import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import io.dapr.client.domain.HttpExtension;
import io.dapr.client.DaprInvokeHttpClient;
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:
Expand All @@ -24,6 +30,16 @@
* 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'
*
* <p>This example demonstrates calling another Dapr-enabled application over HTTP.
* Two equivalent approaches are shown:
* <ol>
* <li>{@link DaprClient#invokeHttpClient(String)} — an SDK-provided {@link java.net.http.HttpClient}
* wrapper pre-bound to the sidecar's {@code /v1.0/invoke/&lt;app-id&gt;/method/} prefix,
* with the {@code dapr-api-token} header attached when configured.</li>
* <li>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.</li>
* </ol>
*/
public class InvokeClient {

Expand All @@ -32,22 +48,49 @@ 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()) {
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) {
byte[] response = client.invokeMethod(SERVICE_APP_ID, "say", message, HttpExtension.POST, null,
byte[].class).block();
System.out.println(new String(response));
}
// Form 1: SDK helper — paths resolve against /v1.0/invoke/<app-id>/method/.
HttpRequest sdkRequest = invoker.newRequestBuilder(METHOD)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(message))
.build();
HttpResponse<byte[]> sdkResponse =
invoker.send(sdkRequest, HttpResponse.BodyHandlers.ofByteArray());
System.out.println(new String(sdkResponse.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");
// 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<byte[]> 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.
// Normally a dapr app would be a web service and not exit main.
System.out.println("Done");
}
}
79 changes: 53 additions & 26 deletions examples/src/main/java/io/dapr/examples/invoke/http/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@ 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 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/<app-id>/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

Expand Down Expand Up @@ -121,39 +126,62 @@ 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 calls the remote method through the Dapr sidecar using two equivalent approaches:

1. `DaprClient.invokeHttpClient(appId)` — an SDK-provided wrapper around `java.net.http.HttpClient` pre-bound to `/v1.0/invoke/<app-id>/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:

```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()) {
private static final String SERVICE_APP_ID = "invokedemo";
private static final String METHOD = "say";

public static void main(String[] args) throws Exception {
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) {
byte[] response = client.invokeMethod(SERVICE_APP_ID, "say", message, HttpExtension.POST, null,
byte[].class).block();
System.out.println(new String(response));
// Form 1: SDK helper — paths resolve against /v1.0/invoke/<app-id>/method/.
HttpRequest sdkRequest = invoker.newRequestBuilder(METHOD)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(message))
.build();
HttpResponse<byte[]> 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<byte[]> 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.
// Normally a dapr app would be a web service and not exit main.
System.out.println("Done");
}

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.
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:

<!-- STEP
name: Run demo client
expected_stdout_lines:
- '"message one" received'
- '"message two" received'
expected_stdout_lines:
- 'Done'
background: true
sleep: 5
Expand All @@ -165,15 +193,14 @@ dapr run --app-id invokeclient -- java -jar target/dapr-java-sdk-examples-exec.j

<!-- END_STEP -->

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.
Expand Down
13 changes: 13 additions & 0 deletions sdk/src/main/java/io/dapr/client/DaprClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,19 @@ Mono<byte[]> invokeMethod(String appId, String methodName, byte[] request, HttpE
@Deprecated
<T> Mono<T> invokeMethod(InvokeMethodRequest invokeMethodRequest, TypeRef<T> type);

/**
* Creates an HTTP client pre-configured for Dapr service invocation against the given app id.
*
* <p>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.
*
Expand Down
15 changes: 15 additions & 0 deletions sdk/src/main/java/io/dapr/client/DaprClientImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -656,6 +657,20 @@ public <T> Mono<T> 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 <T> Mono<T> getMonoForHttpResponse(TypeRef<T> type, DaprHttp.Response r) {
try {
if (type == null) {
Expand Down
16 changes: 16 additions & 0 deletions sdk/src/main/java/io/dapr/client/DaprHttp.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
Loading
Loading