Skip to content

Commit 387cd1e

Browse files
andystaplesCopilot
andcommitted
Merge main into show-history compatibility
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a28a3fa8-303a-4a74-8fbc-8e062e39d97f
2 parents 7c2ef97 + 85e804a commit 387cd1e

4 files changed

Lines changed: 232 additions & 40 deletions

File tree

azure-functions-durable/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ FIXED
3535
- The v1-compatible `get_status()` API now supports `show_history` and
3636
`show_history_output`, including compacted `historyEvents` output without an
3737
additional history request when history is not requested.
38+
- Check-status responses now include the standard `Retry-After: 10` polling
39+
header. Failed orchestrations return HTTP 200 from the wait helper by default;
40+
callers can request HTTP 500 responses through
41+
`return_internal_server_error_on_failure`.
3842
- HTTP management payloads now preserve the host-provided management URL
3943
templates and configured HTTP base paths, include `rewindPostUri`, encode
4044
instance IDs, and use forwarded request origins when enabled by the host.

azure-functions-durable/azure/durable_functions/client.py

Lines changed: 81 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@ def _build_http_management_payload(
9292
http_base_url: str,
9393
required_query_string_parameters: str,
9494
use_forwarded_host: bool,
95-
request: func.HttpRequest | None) -> HttpManagementPayload:
95+
request: func.HttpRequest | None,
96+
return_internal_server_error_on_failure: bool = False) -> HttpManagementPayload:
9697
encoded_instance_id = quote(instance_id, safe="")
9798
configured_base_url = http_base_url or base_url
9899
request_origin: str | None = None
@@ -114,7 +115,9 @@ def _build_http_management_payload(
114115
instance_status_url,
115116
required_query_string_parameters,
116117
management_urls=management_urls,
117-
request_origin=request_origin)
118+
request_origin=request_origin,
119+
return_internal_server_error_on_failure=(
120+
return_internal_server_error_on_failure))
118121

119122

120123
# Client class used for Durable Functions
@@ -248,30 +251,43 @@ def _parse_client_configuration(self, client_as_string: str) -> None:
248251
# TODO: convert the string value back to timedelta - annoying regex?
249252
self.grpcHttpClientTimeout = client.get("grpcHttpClientTimeout") or timedelta(seconds=30)
250253

251-
def create_check_status_response(self, request: func.HttpRequest, instance_id: str) -> func.HttpResponse:
254+
def create_check_status_response(
255+
self,
256+
request: func.HttpRequest,
257+
instance_id: str,
258+
return_internal_server_error_on_failure: bool = False
259+
) -> func.HttpResponse:
252260
"""Creates an HTTP response for checking the status of a Durable Function instance.
253261
254262
Args:
255263
request (func.HttpRequest): The incoming HTTP request.
256264
instance_id (str): The ID of the Durable Function instance.
265+
return_internal_server_error_on_failure (bool): Whether status
266+
queries should return HTTP 500 for failed orchestrations.
257267
"""
258-
payload = self._get_client_response_links(request, instance_id)
268+
payload = self._get_client_response_links(
269+
request,
270+
instance_id,
271+
return_internal_server_error_on_failure)
259272
return func.HttpResponse(
260273
body=str(payload),
261274
status_code=202,
262275
headers={
263-
'content-type': 'application/json',
276+
'Content-Type': 'application/json',
264277
# Match v1: Location points at statusQueryGetUri, which includes
265278
# the required query string (webhook key / task hub / connection)
266279
# so a client that follows the header is authorized.
267280
'Location': payload['statusQueryGetUri'],
281+
'Retry-After': '10',
268282
},
269283
)
270284

