{"record":{"id":"8c149a0472e49bdc","repo":"floci-io/floci","slug":"customresourcetimeout","errorCode":"CustomResourceTimeout","errorMessage":"Timed out waiting for custom resource {} to PUT its response to ResponseURL","messagePattern":"Timed out waiting for custom resource (.+?) to PUT its response to ResponseURL","errorType":"exception","errorClass":"AwsException","httpStatus":504,"severity":"error","filePath":"src/main/java/io/github/hectorvent/floci/services/cloudformation/CloudFormationResourceProvisioner.java","lineNumber":5066,"sourceCode":"            if (oldResourceProperties != null) {\n                event.set(\"OldResourceProperties\", oldResourceProperties);\n            }\n\n            byte[] payload = objectMapper.writeValueAsBytes(event);\n            InvokeResult result = lambdaService.invoke(region, serviceToken, payload,\n                    InvocationType.RequestResponse);\n            if (result.getFunctionError() != null) {\n                String body = result.getPayload() != null\n                        ? new String(result.getPayload(), StandardCharsets.UTF_8) : \"\";\n                throw new AwsException(\"CustomResourceFailed\",\n                        \"Custom resource handler errored (\" + result.getFunctionError() + \"): \" + body, 400);\n            }\n\n            return customResourceResponseStore.await(token, CR_RESPONSE_TIMEOUT);\n        } catch (AwsException e) {\n            throw e;\n        } catch (TimeoutException e) {\n            throw new AwsException(\"CustomResourceTimeout\",\n                    \"Timed out waiting for custom resource \" + logicalId\n                            + \" to PUT its response to ResponseURL\", 504);\n        } catch (Exception e) {\n            throw new AwsException(\"CustomResourceFailed\",\n                    \"Failed to invoke custom resource \" + logicalId + \": \" + e.getMessage(), 500);\n        }\n    }\n\n    private static String nodeToAttributeValue(JsonNode node) {\n        if (node == null || node.isNull()) {\n            return \"\";\n        }\n        return node.isValueNode() ? node.asText() : node.toString();\n    }\n\n    /**\n     * Mirrors CloudFormation's stringification of custom-resource ResourceProperties: every scalar\n     * (boolean, number, text) becomes a string, while object and array structure is preserved.","sourceCodeStart":5048,"sourceCodeEnd":5084,"githubUrl":"https://github.com/floci-io/floci/blob/62ff490619e7bd3554597c28c704081b4c15add5/src/main/java/io/github/hectorvent/floci/services/cloudformation/CloudFormationResourceProvisioner.java#L5048-L5084","documentation":"The custom resource Lambda invoke succeeded (no FunctionError), but the handler never PUT its response to the ResponseURL within CR_RESPONSE_TIMEOUT — customResourceResponseStore.await(token, ...) timed out. Floci surfaces this as CustomResourceTimeout with HTTP 504. Under the synchronous Pattern 1 design the response is expected immediately after the handler returns.","triggerScenarios":"Handler returns 200 without calling cfnresponse.send / without PUTting to the ResponseURL; handler PUTs asynchronously after returning (deferred thread/callback) so the PUT races past the wait; handler took longer than the timeout before responding.","commonSituations":"Node handlers that respond via a callback after the main function returns without awaiting it; handlers written for the async Provider framework that skip the response PUT; long-running work exceeding CR_RESPONSE_TIMEOUT; response PUT sent to a wrong/rotated URL.","solutions":["Make sure the handler PUTs to event['ResponseURL'] before it returns — in Node await the PUT, in Python call cfnresponse.send synchronously.","Add/verify logging around the response PUT and check the Floci CfnResponseController received anything at all.","Shorten or split the handler's work so it finishes within the configured timeout, or emit SUCCESS early only when work is truly done.","Confirm the handler is Pattern 1 (single synchronous Lambda); async provider-framework handlers need rewriting or a synchronous wrapper."],"exampleFix":"// before (Node): PUT fires after return, races the waiter\nexport const handler = async (event) => { doWork(event); sendResponse(event); }\n// after\nexport const handler = async (event) => {\n  await doWork(event);\n  await sendResponse(event); // PUT completes before handler returns\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n    stackOps.executeChangeSet(name, cs);\n} catch (AwsException e) {\n    if (\"CustomResourceTimeout\".equals(e.getCode())) {\n        // transient: handler may have PUT just after the window — safe to re-run the change set\n        return retryWithBackoff();\n    }\n    throw e;\n}","preventionTips":["Always await the ResponseURL PUT before the handler returns (async handler semantics).","Keep custom resource work well under the emulator's CR_RESPONSE_TIMEOUT.","Log immediately before the PUT so timeouts are diagnosable from function logs."],"tags":["cloudformation","custom-resource","timeout","lambda"],"backgroundTag":null,"analyzedSha":"62ff490619e7bd3554597c28c704081b4c15add5","analyzedAt":"2026-08-14T14:25:23.764Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}