floci-io/floci · error · AwsException
ObjectNotFoundException
ObjectNotFoundException
Error message
No scalable target found for service namespace: {serviceNamespace}, resource ID: {resourceId}, scalable dimension: {scalableDimension} What it means
Thrown by DeregisterScalableTarget when no target exists for the exact (namespace, resourceId, dimension) triple in that region. Per AWS semantics mirrored here, deregistration also deletes attached scaling policies and their CloudWatch alarms — but only after the existence check passes. Note the code uses HTTP 400 with code ObjectNotFoundException, which matches AWS's Application Auto Scaling behavior of signaling object-not-found as a 400-class ValidationException-family error rather than 404.
Source
Thrown at src/main/java/io/github/hectorvent/floci/services/applicationautoscaling/ApplicationAutoScalingService.java:189
String prefix = region + "::" + serviceNamespace + "::";
return targets.scan(k -> k.startsWith(prefix)).stream()
.filter(t -> resourceIds == null || resourceIds.isEmpty() || resourceIds.contains(t.getResourceId()))
.filter(t -> scalableDimension == null || scalableDimension.equals(t.getScalableDimension()))
.sorted(Comparator.comparing(ScalableTarget::getResourceId)
.thenComparing(ScalableTarget::getScalableDimension))
.toList();
}
/**
* Deregisters a scalable target and, per AWS semantics, deletes every scaling policy
* attached to it along with the CloudWatch alarms those policies own.
*/
public void deregisterScalableTarget(String serviceNamespace, String resourceId,
String scalableDimension, String region) {
validateTriple(serviceNamespace, resourceId, scalableDimension);
String key = targetKey(region, serviceNamespace, resourceId, scalableDimension);
if (targets.get(key).isEmpty()) {
throw new AwsException("ObjectNotFoundException",
"No scalable target found for service namespace: " + serviceNamespace
+ ", resource ID: " + resourceId
+ ", scalable dimension: " + scalableDimension, 400);
}
for (ScalingPolicy policy : findPolicies(region, serviceNamespace, resourceId, scalableDimension)) {
deleteAlarms(policy, region);
policies.delete(policyKey(region, serviceNamespace, resourceId, scalableDimension, policy.getPolicyName()));
}
targets.delete(key);
LOG.infov("DeregisterScalableTarget: {0} {1} {2} in {3}",
serviceNamespace, resourceId, scalableDimension, region);
}
// ---------------------------------------------------------------- scaling policies
public ScalingPolicy putScalingPolicy(String policyName, String policyType, String serviceNamespace,
String resourceId, String scalableDimension,
TargetTrackingConfiguration targetTracking,View on GitHub (pinned to 62ff490619)
Solutions
- Describe first and deregister only on hit: DescribeScalableTargets with the same triple, then DeregisterScalableTarget if a target is returned.
- Verify resourceId and dimension byte-for-byte against the original RegisterScalableTarget call (check casing and the service prefix).
- Confirm the region of the call matches the region where the target was registered.
- In teardown code, treat ObjectNotFoundException as the benign 'already gone' outcome and continue.
Example fix
// before
client.deregisterScalableTarget(DeregisterScalableTargetRequest.builder()
.serviceNamespace(ns).resourceId(rid).scalableDimension(dim).build());
// after
boolean exists = !client.describeScalableTargets(DescribeScalableTargetsRequest.builder()
.serviceNamespace(ns).resourceIds(rid).scalableDimension(dim).build()
.scalableTargets().isEmpty();
if (exists) {
client.deregisterScalableTarget(DeregisterScalableTargetRequest.builder()
.serviceNamespace(ns).resourceId(rid).scalableDimension(dim).build());
} Defensive patterns
Strategy: try-catch
Validate before calling
// Existence gate before deregister
boolean exists = !client.describeScalableTargets(DescribeScalableTargetsRequest.builder()
.serviceNamespace(ns).resourceIds(rid).scalableDimension(dim).build()
.scalableTargets().isEmpty();
if (exists) {
client.deregisterScalableTarget(DeregisterScalableTargetRequest.builder()
.serviceNamespace(ns).resourceId(rid).scalableDimension(dim).build());
} Try / catch
catch (AwsException e) {
if ("ObjectNotFoundException".equals(e.getCode())) {
// already gone — benign for cleanup flows
log.debug("Scalable target already deregistered: {} {} {}", ns, rid, dim);
return;
}
throw e;
} Prevention
- Make teardown idempotent: swallow ObjectNotFoundException on deregister.
- Reuse the exact resourceId/dimension strings from the register call (verify casing).
- Confirm the region matches where the target was registered.
When it happens
Trigger: Calling DeregisterScalableTarget for a target already deregistered, a resourceId with a typo or different casing (e.g. 'table/MyTable' vs 'table/mytable'), a dimension string mismatch (service prefix differences like 'dynamodb:table:...' vs 'dynamoDB:table:...'), or the wrong region. Any of these make the key lookup miss.
Common situations: Cleanup scripts run twice. Drift between regions (target registered in us-east-1, cleanup pointed at us-west-2). Resource ids rebuilt by string concat with subtle differences from what was registered. Tests that deregister in teardown but never registered in setup.
Related errors
- ValidationException
- ResourceNotFoundException
- ResourceNotFoundException
- ValidationError
- ChangeSetNotFoundException
AI-assisted analysis of floci-io/floci@62ff490619 (2026-08-14).
Data as JSON: /api/errors/fa6dcd5933ffe661.
Report an issue: GitHub.