Skip to content

Commit 85e804a

Browse files
authored
Restore check-status response parity (#247)
* Restore check-status response parity Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4608fda7-552e-4297-a493-96bbeac57614 * Use canonical content type header casing Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4608fda7-552e-4297-a493-96bbeac57614 --------- Copilot-Session: 4608fda7-552e-4297-a493-96bbeac57614
1 parent 6d929cd commit 85e804a

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
@@ -32,6 +32,10 @@ avoiding repeated allocation of unused worker resources.
3232

3333
FIXED
3434

35+
- Check-status responses now include the standard `Retry-After: 10` polling
36+
header. Failed orchestrations return HTTP 200 from the wait helper by default;
37+
callers can request HTTP 500 responses through
38+
`return_internal_server_error_on_failure`.
3539
- HTTP management payloads now preserve the host-provided management URL
3640
templates and configured HTTP base paths, include `rewindPostUri`, encode
3741
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
@@ -91,7 +91,8 @@ def _build_http_management_payload(
9191
http_base_url: str,
9292
required_query_string_parameters: str,
9393
use_forwarded_host: bool,
94-
request: func.HttpRequest | None) -> HttpManagementPayload:
94+
request: func.HttpRequest | None,
95+
return_internal_server_error_on_failure: bool = False) -> HttpManagementPayload:
9596
encoded_instance_id = quote(instance_id, safe="")
9697
configured_base_url = http_base_url or base_url
9798
request_origin: str | None = None
@@ -113,7 +114,9 @@ def _build_http_management_payload(
113114
instance_status_url,
114115
required_query_string_parameters,
115116
management_urls=management_urls,
116-
request_origin=request_origin)
117+
request_origin=request_origin,
118+
return_internal_server_error_on_failure=(
119+
return_internal_server_error_on_failure))
117120

118121

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

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

270284
def create_http_management_payload(
271285
self,
272286
request: func.HttpRequest | str | None = None,
273-
instance_id: str | None = None) -> HttpManagementPayload:
287+
instance_id: str | None = None,
288+
return_internal_server_error_on_failure: bool = False
289+
) -> HttpManagementPayload:
274290
"""Creates an HTTP management payload for a Durable Function instance.
275291
276292
Two call styles are supported:
@@ -286,6 +302,8 @@ def create_http_management_payload(
286302
for backwards compatibility, the instance ID when called with a
287303
single positional argument.
288304
instance_id (str | None): The ID of the Durable Function instance.
305+
return_internal_server_error_on_failure (bool): Whether the status
306+
query should return HTTP 500 for failed orchestrations.
289307
"""
290308
# Backwards-compatibility: v1 accepted a single positional ``instance_id``.
291309
if instance_id is None and isinstance(request, str):
@@ -294,17 +312,26 @@ def create_http_management_payload(
294312
if instance_id is None:
295313
raise TypeError("instance_id is required")
296314
resolved_request = request if isinstance(request, func.HttpRequest) else None
297-
return self._get_client_response_links(resolved_request, instance_id)
315+
return self._get_client_response_links(
316+
resolved_request,
317+
instance_id,
318+
return_internal_server_error_on_failure)
298319

299-
def _get_client_response_links(self, request: func.HttpRequest | None, instance_id: str) -> HttpManagementPayload:
320+
def _get_client_response_links(
321+
self,
322+
request: func.HttpRequest | None,
323+
instance_id: str,
324+
return_internal_server_error_on_failure: bool = False
325+
) -> HttpManagementPayload:
300326
return _build_http_management_payload(
301327
instance_id,
302328
self.managementUrls,
303329
self.baseUrl,
304330
self.httpBaseUrl,
305331
self.requiredQueryStringParameters,
306332
self.useForwardedHost,
307-
request)
333+
request,
334+
return_internal_server_error_on_failure)
308335

