floci-io/floci · error · AwsException

CustomResourceFailed

CustomResourceFailed

Error message

Custom resource handler reported FAILED: {}

What it means

The custom resource's backing Lambda ran successfully at the HTTP level, but its response (captured via the ResponseURL PUT that Floci's CfnResponseController records) carried Status=FAILED or no Status at all (default FAILED). Floci only accepts SUCCESS, so the stack operation fails with the handler-supplied Reason. Only single-Lambda synchronous handlers (Pattern 1, e.g. CDK BucketDeployment) are emulated.

Source

Thrown at src/main/java/io/github/hectorvent/floci/services/cloudformation/CloudFormationResourceProvisioner.java:4891

                ? ((ObjectNode) resolvedProps).deepCopy()
                : objectMapper.createObjectNode();
        ObjectNode resourceProperties = (ObjectNode) stringifyScalars(resolved);

        boolean isUpdate = r.getPhysicalId() != null;
        String requestType = isUpdate ? "Update" : "Create";
        String priorPhysicalId = isUpdate ? r.getPhysicalId() : null;

        // On Update, CloudFormation includes the previous ResourceProperties so the handler can diff.
        // The prior values were stashed at the last create/update; read them before we overwrite below.
        ObjectNode oldResourceProperties = isUpdate ? readStashedProperties(r) : null;

        JsonNode response = invokeCustomResourceHandler(serviceToken, requestType, r.getLogicalId(),
                r.getResourceType(), priorPhysicalId, resourceProperties, oldResourceProperties,
                region, accountId, stackName);

        String status = response.path("Status").asText("FAILED");
        if (!"SUCCESS".equals(status)) {
            throw new AwsException("CustomResourceFailed",
                    "Custom resource handler reported FAILED: "
                            + response.path("Reason").asText("(no reason given)"), 400);
        }

        String returnedPhysicalId = response.path("PhysicalResourceId").asText(null);
        if (returnedPhysicalId != null && !returnedPhysicalId.isBlank()) {
            r.setPhysicalId(returnedPhysicalId);
        } else if (priorPhysicalId != null) {
            r.setPhysicalId(priorPhysicalId);
        } else {
            r.setPhysicalId(r.getLogicalId() + "-" + UUID.randomUUID().toString().substring(0, 12));
        }

        // Data.* become Fn::GetAtt attributes on the custom resource.
        JsonNode data = response.path("Data");
        if (data.isObject()) {
            data.fields().forEachRemaining(e ->
                    r.getAttributes().put(e.getKey(), nodeToAttributeValue(e.getValue())));

View on GitHub (pinned to 62ff490619)

Solutions

  1. Read the Reason in the error message — it is the handler's own failure text and usually names the root cause.
  2. Invoke the backing Lambda directly with a synthetic CloudFormation event payload (aws lambda invoke) and inspect its logs to reproduce outside the stack flow.
  3. Confirm the handler follows Pattern 1: it PUTs to ResponseURL (via cfnresponse or https.put) and does not rely on the async Provider framework's isComplete polling.
  4. Check the Lambda's permissions/inputs (buckets, parameters, secrets) inside the emulator; missing permission is the most common FAILED reason.

Example fix

# before (handler)
try:
    do_work(props)
    cfnresponse.send(event, ctx, cfnresponse.FAILED, {}, reason='boom')
# after
try:
    do_work(props)
    cfnresponse.send(event, ctx, cfnresponse.SUCCESS, {})
except Exception as e:
    cfnresponse.send(event, ctx, cfnresponse.FAILED, {}, reason=str(e))
Defensive patterns

Strategy: try-catch

Try / catch

try {
    cfn.executeChangeSet(...).get();
} catch (CloudFormationException e) {
    if (e.getMessage().contains("handler reported FAILED")) {
        log.error("custom resource FAILED: {}", e.getMessage()); // Reason is embedded
        dumpLambdaLogs(serviceTokenFromTemplate());
    }
    throw e;
}

Prevention

When it happens

Trigger: Handler code explicitly cfnresponse.send(event, context, FAILED, ...) because a pre-condition failed; handler threw and its wrapper caught it and sent FAILED; handler never PUT a response, in which case Status defaults to FAILED with '(no reason given)'.

Common situations: CDK BucketDeployment whose asset bucket/keys are unreachable; handler missing IAM permissions in the emulator; handler expecting Provider-framework event fields (RequestType onEvent/isComplete) it does not get under Pattern 1 emulation; runtime bug in user Lambda code.

Related errors


AI-assisted analysis of floci-io/floci@62ff490619 (2026-08-14). Data as JSON: /api/errors/9f642ebfe7962777. Report an issue: GitHub.