{"record":{"id":"3e2413c7259c9513","repo":"floci-io/floci","slug":"distributionnotdisabled","errorCode":"DistributionNotDisabled","errorMessage":"The distribution you are trying to delete has not been disabled.","messagePattern":"The distribution you are trying to delete has not been disabled\\.","errorType":"error_code","errorClass":"AwsException","httpStatus":409,"severity":"error","filePath":"src/main/java/io/github/hectorvent/floci/services/cloudfront/CloudFrontService.java","lineNumber":165,"sourceCode":"        updated.setId(id);\n        updated.setArn(existing.getArn());\n        updated.setDomainName(existing.getDomainName());\n        updated.setStatus(\"Deployed\");\n        updated.setLastModifiedTime(Instant.now());\n        updated.setEtag(UUID.randomUUID().toString());\n        updated.setTags(existing.getTags());\n        distStore.put(id, updated);\n        return updated;\n    }\n\n    public synchronized void deleteDistribution(String id, String ifMatch) {\n        Distribution existing = getDistribution(id);\n        if (!existing.getEtag().equals(ifMatch)) {\n            throw new AwsException(\"InvalidIfMatchVersion\",\n                    \"The If-Match version is missing or not valid for the resource.\", 400);\n        }\n        if (existing.getConfig() != null && existing.getConfig().isEnabled()) {\n            throw new AwsException(\"DistributionNotDisabled\",\n                    \"The distribution you are trying to delete has not been disabled.\", 409);\n        }\n        distStore.delete(id);\n        invalidationStore.delete(id);\n        tagStore.delete(\"distribution/\" + id);\n    }\n\n    /**\n     * Removes a distribution and its associated invalidations/tags without the disable/If-Match guards\n     * enforced by {@link #deleteDistribution(String, String)}. Used by CloudFormation stack deletion,\n     * which owns the resource lifecycle at the stack level.\n     */\n    public synchronized void removeDistribution(String id) {\n        distStore.delete(id);\n        invalidationStore.delete(id);\n        tagStore.delete(\"distribution/\" + id);\n    }\n","sourceCodeStart":147,"sourceCodeEnd":183,"githubUrl":"https://github.com/floci-io/floci/blob/62ff490619e7bd3554597c28c704081b4c15add5/src/main/java/io/github/hectorvent/floci/services/cloudfront/CloudFrontService.java#L147-L183","documentation":"Floci's CloudFront service layer refuses to delete a distribution whose config still has Enabled=true, returning DistributionNotDisabled (HTTP 409). AWS requires a distribution to be fully disabled before deletion, and the emulator enforces the same state machine after the ETag check passes.","triggerScenarios":"DELETE /distribution/{Id} with a valid If-Match while the distribution's DistributionConfig has Enabled=true.","commonSituations":"Teardown scripts that create, test, and delete in one run without a disable step; forgetting that disable is itself an update (which also rotates the ETag); assuming the emulator skips lifecycle guards.","solutions":["First update the distribution with Enabled=false (using the current ETag as If-Match)","Then re-GET the config for the new ETag and issue the delete with it","Model the full sequence: disable -> (optionally wait for status) -> delete"],"exampleFix":"// before\ncf.deleteDistribution(req -> req.id(id).ifMatch(etag));\n\n// after: disable first, then delete with refreshed etag\nDistributionConfig disabled = cfg.toBuilder().enabled(false).build();\ncf.updateDistribution(req -> req.id(id).ifMatch(etag).distributionConfig(disabled));\nString freshEtag = cf.getDistributionConfig(r -> r.id(id)).eTag();\ncf.deleteDistribution(req -> req.id(id).ifMatch(freshEtag));","handlingStrategy":"validation","validationCode":"GetDistributionConfigResponse cur = cf.getDistributionConfig(r -> r.id(id));\nif (Boolean.TRUE.equals(cur.distributionConfig().enabled())) {\n    // must disable first: update with enabled=false using cur.eTag()\n}","typeGuard":null,"tryCatchPattern":"try {\n    cf.deleteDistribution(req -> req.id(id).ifMatch(etag));\n} catch (CloudFrontException e) {\n    if (\"DistributionNotDisabled\".equals(e.awsErrorDetails().errorCode())) {\n        DistributionConfig off = cfg.toBuilder().enabled(false).build();\n        cf.updateDistribution(req -> req.id(id).ifMatch(etag).distributionConfig(off));\n        String fresh = cf.getDistributionConfig(r -> r.id(id)).eTag();\n        cf.deleteDistribution(req -> req.id(id).ifMatch(fresh));\n    } else { throw e; }\n}","preventionTips":["Encode the disable-before-delete sequence in a single teardown helper","Treat 409 from CloudFront deletes as a state error, not a transient one — fix state, don't blind-retry"],"tags":["cloudfront","distribution","lifecycle","state-machine","delete"],"backgroundTag":null,"analyzedSha":"62ff490619e7bd3554597c28c704081b4c15add5","analyzedAt":"2026-08-14T14:25:23.764Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}