Skip to content

Add support for calls to HTTP endpoints#271

Open
bachuv wants to merge 4 commits intomainfrom
vabachu/callhttp
Open

Add support for calls to HTTP endpoints#271
bachuv wants to merge 4 commits intomainfrom
vabachu/callhttp

Conversation

@bachuv
Copy link
Contributor

@bachuv bachuv commented Mar 15, 2026

Issue describing the changes in this PR

Adds support for Calls to HTTP endpoints to the Java SDK.

Pull request checklist

  • My changes do not require documentation changes
    • Otherwise: Documentation issue linked to PR
  • My changes are added to the CHANGELOG.md
  • I have added all required tests (Unit tests, E2E tests)

@bachuv bachuv requested a review from a team as a code owner March 15, 2026 18:56
Copilot AI review requested due to automatic review settings March 15, 2026 18:56
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-class Durable Functions “durable HTTP” support to the Java SDK by introducing request/response models, retry/auth configuration, and orchestration helpers, along with unit and end-to-end coverage.

Changes:

  • Introduces DurableHttp APIs and supporting models (DurableHttpRequest/Response, retry options, token sources) in the core client SDK.
  • Adds unit tests validating JSON wire compatibility (TimeSpan formatting, managed identity options, retry options, request/response serialization).
  • Adds an Azure Functions E2E function + test, and centralizes JUnit dependencies at the root Gradle level.

Reviewed changes

Copilot reviewed 23 out of 24 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
samples-azure-functions/build.gradle Removes per-module JUnit deps (now provided centrally).
endtoendtests/src/test/java/com/functions/EndToEndTests.java Adds E2E test for durable HTTP orchestration.
endtoendtests/src/main/java/com/functions/CallHttpFunction.java Adds Functions endpoints/orchestrator that performs a durable HTTP GET.
endtoendtests/build.gradle Removes per-module JUnit deps (now provided centrally).
client/src/test/java/com/microsoft/durabletask/TimeSpanHelperTest.java Adds unit tests for .NET TimeSpan ↔ Java Duration conversion.
client/src/test/java/com/microsoft/durabletask/ManagedIdentityTokenSourceTest.java Adds unit tests for managed identity token source behavior/JSON.
client/src/test/java/com/microsoft/durabletask/ManagedIdentityOptionsTest.java Adds unit tests for managed identity options JSON shape.
client/src/test/java/com/microsoft/durabletask/HttpRetryOptionsTest.java Adds unit tests for HTTP retry options behavior/JSON.
client/src/test/java/com/microsoft/durabletask/DurableHttpResponseTest.java Adds unit tests for durable HTTP response model/JSON.
client/src/test/java/com/microsoft/durabletask/DurableHttpRequestTest.java Adds unit tests for durable HTTP request model/JSON and MI scenarios.
client/src/main/java/com/microsoft/durabletask/TokenSource.java Adds polymorphic base type for durable HTTP auth token sources.
client/src/main/java/com/microsoft/durabletask/TimeSpanHelper.java Adds internal utility for TimeSpan wire-format conversions.
client/src/main/java/com/microsoft/durabletask/TaskOrchestrationContext.java Adds convenience callHttp(...) default methods on orchestration context.
client/src/main/java/com/microsoft/durabletask/ManagedIdentityTokenSource.java Adds managed identity token source model with resource normalization.
client/src/main/java/com/microsoft/durabletask/ManagedIdentityOptions.java Adds managed identity options model for authority host/tenant.
client/src/main/java/com/microsoft/durabletask/HttpRetryOptions.java Adds durable HTTP retry policy model with TimeSpan serialization helpers.
client/src/main/java/com/microsoft/durabletask/DurableHttpResponse.java Adds durable HTTP response model.
client/src/main/java/com/microsoft/durabletask/DurableHttpRequest.java Adds durable HTTP request model incl. timeout/retry/token source.
client/src/main/java/com/microsoft/durabletask/DurableHttp.java Adds orchestrator-side helper for invoking the built-in HTTP activity.
client/build.gradle Improves JDK11 test runtime selection and Windows executable handling; removes local JUnit deps.
build.gradle Centralizes JUnit 5 dependencies for all Java subprojects.
azuremanaged/build.gradle Aligns test runtime selection and removes per-module JUnit deps.
azurefunctions/src/test/java/com/microsoft/durabletask/azurefunctions/DurableHttpTest.java Adds unit tests ensuring DurableHttp delegates to callActivity as expected.
azurefunctions/build.gradle Adds test toolchain config and Mockito deps to support new unit tests.

You can also share your feedback on Copilot code review. Take the survey.

Copy link
Member

@YunchuWang YunchuWang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

Overall risk: Moderate
Merge recommendation: Needs changes (at least F1/F2 should be addressed first)

This PR adds comprehensive callHttp support for the Java Durable Task SDK. The overall design is solid - good wire-format compatibility with .NET, strong test coverage, and clean API surface. A few issues should be fixed before merge.

