Skip to content
Open
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 @@ -156,6 +156,10 @@ public static async Task<HttpResponseData> RunWorkflowOrchestrationHttpTriggerAs
/// Returns the workflow status including any pending HITL requests.
/// The run ID is extracted from the route parameter <c>{runId}</c>.
/// </summary>
/// <remarks>
/// This endpoint has no plain-text representation, so it always responds with JSON on both
/// success and failure regardless of the request's <c>Accept</c> header.
/// </remarks>
public static async Task<HttpResponseData> GetWorkflowStatusAsync(
[HttpTrigger] HttpRequestData req,
[DurableClient] DurableTaskClient client,
Expand All @@ -164,13 +168,13 @@ public static async Task<HttpResponseData> GetWorkflowStatusAsync(
string? runId = context.BindingContext.BindingData.TryGetValue("runId", out object? value) ? value?.ToString() : null;
if (string.IsNullOrEmpty(runId))
{
return await CreateErrorResponseAsync(req, context, HttpStatusCode.BadRequest, "Run ID is required.");
return await CreateErrorResponseAsync(req, context, HttpStatusCode.BadRequest, "Run ID is required.", acceptsJson: true);
}

OrchestrationMetadata? metadata = await client.GetInstanceAsync(runId, getInputsAndOutputs: true);
if (metadata is null || !IsOrchestrationOwnedByWorkflow(metadata.Name, context.FunctionDefinition.Name, StatusFunctionSuffix))
{
return await CreateErrorResponseAsync(req, context, HttpStatusCode.NotFound, $"Workflow run '{runId}' not found.");
return await CreateErrorResponseAsync(req, context, HttpStatusCode.NotFound, $"Workflow run '{runId}' not found.", acceptsJson: true);
}

// Parse HITL inputs the workflow is waiting for from the durable workflow status
Expand All @@ -195,6 +199,10 @@ await response.WriteAsJsonAsync(new
/// Sends a response to a pending RequestPort, resuming the workflow.
/// Expects a JSON body: <c>{ "eventName": "...", "response": { ... } }</c>.
/// </summary>
/// <remarks>
/// This endpoint has no plain-text representation, so it always responds with JSON on both
/// success and failure regardless of the request's <c>Accept</c> header.
/// </remarks>
public static async Task<HttpResponseData> RespondToWorkflowAsync(
[HttpTrigger] HttpRequestData req,
[DurableClient] DurableTaskClient client,
Expand All @@ -203,38 +211,40 @@ public static async Task<HttpResponseData> RespondToWorkflowAsync(
string? runId = context.BindingContext.BindingData.TryGetValue("runId", out object? value) ? value?.ToString() : null;
if (string.IsNullOrEmpty(runId))
{
return await CreateErrorResponseAsync(req, context, HttpStatusCode.BadRequest, "Run ID is required.");
return await CreateErrorResponseAsync(req, context, HttpStatusCode.BadRequest, "Run ID is required.", acceptsJson: true);
}

WorkflowRespondRequest? request;
try
{
request = await req.ReadFromJsonAsync<WorkflowRespondRequest>(context.CancellationToken);
}
catch (JsonException)
catch (Exception ex) when (ex is JsonException or AggregateException { InnerException: JsonException })
{
return await CreateErrorResponseAsync(req, context, HttpStatusCode.BadRequest, "Request body is not valid JSON.");
// ReadFromJsonAsync surfaces deserialization failures through a ContinueWith that reads
// Task.Result, so a malformed body arrives wrapped in an AggregateException.
return await CreateErrorResponseAsync(req, context, HttpStatusCode.BadRequest, "Request body is not valid JSON.", acceptsJson: true);
}

if (request is null || string.IsNullOrEmpty(request.EventName)
|| request.Response.ValueKind == JsonValueKind.Undefined)
{
return await CreateErrorResponseAsync(req, context, HttpStatusCode.BadRequest, "Body must contain a non-empty 'eventName' and a 'response' property.");
return await CreateErrorResponseAsync(req, context, HttpStatusCode.BadRequest, "Body must contain a non-empty 'eventName' and a 'response' property.", acceptsJson: true);
}

// Verify the orchestration exists and is in a valid state
OrchestrationMetadata? metadata = await client.GetInstanceAsync(runId, getInputsAndOutputs: true);
if (metadata is null || !IsOrchestrationOwnedByWorkflow(metadata.Name, context.FunctionDefinition.Name, RespondFunctionSuffix))
{
return await CreateErrorResponseAsync(req, context, HttpStatusCode.NotFound, $"Workflow run '{runId}' not found.");
return await CreateErrorResponseAsync(req, context, HttpStatusCode.NotFound, $"Workflow run '{runId}' not found.", acceptsJson: true);
}

if (metadata.RuntimeStatus is OrchestrationRuntimeStatus.Completed
or OrchestrationRuntimeStatus.Failed
or OrchestrationRuntimeStatus.Terminated)
{
return await CreateErrorResponseAsync(req, context, HttpStatusCode.BadRequest,
$"Workflow run '{runId}' is in terminal state '{metadata.RuntimeStatus}'.");
$"Workflow run '{runId}' is in terminal state '{metadata.RuntimeStatus}'.", acceptsJson: true);
}

// Verify the workflow is waiting for the specified event.
Expand All @@ -246,7 +256,7 @@ or OrchestrationRuntimeStatus.Failed
if (!liveStatus.PendingEvents.Exists(p => string.Equals(p.EventName, request.EventName, StringComparison.Ordinal)))
{
return await CreateErrorResponseAsync(req, context, HttpStatusCode.BadRequest,
$"Workflow is not waiting for event '{request.EventName}'.");
$"Workflow is not waiting for event '{request.EventName}'.", acceptsJson: true);
}

eventValidated = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## [Unreleased]

- [BREAKING] Always return JSON from the workflow status and respond endpoints, including on errors and when the request sends no `Accept` header, and fix malformed request bodies surfacing as an unhandled error instead of `400 Bad Request` ([#60](https://github.com/microsoft/agent-framework-durable-extension/pull/60))
- [BREAKING] Support bounded synchronous workflow HTTP invocation through query parameters and default workflow run responses to JSON, with `Accept: text/plain` available for the legacy text format and the same negotiated asynchronous response returned on timeout ([#52](https://github.com/microsoft/agent-framework-durable-extension/pull/52))
- Added `DurableTaskClient.AsWorkflowClient` so functions can invoke durable workflows without constructing an `HttpClient` ([#48](https://github.com/microsoft/agent-framework-durable-extension/pull/48))
- [BREAKING] Consolidated the `AddWorkflow` extension overloads into a single method with optional `enableStatusEndpoint` and `enableMcpToolTrigger` parameters, and changed it to return `DurableWorkflowOptions` instead of `void` so multiple workflows can be registered fluently ([#39](https://github.com/microsoft/agent-framework-durable-extension/pull/39))
Expand Down
Loading
Loading