Skip to content
Merged
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
4 changes: 4 additions & 0 deletions azure-functions-durable/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ avoiding repeated allocation of unused worker resources.

FIXED

- Check-status responses now include the standard `Retry-After: 10` polling
header. Failed orchestrations return HTTP 200 from the wait helper by default;
callers can request HTTP 500 responses through
`return_internal_server_error_on_failure`.
- HTTP management payloads now preserve the host-provided management URL
templates and configured HTTP base paths, include `rewindPostUri`, encode
instance IDs, and use forwarded request origins when enabled by the host.
Expand Down
103 changes: 81 additions & 22 deletions azure-functions-durable/azure/durable_functions/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ def _build_http_management_payload(
http_base_url: str,
required_query_string_parameters: str,
use_forwarded_host: bool,
request: func.HttpRequest | None) -> HttpManagementPayload:
request: func.HttpRequest | None,
return_internal_server_error_on_failure: bool = False) -> HttpManagementPayload:
encoded_instance_id = quote(instance_id, safe="")
configured_base_url = http_base_url or base_url
request_origin: str | None = None
Expand All @@ -113,7 +114,9 @@ def _build_http_management_payload(
instance_status_url,
required_query_string_parameters,
management_urls=management_urls,
request_origin=request_origin)
request_origin=request_origin,
return_internal_server_error_on_failure=(
return_internal_server_error_on_failure))


# Client class used for Durable Functions
Expand Down Expand Up @@ -247,30 +250,43 @@ def _parse_client_configuration(self, client_as_string: str) -> None:
# TODO: convert the string value back to timedelta - annoying regex?
self.grpcHttpClientTimeout = client.get("grpcHttpClientTimeout") or timedelta(seconds=30)

def create_check_status_response(self, request: func.HttpRequest, instance_id: str) -> func.HttpResponse:
def create_check_status_response(
self,
request: func.HttpRequest,
instance_id: str,
return_internal_server_error_on_failure: bool = False
) -> func.HttpResponse:
"""Creates an HTTP response for checking the status of a Durable Function instance.

Args:
request (func.HttpRequest): The incoming HTTP request.
instance_id (str): The ID of the Durable Function instance.
return_internal_server_error_on_failure (bool): Whether status
queries should return HTTP 500 for failed orchestrations.
"""
payload = self._get_client_response_links(request, instance_id)
payload = self._get_client_response_links(
request,
instance_id,
return_internal_server_error_on_failure)
return func.HttpResponse(
body=str(payload),
status_code=202,
headers={
'content-type': 'application/json',
'Content-Type': 'application/json',
# Match v1: Location points at statusQueryGetUri, which includes
# the required query string (webhook key / task hub / connection)
# so a client that follows the header is authorized.
'Location': payload['statusQueryGetUri'],
'Retry-After': '10',
},
)

def create_http_management_payload(
self,
request: func.HttpRequest | str | None = None,
instance_id: str | None = None) -> HttpManagementPayload:
instance_id: str | None = None,
return_internal_server_error_on_failure: bool = False
) -> HttpManagementPayload:
"""Creates an HTTP management payload for a Durable Function instance.

Two call styles are supported:
Expand All @@ -286,6 +302,8 @@ def create_http_management_payload(
for backwards compatibility, the instance ID when called with a
single positional argument.
instance_id (str | None): The ID of the Durable Function instance.
return_internal_server_error_on_failure (bool): Whether the status
query should return HTTP 500 for failed orchestrations.
"""
# Backwards-compatibility: v1 accepted a single positional ``instance_id``.
if instance_id is None and isinstance(request, str):
Expand All @@ -294,17 +312,26 @@ def create_http_management_payload(
if instance_id is None:
raise TypeError("instance_id is required")
resolved_request = request if isinstance(request, func.HttpRequest) else None
return self._get_client_response_links(resolved_request, instance_id)
return self._get_client_response_links(
resolved_request,
instance_id,
return_internal_server_error_on_failure)

def _get_client_response_links(self, request: func.HttpRequest | None, instance_id: str) -> HttpManagementPayload:
def _get_client_response_links(
self,
request: func.HttpRequest | None,
instance_id: str,
return_internal_server_error_on_failure: bool = False
) -> HttpManagementPayload:
return _build_http_management_payload(
instance_id,
self.managementUrls,
self.baseUrl,
self.httpBaseUrl,
self.requiredQueryStringParameters,
self.useForwardedHost,
request)
request,
return_internal_server_error_on_failure)

# ------------------------------------------------------------------
# Backwards-compatibility shims for the v1 azure-functions-durable
Expand Down Expand Up @@ -521,7 +548,9 @@ async def wait_for_completion_or_create_check_status_response(
request: func.HttpRequest,
instance_id: str,
timeout_in_milliseconds: int = 10000,
retry_interval_in_milliseconds: int = 1000) -> func.HttpResponse:
retry_interval_in_milliseconds: int = 1000,
return_internal_server_error_on_failure: bool = False
) -> func.HttpResponse:
"""Wait for an orchestration to complete, or return a check-status response.

If the orchestration completes within the timeout, an HTTP response
Expand All @@ -530,6 +559,9 @@ async def wait_for_completion_or_create_check_status_response(

The ``retry_interval_in_milliseconds`` argument has no durabletask
equivalent (durabletask waits server-side) and is ignored.

When ``return_internal_server_error_on_failure`` is true, failed
orchestrations return HTTP 500 instead of HTTP 200.
"""
if retry_interval_in_milliseconds > timeout_in_milliseconds:
raise Exception(
Expand All @@ -540,10 +572,16 @@ async def wait_for_completion_or_create_check_status_response(
state = await self.wait_for_orchestration_completion(
instance_id, timeout=timeout_in_milliseconds / 1000)
except TimeoutError:
return self.create_check_status_response(request, instance_id)
return self.create_check_status_response(
request,
instance_id,
return_internal_server_error_on_failure)

if state is None:
return self.create_check_status_response(request, instance_id)
return self.create_check_status_response(
request,
instance_id,
return_internal_server_error_on_failure)

if state.runtime_status == OrchestrationStatus.COMPLETED:
return self._create_http_response(200, state.serialized_output)
Expand All @@ -552,8 +590,12 @@ async def wait_for_completion_or_create_check_status_response(
200, DurableOrchestrationStatus.from_orchestration_state(state).to_json())
if state.runtime_status == OrchestrationStatus.FAILED:
return self._create_http_response(
500, DurableOrchestrationStatus.from_orchestration_state(state).to_json())
return self.create_check_status_response(request, instance_id)
500 if return_internal_server_error_on_failure else 200,
DurableOrchestrationStatus.from_orchestration_state(state).to_json())
return self.create_check_status_response(
request,
instance_id,
return_internal_server_error_on_failure)

@deprecated("rewind is deprecated; use rewind_orchestration instead.")
async def rewind(
Expand Down Expand Up @@ -649,40 +691,57 @@ def _parse_client_configuration(self, client_as_string: str) -> None:
"grpcHttpClientTimeout") or timedelta(seconds=30)

def create_check_status_response(
self, request: func.HttpRequest, instance_id: str) -> func.HttpResponse:
payload = self._get_client_response_links(request, instance_id)
self,
request: func.HttpRequest,
instance_id: str,
return_internal_server_error_on_failure: bool = False
) -> func.HttpResponse:
payload = self._get_client_response_links(
request,
instance_id,
return_internal_server_error_on_failure)
return func.HttpResponse(
body=str(payload),
status_code=202,
headers={
"content-type": "application/json",
"Content-Type": "application/json",
"Location": payload["statusQueryGetUri"],
"Retry-After": "10",
},
)

def create_http_management_payload(
self,
request: func.HttpRequest | str | None = None,
instance_id: str | None = None) -> HttpManagementPayload:
instance_id: str | None = None,
return_internal_server_error_on_failure: bool = False
) -> HttpManagementPayload:
if instance_id is None and isinstance(request, str):
instance_id = request
request = None
if instance_id is None:
raise TypeError("instance_id is required")
resolved_request = request if isinstance(request, func.HttpRequest) else None
return self._get_client_response_links(resolved_request, instance_id)
return self._get_client_response_links(
resolved_request,
instance_id,
return_internal_server_error_on_failure)

def _get_client_response_links(
self, request: func.HttpRequest | None,
instance_id: str) -> HttpManagementPayload:
self,
request: func.HttpRequest | None,
instance_id: str,
return_internal_server_error_on_failure: bool = False
) -> HttpManagementPayload:
return _build_http_management_payload(
instance_id,
self.managementUrls,
self.baseUrl,
self.httpBaseUrl,
self.requiredQueryStringParameters,
self.useForwardedHost,
request)
request,
return_internal_server_error_on_failure)


def _close_cached_sync_clients() -> None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ def __init__(
required_query_string_parameters: str,
*,
management_urls: Mapping[str, str] | None = None,
request_origin: str | None = None):
request_origin: str | None = None,
return_internal_server_error_on_failure: bool = False):
"""Initializes the HttpManagementPayload with the necessary URLs.

Args:
Expand All @@ -40,6 +41,8 @@ def __init__(
provided by the Durable extension.
request_origin (str | None): Externally visible request origin used
to replace the templates' internal origin.
return_internal_server_error_on_failure (bool): Whether the status
query should return HTTP 500 for failed orchestrations.
"""
fallback_urls = {
'purgeHistoryDeleteUri': instance_status_url + "?" + required_query_string_parameters,
Expand Down Expand Up @@ -67,6 +70,20 @@ def __init__(
url = url.replace(_INSTANCE_ID_PLACEHOLDER, encoded_instance_id)
urls[name] = replace_url_origin(url, request_origin)

if return_internal_server_error_on_failure:
status_url = urlsplit(urls["statusQueryGetUri"])
query = status_url.query
if query:
query += "&"
query += "returnInternalServerErrorOnFailure=true"
urls["statusQueryGetUri"] = urlunsplit((
status_url.scheme,
status_url.netloc,
status_url.path,
query,
status_url.fragment,
))

super().__init__(urls)

def __str__(self) -> str:
Expand Down
Loading
Loading