271285
def create_http_management_payload(
272286
self,
273287
request: func.HttpRequest | str | None = None,
274-
instance_id: str | None = None) -> HttpManagementPayload:
288+
instance_id: str | None = None,
289+
return_internal_server_error_on_failure: bool = False
290+
) -> HttpManagementPayload:
275291
"""Creates an HTTP management payload for a Durable Function instance.
276292
277293
Two call styles are supported:
@@ -287,6 +303,8 @@ def create_http_management_payload(
287303
for backwards compatibility, the instance ID when called with a
288304
single positional argument.
289305
instance_id (str | None): The ID of the Durable Function instance.
306+
return_internal_server_error_on_failure (bool): Whether the status
307+
query should return HTTP 500 for failed orchestrations.
290308
"""
291309
# Backwards-compatibility: v1 accepted a single positional ``instance_id``.
292310
if instance_id is None and isinstance(request, str):
@@ -295,17 +313,26 @@ def create_http_management_payload(
295313
if instance_id is None:
296314
raise TypeError("instance_id is required")
297315
resolved_request = request if isinstance(request, func.HttpRequest) else None
298-
return self._get_client_response_links(resolved_request, instance_id)
316+
return self._get_client_response_links(
317+
resolved_request,
318+
instance_id,
319+
return_internal_server_error_on_failure)
299320

300-
def _get_client_response_links(self, request: func.HttpRequest | None, instance_id: str) -> HttpManagementPayload:
321+
def _get_client_response_links(
322+
self,
323+
request: func.HttpRequest | None,
324+
instance_id: str,
325+
return_internal_server_error_on_failure: bool = False
326+
) -> HttpManagementPayload:
301327
return _build_http_management_payload(
302328
instance_id,
303329
self.managementUrls,
304330
self.baseUrl,
305331
self.httpBaseUrl,
306332
self.requiredQueryStringParameters,
307333
self.useForwardedHost,
308-
request)
334+
request,
335+
return_internal_server_error_on_failure)
309336

310337
# ------------------------------------------------------------------
311338
# Backwards-compatibility shims for the v1 azure-functions-durable
@@ -534,7 +561,9 @@ async def wait_for_completion_or_create_check_status_response(
534561
request: func.HttpRequest,
535562
instance_id: str,
536563
timeout_in_milliseconds: int = 10000,
537-
retry_interval_in_milliseconds: int = 1000) -> func.HttpResponse:
564+
retry_interval_in_milliseconds: int = 1000,
565+
return_internal_server_error_on_failure: bool = False
566+
) -> func.HttpResponse:
538567
"""Wait for an orchestration to complete, or return a check-status response.
539568
540569
If the orchestration completes within the timeout, an HTTP response
@@ -543,6 +572,9 @@ async def wait_for_completion_or_create_check_status_response(
543572
544573
The ``retry_interval_in_milliseconds`` argument has no durabletask
545574
equivalent (durabletask waits server-side) and is ignored.
575+
576+
When ``return_internal_server_error_on_failure`` is true, failed
577+
orchestrations return HTTP 500 instead of HTTP 200.
546578
"""
547579
if retry_interval_in_milliseconds > timeout_in_milliseconds:
548580
raise Exception(
@@ -553,10 +585,16 @@ async def wait_for_completion_or_create_check_status_response(
553585
state = await self.wait_for_orchestration_completion(
554586
instance_id, timeout=timeout_in_milliseconds / 1000)
555587
except TimeoutError:
556-
return self.create_check_status_response(request, instance_id)
588+
return self.create_check_status_response(
589+
request,
590+
instance_id,
591+
return_internal_server_error_on_failure)
557592

558593
if state is None:
559-
return self.create_check_status_response(request, instance_id)
594+
return self.create_check_status_response(
595+
request,
596+
instance_id,
597+
return_internal_server_error_on_failure)
560598

561599
if state.runtime_status == OrchestrationStatus.COMPLETED:
562600
return self._create_http_response(200, state.serialized_output)
@@ -565,8 +603,12 @@ async def wait_for_completion_or_create_check_status_response(
565603
200, DurableOrchestrationStatus.from_orchestration_state(state).to_json())
566604
if state.runtime_status == OrchestrationStatus.FAILED:
567605
return self._create_http_response(
568-
500, DurableOrchestrationStatus.from_orchestration_state(state).to_json())
569-
return self.create_check_status_response(request, instance_id)
606+
500 if return_internal_server_error_on_failure else 200,
607+
DurableOrchestrationStatus.from_orchestration_state(state).to_json())
608+
return self.create_check_status_response(
609+
request,
610+
instance_id,
611+
return_internal_server_error_on_failure)
570612

571613
@deprecated("rewind is deprecated; use rewind_orchestration instead.")
572614
async def rewind(
@@ -662,40 +704,57 @@ def _parse_client_configuration(self, client_as_string: str) -> None:
662704
"grpcHttpClientTimeout") or timedelta(seconds=30)
663705

664706
def create_check_status_response(
665-
self, request: func.HttpRequest, instance_id: str) -> func.HttpResponse:
666-
payload = self._get_client_response_links(request, instance_id)
707+
self,
708+
request: func.HttpRequest,
709+
instance_id: str,
710+
return_internal_server_error_on_failure: bool = False
711+
) -> func.HttpResponse:
712+
payload = self._get_client_response_links(
713+
request,
714+
instance_id,
715+
return_internal_server_error_on_failure)
667716
return func.HttpResponse(
668717
body=str(payload),
669718
status_code=202,
670719
headers={
671-
"content-type": "application/json",
720+
"Content-Type": "application/json",
672721
"Location": payload["statusQueryGetUri"],
722+
"Retry-After": "10",
673723
},
674724
)
675725