Findings

  • High (P1): 1 - normalizeResource prefix matching too broad (F1)
  • Medium (P2): 1 - asynchronousPatternEnabled default mismatch on deserialization (F2)
  • Low (P3): 2 - duplicated JDK path logic in build files (F3), setMaxRetryInterval missing validation (F4)
  • Nit (P4): 1 - normalizeResource case-sensitive matching (F5)

See inline comments for details on F1, F2, and F4.


[F3] JDK path resolution logic duplicated across 3 build.gradle files (Low / P3)

The ~15-line JDK path detection block (rawJdkPath, isWindows, exeSuffix) is copy-pasted identically in client/build.gradle, azurefunctions/build.gradle, and azuremanaged/build.gradle. Consider extracting to a shared Gradle script (e.g., gradle/jdk-config.gradle) and using apply from: in each module. Not blocking merge but worth a follow-up.

[F5] normalizeResource is case-sensitive (Nit / P4)

startsWith in ManagedIdentityTokenSource.normalizeResource() is case-sensitive. If a user passes HTTPS://MANAGEMENT.CORE.WINDOWS.NET, it won't match. In practice users almost always use lowercase, but using resource.toLowerCase(Locale.ROOT).startsWith(base) would be more robust.


Positive notes: Excellent test coverage (constructor, serialization, round-trip, cross-extension parity tests), correct defensive copies with unmodifiable maps, proper polymorphic Jackson setup for TokenSource, and correct .NET TimeSpan format handling in TimeSpanHelper.

*/
private static String normalizeResource(String resource) {
for (String base : KNOWN_RESOURCE_BASES) {
if (resource.startsWith(base) && !resource.endsWith(DEFAULT_SUFFIX)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[F1] normalizeResource prefix matching too broad - may match unintended domains (High / P1)

resource.startsWith(base) will match substrings beyond the intended base URI:

// Matches! But this is a different domain entirely:
new ManagedIdentityTokenSource("https://management.core.windows.net.evil.com");
// -> "https://management.core.windows.net.evil.com/.default"

// Also matches paths that already have content after the base:
new ManagedIdentityTokenSource("https://management.core.windows.net/some/path");
// -> "https://management.core.windows.net/some/path/.default"

Suggested fix - require exact match or a / boundary:

private static String normalizeResource(String resource) {
    for (String base : KNOWN_RESOURCE_BASES) {
        if ((resource.equals(base) || resource.startsWith(base + "/"))
                && !resource.endsWith(DEFAULT_SUFFIX)) {
            return resource + DEFAULT_SUFFIX;
        }
    }
    return resource;
}

This ensures https://management.core.windows.net.evil.com won't match, while https://management.core.windows.net and https://management.core.windows.net/ still normalize correctly.

@JsonProperty("tokenSource") @Nullable TokenSource tokenSource,
@JsonProperty("asynchronousPatternEnabled") boolean asynchronousPatternEnabled,
@JsonProperty("timeout") @Nullable String timeout,
@JsonProperty("retryOptions") @Nullable HttpRetryOptions httpRetryOptions) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[F2] asynchronousPatternEnabled defaults to false on deserialization when field is absent (Medium / P2)

The @JsonCreator constructor uses Java primitive boolean for asynchronousPatternEnabled. When the JSON payload omits this field, Jackson passes false (the primitive default). However, all public constructors default to true, and the .NET SDK also defaults to true.

Impact: If a JSON payload from another SDK or extension omits asynchronousPatternEnabled, this Java SDK will silently disable HTTP 202 polling - breaking long-running HTTP patterns.

Suggested fix - use Boolean wrapper + null check:

@JsonCreator
DurableHttpRequest(
        ...
        @JsonProperty("asynchronousPatternEnabled") @Nullable Boolean asynchronousPatternEnabled,
        ...) {
    ...
    this.asynchronousPatternEnabled = asynchronousPatternEnabled != null
            ? asynchronousPatternEnabled : true;  // default true, aligned with .NET
    ...
}

Also add a test:

@Test
void deserializeWithoutAsyncPattern_defaultsToTrue() throws Exception {
    String json = "{\"method\":\"GET\",\"uri\":\"https://example.com\"}";
    DurableHttpRequest req = mapper.readValue(json, DurableHttpRequest.class);
    assertTrue(req.isAsynchronousPatternEnabled());
}

public void setMaxRetryInterval(Duration maxRetryInterval) {
this.maxRetryInterval = maxRetryInterval;
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[F4] setMaxRetryInterval accepts null and negative values without validation (Low / P3)

Unlike setBackoffCoefficient which validates its input, setMaxRetryInterval silently accepts null and negative durations. Setting a negative value here won't fail until serialization time (when TimeSpanHelper.format() throws), making the error harder to debug.

Suggested fix - validate at the point of call:

public void setMaxRetryInterval(Duration maxRetryInterval) {
    if (maxRetryInterval != null && (maxRetryInterval.isNegative() || maxRetryInterval.isZero())) {
        throw new IllegalArgumentException("maxRetryInterval must be positive");
    }
    this.maxRetryInterval = maxRetryInterval;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants