diff --git a/azure/durable_functions/models/DurableOrchestrationContext.py b/azure/durable_functions/models/DurableOrchestrationContext.py index df101003..01ec9000 100644 --- a/azure/durable_functions/models/DurableOrchestrationContext.py +++ b/azure/durable_functions/models/DurableOrchestrationContext.py @@ -214,7 +214,8 @@ def call_activity_with_retry(self, def call_http(self, method: str, uri: str, content: Optional[str] = None, headers: Optional[Dict[str, str]] = None, - token_source: TokenSource = None) -> TaskBase: + token_source: TokenSource = None, + is_raw_str: bool = False) -> TaskBase: """Schedule a durable HTTP call to the specified endpoint. Parameters @@ -229,6 +230,9 @@ def call_http(self, method: str, uri: str, content: Optional[str] = None, The HTTP request headers. token_source: TokenSource The source of OAuth token to add to the request. + is_raw_str: bool, optional + If True, send string content as-is. + If False (default), serialize content to JSON. Returns ------- @@ -236,10 +240,21 @@ def call_http(self, method: str, uri: str, content: Optional[str] = None, The durable HTTP request to schedule. """ json_content: Optional[str] = None - if content and content is not isinstance(content, str): - json_content = json.dumps(content) - else: - json_content = content + + # validate parameters + if (not isinstance(content, str)) and is_raw_str: + raise TypeError( + "Invalid use of 'is_raw_str' parameter: 'is_raw_str' is " + "set to 'True' but 'content' is not an instance of type 'str'. " + "Either set 'is_raw_str' to 'False', or ensure your 'content' " + "is of type 'str'.") + + if content is not None: + if isinstance(content, str) and is_raw_str: + # don't serialize the str value - use it as the raw HTTP request payload + json_content = content + else: + json_content = json.dumps(content) request = DurableHttpRequest(method, uri, json_content, headers, token_source) action = CallHttpAction(request) diff --git a/tests/orchestrator/test_call_http.py b/tests/orchestrator/test_call_http.py index be46d870..b42b36cd 100644 --- a/tests/orchestrator/test_call_http.py +++ b/tests/orchestrator/test_call_http.py @@ -1,5 +1,6 @@ from azure.durable_functions.models.ReplaySchema import ReplaySchema import json +import pytest from typing import Dict from azure.durable_functions.constants import HTTP_ACTION_NAME @@ -174,3 +175,61 @@ def test_post_completed_state(): # assert_valid_schema(result) assert_orchestration_state_equals(expected, result) validate_result_http_request(result) + +@pytest.mark.parametrize("content, expected_content, is_raw_str", [ + (None, None, False), + ("string data", '"string data"', False), + ('{"key": "value"}', '"{\\"key\\": \\"value\\"}"', False), + ('["list", "content"]', '"[\\"list\\", \\"content\\"]"', False), + ('[]', '"[]"', False), + ('42', '"42"', False), + ('true', '"true"', False), + # Cases that test actual behavior (not strictly adhering to Optional[str]) + ({"key": "value"}, '{"key": "value"}', False), + (["list", "content"], '["list", "content"]', False), + ([], '[]', False), + (42, '42', False), + (True, 'true', False), + # Cases when is_raw_str is True + ("string data", "string data", True), + ('{"key": "value"}', '{"key": "value"}', True), + ('[]', '[]', True), +]) +def test_call_http_content_handling(content, expected_content, is_raw_str): + def orchestrator_function(context): + yield context.call_http("POST", TEST_URI, content, is_raw_str=is_raw_str) + + context_builder = ContextBuilder('test_call_http_content_handling') + result = get_orchestration_state_result(context_builder, orchestrator_function) + + assert len(result['actions']) == 1 + http_action = result['actions'][0][0]['httpRequest'] + + assert http_action['method'] == "POST" + assert http_action['uri'] == TEST_URI + assert http_action['content'] == expected_content + +# Test that call_http raises a TypeError when is_raw_str is True but content is not a string +def test_call_http_non_string_content_with_raw_str(): + def orchestrator_function(context): + yield context.call_http("POST", TEST_URI, {"key": "value"}, is_raw_str=True) + + context_builder = ContextBuilder('test_call_http_non_string_content_with_raw_str') + + try: + result = get_orchestration_state_result(context_builder, orchestrator_function) + assert False + except Exception as e: + error_label = "\n\n$OutOfProcData$:" + error_str = str(e) + + expected_state = base_expected_state() + error_msg = "Invalid use of 'is_raw_str' parameter: 'is_raw_str' is "\ + "set to 'True' but 'content' is not an instance of type 'str'. "\ + "Either set 'is_raw_str' to 'False', or ensure your 'content' "\ + "is of type 'str'." + expected_state._error = error_msg + state_str = expected_state.to_json_string() + + expected_error_str = f"{error_msg}{error_label}{state_str}" + assert expected_error_str == error_str \ No newline at end of file