676726
def create_http_management_payload(
677727
self,
678728
request: func.HttpRequest | str | None = None,
679-
instance_id: str | None = None) -> HttpManagementPayload:
729+
instance_id: str | None = None,
730+
return_internal_server_error_on_failure: bool = False
731+
) -> HttpManagementPayload:
680732
if instance_id is None and isinstance(request, str):
681733
instance_id = request
682734
request = None
683735
if instance_id is None:
684736
raise TypeError("instance_id is required")
685737
resolved_request = request if isinstance(request, func.HttpRequest) else None
686-
return self._get_client_response_links(resolved_request, instance_id)
738+
return self._get_client_response_links(
739+
resolved_request,
740+
instance_id,
741+
return_internal_server_error_on_failure)
687742

688743
def _get_client_response_links(
689-
self, request: func.HttpRequest | None,
690-
instance_id: str) -> HttpManagementPayload:
744+
self,
745+
request: func.HttpRequest | None,
746+
instance_id: str,
747+
return_internal_server_error_on_failure: bool = False
748+
) -> HttpManagementPayload:
691749
return _build_http_management_payload(
692750
instance_id,
693751
self.managementUrls,
694752
self.baseUrl,
695753
self.httpBaseUrl,
696754
self.requiredQueryStringParameters,
697755
self.useForwardedHost,
698-
request)
756+
request,
757+
return_internal_server_error_on_failure)
699758

700759

701760
def _close_cached_sync_clients() -> None:

azure-functions-durable/azure/durable_functions/http/http_management_payload.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ def __init__(
2929
required_query_string_parameters: str,
3030
*,
3131
management_urls: Mapping[str, str] | None = None,
32-
request_origin: str | None = None):
32+
request_origin: str | None = None,
33+
return_internal_server_error_on_failure: bool = False):
3334
"""Initializes the HttpManagementPayload with the necessary URLs.
3435
3536
Args:
@@ -40,6 +41,8 @@ def __init__(
4041
provided by the Durable extension.
4142
request_origin (str | None): Externally visible request origin used
4243
to replace the templates' internal origin.
44+
return_internal_server_error_on_failure (bool): Whether the status
45+
query should return HTTP 500 for failed orchestrations.
4346
"""
4447
fallback_urls = {
4548
'purgeHistoryDeleteUri': instance_status_url + "?" + required_query_string_parameters,
@@ -67,6 +70,20 @@ def __init__(
6770
url = url.replace(_INSTANCE_ID_PLACEHOLDER, encoded_instance_id)
6871
urls[name] = replace_url_origin(url, request_origin)
6972

73+
if return_internal_server_error_on_failure:
74+
status_url = urlsplit(urls["statusQueryGetUri"])
75+
query = status_url.query
76+
if query:
77+
query += "&"
78+
query += "returnInternalServerErrorOnFailure=true"
79+
urls["statusQueryGetUri"] = urlunsplit((
80+
status_url.scheme,
81+
status_url.netloc,
82+
status_url.path,
83+
query,
84+
status_url.fragment,
85+
))
86+
7087
super().__init__(urls)
7188

7289
def __str__(self) -> str:

0 commit comments

Comments
 (0)