309336
# ------------------------------------------------------------------
310337
# Backwards-compatibility shims for the v1 azure-functions-durable
@@ -521,7 +548,9 @@ async def wait_for_completion_or_create_check_status_response(
521548
request: func.HttpRequest,
522549
instance_id: str,
523550
timeout_in_milliseconds: int = 10000,
524-
retry_interval_in_milliseconds: int = 1000) -> func.HttpResponse:
551+
retry_interval_in_milliseconds: int = 1000,
552+
return_internal_server_error_on_failure: bool = False
553+
) -> func.HttpResponse:
525554
"""Wait for an orchestration to complete, or return a check-status response.
526555
527556
If the orchestration completes within the timeout, an HTTP response
@@ -530,6 +559,9 @@ async def wait_for_completion_or_create_check_status_response(
530559
531560
The ``retry_interval_in_milliseconds`` argument has no durabletask
532561
equivalent (durabletask waits server-side) and is ignored.
562+
563+
When ``return_internal_server_error_on_failure`` is true, failed
564+
orchestrations return HTTP 500 instead of HTTP 200.
533565
"""
534566
if retry_interval_in_milliseconds > timeout_in_milliseconds:
535567
raise Exception(
@@ -540,10 +572,16 @@ async def wait_for_completion_or_create_check_status_response(
540572
state = await self.wait_for_orchestration_completion(
541573
instance_id, timeout=timeout_in_milliseconds / 1000)
542574
except TimeoutError:
543-
return self.create_check_status_response(request, instance_id)
575+
return self.create_check_status_response(
576+
request,
577+
instance_id,
578+
return_internal_server_error_on_failure)
544579

545580
if state is None:
546-
return self.create_check_status_response(request, instance_id)
581+
return self.create_check_status_response(
582+
request,
583+
instance_id,
584+
return_internal_server_error_on_failure)
547585

548586
if state.runtime_status == OrchestrationStatus.COMPLETED:
549587
return self._create_http_response(200, state.serialized_output)
@@ -552,8 +590,12 @@ async def wait_for_completion_or_create_check_status_response(
552590
200, DurableOrchestrationStatus.from_orchestration_state(state).to_json())
553591
if state.runtime_status == OrchestrationStatus.FAILED:
554592
return self._create_http_response(
555-
500, DurableOrchestrationStatus.from_orchestration_state(state).to_json())
556-
return self.create_check_status_response(request, instance_id)
593+
500 if return_internal_server_error_on_failure else 200,
594+
DurableOrchestrationStatus.from_orchestration_state(state).to_json())
595+
return self.create_check_status_response(
596+
request,
597+
instance_id,
598+
return_internal_server_error_on_failure)
557599

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

651693
def create_check_status_response(
652-
self, request: func.HttpRequest, instance_id: str) -> func.HttpResponse:
653-
payload = self._get_client_response_links(request, instance_id)
694+
self,
695+
request: func.HttpRequest,
696+
instance_id: str,
697+
return_internal_server_error_on_failure: bool = False
698+
) -> func.HttpResponse:
699+
payload = self._get_client_response_links(
700+
request,
701+
instance_id,
702+
return_internal_server_error_on_failure)
654703
return func.HttpResponse(
655704
body=str(payload),
656705
status_code=202,
657706
headers={
658-
"content-type": "application/json",
707+
"Content-Type": "application/json",
659708
"Location": payload["statusQueryGetUri"],
709+
"Retry-After": "10",
660710
},
661711
)
662712

663713
def create_http_management_payload(
664714
self,
665715
request: func.HttpRequest | str | None = None,
666-
instance_id: str | None = None) -> HttpManagementPayload:
716+
instance_id: str | None = None,
717+
return_internal_server_error_on_failure: bool = False
718+
) -> HttpManagementPayload:
667719
if instance_id is None and isinstance(request, str):
668720
instance_id = request
669721
request = None
670722
if instance_id is None:
671723
raise TypeError("instance_id is required")
672724
resolved_request = request if isinstance(request, func.HttpRequest) else None
673-
return self._get_client_response_links(resolved_request, instance_id)
725+
return self._get_client_response_links(
726+
resolved_request,
727+
instance_id,
728+
return_internal_server_error_on_failure)
674729

675730
def _get_client_response_links(
676-
self, request: func.HttpRequest | None,
677-
instance_id: str) -> HttpManagementPayload:
731+
self,
732+
request: func.HttpRequest | None,
733+
instance_id: str,
734+
return_internal_server_error_on_failure: bool = False
735+
) -> HttpManagementPayload:
678736
return _build_http_management_payload(
679737
instance_id,
680738
self.managementUrls,
681739
self.baseUrl,
682740
self.httpBaseUrl,
683741
self.requiredQueryStringParameters,
684742
self.useForwardedHost,
685-
request)
743+
request,
744+
return_internal_server_error_on_failure)
686745

687746

688747
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)