Skip to content

fix(intrinsics): only resolve the selected Fn::If branch - #9134

Open
EPNOS wants to merge 6 commits into
aws:developfrom
EPNOS:fix/4510-lazy-fn-if-resolution
Open

fix(intrinsics): only resolve the selected Fn::If branch#9134
EPNOS wants to merge 6 commits into
aws:developfrom
EPNOS:fix/4510-lazy-fn-if-resolution

Conversation

@EPNOS

@EPNOS EPNOS commented Jul 20, 2026

Copy link
Copy Markdown

Which issue(s) does this change fix?

Fixes #4510, Fixes #6205

Why is this change necessary?

Fn::If eagerly resolved both the true and false branches before checking
the condition. If the branch that the condition did NOT select contained a
value the local resolver can't handle (e.g. Fn::GetAtt to a FunctionUrl
attribute, or !Ref AWS::NoValue in a Layers list), resolution of the
entire Fn::If failed even though the condition picked the other,
perfectly resolvable branch. sam deploy/sam build were unaffected since
CloudFormation itself only evaluates the selected branch.

How does it address the issue?

  • handle_fn_if now evaluates the condition first and only recursively
    resolves the branch it selects, instead of resolving both branches
    unconditionally.
  • handle_fn_getatt now forwards its own ignore_errors argument to
    resolve_symbols, which it previously dropped. This matters once the
    Fn::If fix is in place: if the selected branch itself contains an
    unsupported Fn::GetAtt, ignore_errors=True should still let it
    degrade to a placeholder instead of raising.

What side effects does this change have?

None expected. Both branches were already required to independently
type-check before this change; only the eager resolution of the unselected
branch's value is removed.

Mandatory Checklist

PRs will only be reviewed after checklist is complete

  • Review the generative AI contribution guidelines
  • Add input/output type hints to new functions/methods
  • Write design document if needed
  • Write/update unit tests
  • Write/update integration tests
  • Write/update functional tests if needed
  • make pr passes
  • make update-reproducible-reqs if dependencies were changed
  • Write documentation

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

EPNOS added 3 commits July 19, 2026 19:01
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 aws#4510, Fixes aws#6205
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.
…le branch

Reproduces the aws#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.
@EPNOS
EPNOS requested a review from a team as a code owner July 20, 2026 02:37
@github-actions github-actions Bot added pr/external stage/needs-triage Automatically applied to new issues and PRs, indicating they haven't been looked at. labels Jul 20, 2026

@aws-sam-tooling-bot aws-sam-tooling-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Results

Reviewed: 9101836..0a7b27d
Files: 4
Comments: 1

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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[BUG] Forwarding ignore_errors into resolve_symbols changes behavior for every unresolvable Fn::GetAtt in the template, not just ones inside a selected Fn::If branch, and it regresses nested-stack layer handling.

SamBaseProvider.get_template / get_resolved_template_dict both call resolver.resolve_template(ignore_errors=True), so this is the only mode sam build and sam local ever run in. Previously an unresolvable GetAtt raised InvalidSymbolException, which the dict-recursion handler in intrinsic_property_resolver (and resolve_attribute) caught and used to leave the property as the raw intrinsic dict. Now it silently becomes the string "$LogicalId.Attribute", and downstream code that distinguishes "unresolved intrinsic dict" from "resolved string" takes the wrong path.

Concretely, for a layer referencing a nested stack output — the exact shape nested_stack_manager.py emits and that SamFunctionProvider._locate_layer_from_nested documents:

Layers:
  - !GetAtt AwsSamAutoDependencyLayerNestedStack.Outputs.MyDepLayer

resolve_symbols("AwsSamAutoDependencyLayerNestedStack", "Outputs.MyDepLayer") has no match in logical_id_translator, parameters, default_type_resolver, or common_attribute_resolver (which only handles Ref and Arn). Before this change the entry stayed a dict and _parse_layer_info fell into its else branch, logging layer "..." is not recognizable ... Skipping. After this change the entry is the string "$AwsSamAutoDependencyLayerNestedStack.Outputs.MyDepLayer", so isinstance(layer, str) is true, locate_layer_nested is False for build and local invoke, and it is passed to LayerVersion(layer, None, ...). _compute_layer_name / _compute_layer_version then fail to rsplit an ARN out of it and raise InvalidLayerVersionArn — a hard failure where the layer used to be skipped.

