Address PR review feedback.

- Return 404 Not Found when no orchestration with the given ID exists
- Return 200 OK for failed workflows (the HTTP operation succeeded;
  the workflow outcome is conveyed via the response body)
- Rename 'status' to 'workflowStatus' in WorkflowRunResponse to avoid
  inconsistency with AgentRunSuccessResponse which uses integer status
- Add optional 'error' field (omitted from JSON when null) to
  WorkflowRunResponse for failed workflow details
This commit is contained in:
Shyju Krishnankutty
2026-04-21 13:35:13 -07:00
parent 06f165425f
commit edf234aa4a
3 changed files with 52 additions and 11 deletions
@@ -107,7 +107,7 @@ curl -X POST http://localhost:7071/api/workflows/CancelOrder/run \
```json
{
"runId": "abc123def456",
"status": "Completed",
"workflowStatus": "Completed",
"result": "Cancellation email sent for order 12345 to jerry@example.com."
}
```
@@ -445,15 +445,28 @@ internal static class BuiltInFunctions
if (metadata is null)
{
return await CreateErrorResponseAsync(req, context, HttpStatusCode.InternalServerError,
$"Workflow orchestration '{instanceId}' returned no metadata.", acceptsJson);
return await CreateErrorResponseAsync(req, context, HttpStatusCode.NotFound,
$"No workflow orchestration with ID '{instanceId}' was found.", acceptsJson);
}
if (metadata.RuntimeStatus is OrchestrationRuntimeStatus.Failed)
{
string errorMessage = metadata.FailureDetails?.ErrorMessage ?? "Unknown error";
return await CreateErrorResponseAsync(req, context, HttpStatusCode.InternalServerError,
$"Workflow orchestration '{instanceId}' failed: {errorMessage}", acceptsJson);
HttpResponseData failedResponse = req.CreateResponse(HttpStatusCode.OK);
if (acceptsJson)
{
await failedResponse.WriteAsJsonAsync(
new WorkflowRunResponse(instanceId, metadata.RuntimeStatus.ToString(), Result: null, Error: errorMessage),
context.CancellationToken);
}
else
{
failedResponse.Headers.Add("Content-Type", "text/plain");
await failedResponse.WriteStringAsync(errorMessage, context.CancellationToken);
}
return failedResponse;
}
if (metadata.RuntimeStatus is not OrchestrationRuntimeStatus.Completed)
@@ -490,7 +503,9 @@ internal static class BuiltInFunctions
}
}
await response.WriteAsJsonAsync(new WorkflowRunSuccessResponse(instanceId, metadata.RuntimeStatus.ToString(), resultElement), context.CancellationToken);
await response.WriteAsJsonAsync(
new WorkflowRunResponse(instanceId, metadata.RuntimeStatus.ToString(), resultElement),
context.CancellationToken);
}
else
{
@@ -687,15 +702,17 @@ internal static class BuiltInFunctions
[property: JsonPropertyName("response")] JsonElement Response);
/// <summary>
/// Represents a successful workflow run response when waiting for completion.
/// Represents a workflow run response when waiting for completion.
/// </summary>
/// <param name="RunId">The orchestration run ID.</param>
/// <param name="Status">The orchestration runtime status.</param>
/// <param name="WorkflowStatus">The orchestration runtime status (e.g., "Completed", "Failed").</param>
/// <param name="Result">The workflow result as a JSON element so POCOs serialize as nested objects rather than escaped strings.</param>
private sealed record WorkflowRunSuccessResponse(
/// <param name="Error">An optional error message when the workflow has failed.</param>
private sealed record WorkflowRunResponse(
[property: JsonPropertyName("runId")] string RunId,
[property: JsonPropertyName("status")] string Status,
[property: JsonPropertyName("result")] JsonElement? Result);
[property: JsonPropertyName("workflowStatus")] string WorkflowStatus,
[property: JsonPropertyName("result")] JsonElement? Result,
[property: JsonPropertyName("error"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] string? Error = null);
/// <summary>
/// A service provider that combines the original service provider with an additional DurableTaskClient instance.
@@ -3,6 +3,7 @@
using System.Diagnostics;
using System.Reflection;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Client;
@@ -141,6 +142,29 @@ public sealed class WorkflowSamplesValidation(ITestOutputHelper outputHelper) :
// The response should contain the workflow result (not just "started for CancelOrder")
Assert.DoesNotContain("Workflow orchestration started", waitResponseText);
Assert.Contains("55555", waitResponseText);
// Test the wait-for-response with Accept: application/json header
this._outputHelper.WriteLine("Starting CancelOrder workflow with x-ms-wait-for-response and Accept: application/json...");
using HttpRequestMessage jsonWaitRequest = new(HttpMethod.Post, cancelOrderUri);
jsonWaitRequest.Content = new StringContent("77777", Encoding.UTF8, "text/plain");
jsonWaitRequest.Headers.Add("x-ms-wait-for-response", "true");
jsonWaitRequest.Headers.Add("Accept", "application/json");
using CancellationTokenSource jsonWaitCts = new(s_orchestrationTimeout);
using HttpResponseMessage jsonWaitResponse = await s_sharedHttpClient.SendAsync(jsonWaitRequest, jsonWaitCts.Token);
Assert.True(jsonWaitResponse.IsSuccessStatusCode, $"CancelOrder JSON wait-for-response request failed with status: {jsonWaitResponse.StatusCode}");
string jsonWaitResponseText = await jsonWaitResponse.Content.ReadAsStringAsync();
this._outputHelper.WriteLine($"CancelOrder JSON wait-for-response result: {jsonWaitResponseText}");
using JsonDocument jsonDoc = JsonDocument.Parse(jsonWaitResponseText);
JsonElement root = jsonDoc.RootElement;
Assert.True(root.TryGetProperty("runId", out _), "JSON response missing 'runId' property");
Assert.True(root.TryGetProperty("workflowStatus", out JsonElement statusEl), "JSON response missing 'workflowStatus' property");
Assert.Equal("Completed", statusEl.GetString());
Assert.True(root.TryGetProperty("result", out JsonElement resultEl), "JSON response missing 'result' property");
Assert.Contains("77777", resultEl.GetString());
});
}