From d4c07aca8289fba5275f1035ac93302193c4d826 Mon Sep 17 00:00:00 2001 From: EPNOS Date: Sun, 19 Jul 2026 19:01:04 +0900 Subject: [PATCH 1/4] fix(intrinsics): resolve only the selected Fn::If branch Fn::If eagerly resolved both the true and false branches before checking the condition, so an unresolvable value in the branch NOT selected (e.g. Fn::GetAtt to FunctionUrl, which sam local invoke can't resolve) caused the whole intrinsic to be left unresolved even though the condition picked the other, resolvable branch. Fixes #4510, Fixes #6205 --- .../intrinsic_property_resolver.py | 19 +++-------- .../test_intrinsic_resolver.py | 34 ++++++++++++++++++- 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/samcli/lib/intrinsic_resolver/intrinsic_property_resolver.py b/samcli/lib/intrinsic_resolver/intrinsic_property_resolver.py index aecc2a85aad..0ab0352722f 100644 --- a/samcli/lib/intrinsic_resolver/intrinsic_property_resolver.py +++ b/samcli/lib/intrinsic_resolver/intrinsic_property_resolver.py @@ -716,24 +716,14 @@ def handle_fn_if(self, intrinsic_value, ignore_errors): ------- This will return value_if_true and value_if_false depending on how the condition is evaluated """ - arguments = self.intrinsic_property_resolver( - intrinsic_value, ignore_errors, parent_function=IntrinsicResolver.FN_IF - ) - verify_intrinsic_type_list(arguments, IntrinsicResolver.FN_IF) - verify_number_arguments(arguments, IntrinsicResolver.FN_IF, num=3) + verify_intrinsic_type_list(intrinsic_value, IntrinsicResolver.FN_IF) + verify_number_arguments(intrinsic_value, IntrinsicResolver.FN_IF, num=3) condition_name = self.intrinsic_property_resolver( - arguments[0], ignore_errors, parent_function=IntrinsicResolver.FN_IF + intrinsic_value[0], ignore_errors, parent_function=IntrinsicResolver.FN_IF ) verify_intrinsic_type_str(condition_name, IntrinsicResolver.FN_IF) - value_if_true = self.intrinsic_property_resolver( - arguments[1], ignore_errors, parent_function=IntrinsicResolver.FN_IF - ) - value_if_false = self.intrinsic_property_resolver( - arguments[2], ignore_errors, parent_function=IntrinsicResolver.FN_IF - ) - condition = self._conditions.get(condition_name) verify_intrinsic_type_dict( condition, @@ -750,7 +740,8 @@ def handle_fn_if(self, intrinsic_value, ignore_errors): message="The result of {} must evaluate to bool".format(IntrinsicResolver.FN_IF), ) - return value_if_true if condition_evaluated else value_if_false + selected_value = intrinsic_value[1] if condition_evaluated else intrinsic_value[2] + return self.intrinsic_property_resolver(selected_value, ignore_errors, parent_function=IntrinsicResolver.FN_IF) def handle_fn_equals(self, intrinsic_value, ignore_errors): """ diff --git a/tests/unit/lib/intrinsic_resolver/test_intrinsic_resolver.py b/tests/unit/lib/intrinsic_resolver/test_intrinsic_resolver.py index 39d342764de..be930a09c0b 100644 --- a/tests/unit/lib/intrinsic_resolver/test_intrinsic_resolver.py +++ b/tests/unit/lib/intrinsic_resolver/test_intrinsic_resolver.py @@ -3,7 +3,7 @@ from copy import deepcopy from pathlib import Path from unittest import TestCase -from unittest.mock import patch +from unittest.mock import MagicMock, patch from parameterized import parameterized @@ -906,6 +906,38 @@ def test_fn_if_condition_not_bool_fail(self): with self.assertRaises(InvalidIntrinsicException, msg="Invalid Condition"): self.resolver.intrinsic_property_resolver({"Fn::If": ["InvalidCondition", "test", "test"]}, True) + def test_fn_if_selects_resolvable_true_branch_ignoring_unresolvable_false_branch(self): + intrinsic = {"Fn::If": ["TestCondition", "resolved-value", {"Fn::GetAtt": ["Function2Url", "FunctionUrl"]}]} + + result = self.resolver.intrinsic_property_resolver(intrinsic, False) + self.assertEqual(result, "resolved-value") + + def test_fn_if_selects_resolvable_false_branch_ignoring_unresolvable_true_branch(self): + intrinsic = {"Fn::If": ["NotTestCondition", {"Fn::GetAtt": ["Function2Url", "FunctionUrl"]}, "resolved-value"]} + + result = self.resolver.intrinsic_property_resolver(intrinsic, False) + self.assertEqual(result, "resolved-value") + + def test_fn_if_does_not_evaluate_unselected_false_branch(self): + mock_handle_fn_getatt = MagicMock() + self.resolver.intrinsic_key_function_map[IntrinsicResolver.FN_GET_ATT] = mock_handle_fn_getatt + intrinsic = {"Fn::If": ["TestCondition", "resolved-value", {"Fn::GetAtt": ["Function2Url", "FunctionUrl"]}]} + + result = self.resolver.intrinsic_property_resolver(intrinsic, False) + + self.assertEqual(result, "resolved-value") + mock_handle_fn_getatt.assert_not_called() + + def test_fn_if_does_not_evaluate_unselected_true_branch(self): + mock_handle_fn_getatt = MagicMock() + self.resolver.intrinsic_key_function_map[IntrinsicResolver.FN_GET_ATT] = mock_handle_fn_getatt + intrinsic = {"Fn::If": ["NotTestCondition", {"Fn::GetAtt": ["Function2Url", "FunctionUrl"]}, "resolved-value"]} + + result = self.resolver.intrinsic_property_resolver(intrinsic, False) + + self.assertEqual(result, "resolved-value") + mock_handle_fn_getatt.assert_not_called() + class TestIntrinsicAttribteResolution(TestCase): def setUp(self): From 16c70d0c7087255a7cc11202298e02d1af6baff9 Mon Sep 17 00:00:00 2001 From: EPNOS Date: Mon, 20 Jul 2026 10:44:18 +0900 Subject: [PATCH 2/4] fix(intrinsics): forward ignore_errors to Fn::GetAtt symbol resolution handle_fn_getatt called resolve_symbols without forwarding its own ignore_errors argument, so resolve_symbols always defaulted to False. Any Fn::GetAtt referencing an attribute the local resolver doesn't support (e.g. FunctionUrl) raised even when the caller explicitly asked to ignore errors, instead of falling back to a placeholder. --- .../lib/intrinsic_resolver/intrinsic_property_resolver.py | 2 +- .../unit/lib/intrinsic_resolver/test_intrinsic_resolver.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/samcli/lib/intrinsic_resolver/intrinsic_property_resolver.py b/samcli/lib/intrinsic_resolver/intrinsic_property_resolver.py index 0ab0352722f..f1649723a6c 100644 --- a/samcli/lib/intrinsic_resolver/intrinsic_property_resolver.py +++ b/samcli/lib/intrinsic_resolver/intrinsic_property_resolver.py @@ -615,7 +615,7 @@ def handle_fn_getatt(self, intrinsic_value, ignore_errors): verify_intrinsic_type_str(logical_id, IntrinsicResolver.FN_GET_ATT) verify_intrinsic_type_str(resource_type, IntrinsicResolver.FN_GET_ATT) - return self._symbol_resolver.resolve_symbols(logical_id, resource_type) + return self._symbol_resolver.resolve_symbols(logical_id, resource_type, ignore_errors) def handle_fn_ref(self, intrinsic_value, ignore_errors): """ diff --git a/tests/unit/lib/intrinsic_resolver/test_intrinsic_resolver.py b/tests/unit/lib/intrinsic_resolver/test_intrinsic_resolver.py index be930a09c0b..05ec9104c09 100644 --- a/tests/unit/lib/intrinsic_resolver/test_intrinsic_resolver.py +++ b/tests/unit/lib/intrinsic_resolver/test_intrinsic_resolver.py @@ -468,6 +468,13 @@ def test_fn_getatt_second_arguments_invalid(self, name, intrinsic): with self.assertRaises(InvalidIntrinsicException, msg=name): self.resolver.intrinsic_property_resolver({"Fn::GetAtt": ["some logical Id", intrinsic]}, True) + def test_fn_getatt_ignore_errors_forwarded_for_unsupported_attribute(self): + intrinsic = {"Fn::GetAtt": ["UnknownResource", "UnknownAttribute"]} + + result = self.resolver.intrinsic_property_resolver(intrinsic, True) + + self.assertEqual(result, "$UnknownResource.UnknownAttribute") + class TestIntrinsicFnSubResolver(TestCase): def setUp(self): From e16ec5bc36a67c4ec5f28e149c53bf7015742a63 Mon Sep 17 00:00:00 2001 From: EPNOS Date: Mon, 20 Jul 2026 11:22:25 +0900 Subject: [PATCH 3/4] test(intrinsics): add integration coverage for Fn::If with unresolvable branch Reproduces the #4510 scenario end-to-end via sam local invoke: an environment variable that uses Fn::If where the selected branch is a resolvable Ref and the other branch is Fn::GetAtt to a FunctionUrl attribute, which the local resolver can't handle. --- .../local/invoke/test_integrations_cli.py | 21 +++++++++++ .../integration/testdata/invoke/template.yml | 35 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/tests/integration/local/invoke/test_integrations_cli.py b/tests/integration/local/invoke/test_integrations_cli.py index 95a0a95cea4..dfc011e8f7d 100644 --- a/tests/integration/local/invoke/test_integrations_cli.py +++ b/tests/integration/local/invoke/test_integrations_cli.py @@ -393,6 +393,27 @@ def test_invoke_with_env_using_parameters(self): self.assertEqual(environ["MyRuntimeVersion"], "v0") self.assertEqual(environ["EmptyDefaultParameter"], "") + @pytest.mark.flaky(reruns=3) + def test_invoke_with_env_using_fn_if_ignores_unresolvable_branch(self): + command_list = InvokeIntegBase.get_command_list( + "EchoEnvWithFnIf", + template_path=self.template_path, + event_path=self.event_path, + ) + + process = Popen(command_list, stdout=PIPE) + try: + stdout, _ = process.communicate(timeout=TIMEOUT) + except TimeoutExpired: + process.kill() + raise + + self.assertEqual(process.returncode, 0) + process_stdout = stdout.strip() + environ = json.loads(process_stdout.decode("utf-8")) + + self.assertEqual(environ["FunctionUrl"], "https://custom.example.com/") + @pytest.mark.flaky(reruns=3) def test_invoke_multi_tenant_function(self): command_list = InvokeIntegBase.get_command_list( diff --git a/tests/integration/testdata/invoke/template.yml b/tests/integration/testdata/invoke/template.yml index 61efc176eb5..2695fcf3587 100644 --- a/tests/integration/testdata/invoke/template.yml +++ b/tests/integration/testdata/invoke/template.yml @@ -26,6 +26,17 @@ Parameters: Type: String Default: "2" + UseCustomFunctionUrl: + Type: String + Default: "true" + + CustomFunctionUrl: + Type: String + Default: "https://custom.example.com/" + +Conditions: + ShouldUseCustomFunctionUrl: !Equals [!Ref UseCustomFunctionUrl, "true"] + Mappings: common: LambdaFunction: @@ -202,6 +213,30 @@ Resources: MyRuntimeVersion: !Ref MyRuntimeVersion EmptyDefaultParameter: !Ref EmptyDefaultParameter + FunctionWithUrlConfig: + Type: AWS::Serverless::Function + Properties: + Handler: main.handler + Runtime: python3.9 + CodeUri: . + Timeout: 600 + FunctionUrlConfig: + AuthType: NONE + + EchoEnvWithFnIf: + Type: AWS::Serverless::Function + Properties: + Handler: main.env_var_echo_hanler + Runtime: python3.9 + CodeUri: . + Timeout: 600 + Environment: + Variables: + FunctionUrl: !If + - ShouldUseCustomFunctionUrl + - !Ref CustomFunctionUrl + - !GetAtt FunctionWithUrlConfigUrl.FunctionUrl + TimeoutFunctionWithStringParameter: Type: AWS::Serverless::Function Properties: From 88ffaeafa7ecbd7fa407e616bf295ee758b806b6 Mon Sep 17 00:00:00 2001 From: EPNOS Date: Mon, 3 Aug 2026 22:22:27 +0900 Subject: [PATCH 4/4] fix(intrinsics): stop forwarding ignore_errors into Fn::GetAtt symbol resolution fix(intrinsics): stop forwarding ignore_errors into Fn::GetAtt symbol resolution handle_fn_getatt forwarded ignore_errors to resolve_symbols, but resolve_template is always called with ignore_errors=True in sam build and sam local invoke. This made every unresolvable Fn::GetAtt in the template degrade to a .Attribute placeholder string instead of raising, which used to be caught by the dict-recursion handler and left the raw intrinsic dict in place. This broke layers referencing nested stack outputs (the shape nested_stack_manager.py emits): the placeholder string satisfied isinstance(layer, str) in _parse_layer_info and was treated as a literal ARN, raising InvalidLayerVersionArn where the layer used to be safely skipped. The Fn::If fix doesn't need this: when the selected branch is unresolvable, the exception already propagates out of handle_fn_if and is caught by the enclosing dict recursion under ignore_errors=True. Reverts the ignore_errors forwarding from 16c70d0c7 and its placeholder-locking test; adds a regression test covering the nested stack layer scenario. --- .../intrinsic_property_resolver.py | 2 +- .../test_intrinsic_resolver.py | 23 +++++++++++++------ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/samcli/lib/intrinsic_resolver/intrinsic_property_resolver.py b/samcli/lib/intrinsic_resolver/intrinsic_property_resolver.py index f1649723a6c..0ab0352722f 100644 --- a/samcli/lib/intrinsic_resolver/intrinsic_property_resolver.py +++ b/samcli/lib/intrinsic_resolver/intrinsic_property_resolver.py @@ -615,7 +615,7 @@ def handle_fn_getatt(self, intrinsic_value, ignore_errors): verify_intrinsic_type_str(logical_id, IntrinsicResolver.FN_GET_ATT) verify_intrinsic_type_str(resource_type, IntrinsicResolver.FN_GET_ATT) - return self._symbol_resolver.resolve_symbols(logical_id, resource_type, ignore_errors) + return self._symbol_resolver.resolve_symbols(logical_id, resource_type) def handle_fn_ref(self, intrinsic_value, ignore_errors): """ diff --git a/tests/unit/lib/intrinsic_resolver/test_intrinsic_resolver.py b/tests/unit/lib/intrinsic_resolver/test_intrinsic_resolver.py index 05ec9104c09..0759c7a0bdc 100644 --- a/tests/unit/lib/intrinsic_resolver/test_intrinsic_resolver.py +++ b/tests/unit/lib/intrinsic_resolver/test_intrinsic_resolver.py @@ -468,13 +468,6 @@ def test_fn_getatt_second_arguments_invalid(self, name, intrinsic): with self.assertRaises(InvalidIntrinsicException, msg=name): self.resolver.intrinsic_property_resolver({"Fn::GetAtt": ["some logical Id", intrinsic]}, True) - def test_fn_getatt_ignore_errors_forwarded_for_unsupported_attribute(self): - intrinsic = {"Fn::GetAtt": ["UnknownResource", "UnknownAttribute"]} - - result = self.resolver.intrinsic_property_resolver(intrinsic, True) - - self.assertEqual(result, "$UnknownResource.UnknownAttribute") - class TestIntrinsicFnSubResolver(TestCase): def setUp(self): @@ -1052,6 +1045,22 @@ def test_template_ignore_errors(self): } self.assertEqual(expected_template, dict(result)) + def test_template_ignore_errors_leaves_unresolvable_layer_getatt_as_dict(self): + resources = deepcopy(self.resources) + resources["ReferenceLambdaLayerVersionLambdaFunction"]["Properties"]["Layers"] = [ + {"Fn::GetAtt": ["NestedStack", "Outputs.MyDepLayer"]} + ] + template = {"Mappings": self.mappings, "Conditions": self.conditions, "Resources": resources} + symbol_resolver = IntrinsicsSymbolTable(template=template, logical_id_translator=self.logical_id_translator) + resolver = IntrinsicResolver(template=template, symbol_resolver=symbol_resolver) + + result = resolver.resolve_attribute(resources, ignore_errors=True) + + self.assertEqual( + result["ReferenceLambdaLayerVersionLambdaFunction"]["Properties"]["Layers"], + [{"Fn::GetAtt": ["NestedStack", "Outputs.MyDepLayer"]}], + ) + class TestResolveTemplate(TestCase): def test_parameter_not_resolved(self):