Conversation
…fix Windows build compatibility
There was a problem hiding this comment.
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
DurableHttpAPIs 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.
client/src/main/java/com/microsoft/durabletask/HttpRetryOptions.java
Outdated
Show resolved
Hide resolved
client/src/main/java/com/microsoft/durabletask/TimeSpanHelper.java
Outdated
Show resolved
Hide resolved
client/src/main/java/com/microsoft/durabletask/DurableHttpResponse.java
Dismissed
Show dismissed
Hide dismissed
client/src/main/java/com/microsoft/durabletask/DurableHttpRequest.java
Dismissed
Show dismissed
Hide dismissed
YunchuWang
left a comment
There was a problem hiding this comment.
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 -
normalizeResourceprefix matching too broad (F1) - Medium (P2): 1 -
asynchronousPatternEnableddefault mismatch on deserialization (F2) - Low (P3): 2 - duplicated JDK path logic in build files (F3),
setMaxRetryIntervalmissing validation (F4) - Nit (P4): 1 -
normalizeResourcecase-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)) { |
There was a problem hiding this comment.
[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) { |
There was a problem hiding this comment.
[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; | ||
| } | ||
|
|
There was a problem hiding this comment.
[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;
}
Issue describing the changes in this PR
Adds support for Calls to HTTP endpoints to the Java SDK.
Pull request checklist
CHANGELOG.md