This change also isn't required for the Fn::If fix. If the selected branch contains an unsupported Fn::GetAtt, the exception already propagates out of handle_fn_if to the enclosing dict recursion, which catches it under ignore_errors=True and preserves the original value. Suggest reverting this line to keep the blast radius limited to handle_fn_if:

return self._symbol_resolver.resolve_symbols(logical_id, resource_type)

If the placeholder degradation is genuinely wanted, it needs to be scoped so it can't turn an unresolved layer reference into something _parse_layer_info mistakes for a literal ARN, plus a test covering Layers: [!GetAtt Stack.Outputs.Layer]. Note that tests/unit/.../test_intrinsic_resolver.py:471 currently locks in the new placeholder behavior, so this would need updating too.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed
88ffaea

EPNOS added 2 commits August 3, 2026 22:22
… 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 16c70d0 and its
placeholder-locking test; adds a regression test covering the nested
stack layer scenario.

@aws-sam-tooling-bot aws-sam-tooling-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Results

Reviewed: 9101836..57b4fbd
Files: 4
Comments: 1


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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[BUG] handle_fn_if can now return None, which it could never do before, and at least one downstream consumer is not null-safe.

Before this change, a branch containing !Ref AWS::NoValue was pre-resolved to None by the outer intrinsic_property_resolver(intrinsic_value, ...) call, and the subsequent resolve(arguments[1|2]) hit the if intrinsic is None: raise InvalidIntrinsicException guard at the top of intrinsic_property_resolver. So Fn::If always raised, and with ignore_errors=True the enclosing dict loop left the property as the raw {"Fn::If": [...]} dict.

Now the selected branch is resolved directly, so !Ref AWS::NoValue reaches IntrinsicsSymbolTable.handle_pseudo_no_value() and None is returned and assigned as the property value (the generic dict branch does sanitized_dict[sanitized_key] = sanitized_val with no None filtering).

Concrete failure — the common "conditionally omit a property" idiom:

Properties:
 Layers: !If [UseLayers, [!Ref MyLayer], !Ref "AWS::NoValue"]

When UseLayers is false, Properties["Layers"] becomes None. In samcli/lib/providers/sam_function_provider.py:265, resource_properties.get("Layers", []) returns None (the default only applies when the key is absent), and _parse_layer_info then does for layer in list_of_layersTypeError: 'NoneType' object is not iterable, an unhandled traceback instead of a domain error.

Note the element-level form Layers: [!If [Cond, !Ref MyLayer, !Ref "AWS::NoValue"]] is fine — it yields [None] and _parse_layer_info skips unrecognized entries. Only the whole-property form breaks, and that is exactly one of the scenarios this PR sets out to fix, so it is worth closing here rather than leaving it as a newly reachable crash.

Two options:

  • Make the resolver match CloudFormation's AWS::NoValue semantics by dropping keys whose resolved value is None in the generic dict branch of intrinsic_property_resolver. This is the semantically correct fix but has wider blast radius, so it needs its own tests.
  • Harden the consumer, e.g. resource_properties.get("Layers") or [] in both call sites in sam_function_provider.py.

Either way, please add a unit test covering an Fn::If whose selected branch is !Ref AWS::NoValue — the four new tests only cover unresolvable Fn::GetAtt in the unselected branch, so this path is currently untested.

Note on the previous review comment: the handle_fn_getatt change that forwarded ignore_errors into resolve_symbols is no longer in the diff, and test_template_ignore_errors_leaves_unresolvable_layer_getatt_as_dict was added as a guard for the nested-stack layer behavior. That finding is resolved and I did not re-raise it. The PR description still describes the handle_fn_getatt change, so it is now out of date.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr/external stage/needs-triage Automatically applied to new issues and PRs, indicating they haven't been looked at.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: sam local invoke fails when Globals.Function.Layers contains AWS::NoValue Bug: Unable to resolve env var when referencing FunctionUrl

1 participant