apache/iceberg · error · IllegalStateException
Invalid planStatus: for planId
Error message
Invalid planStatus: %s for planId: %s
What it means
Thrown when the FetchScanPlanningResult returns a planStatus that is not one of COMPLETED, SUBMITTED, or FAILED — i.e. the server sent an unknown or null plan status during scan planning. This guards against protocol drift between client and server.
Solutions
- Upgrade the Iceberg client so its PlanStatus enum knows the server's status value
- Fix the server/proxy to emit only spec-defined planStatus values
- Validate the response body with curl against the REST spec
- Capture the full response for the planId and compare with the OpenAPI spec
Example fix
// before: client enum lacks new status
// after: upgrade iceberg-core to a version whose PlanStatus includes the value,
// or pin server to emit standard statuses:
// {"plan-status": "completed", ...} Defensive patterns
Strategy: try-catch
Validate before calling
// validate response shape before interpreting PlanStatus status = response.planStatus(); boolean known = status != null && (status == PlanStatus.COMPLETED || status == PlanStatus.SUBMITTED || status == PlanStatus.FAILED);
Type guard
boolean isKnownPlanStatus(FetchScanPlanningResult r) {
return r != null && r.planStatus() != null
&& EnumSet.of(PlanStatus.COMPLETED, PlanStatus.SUBMITTED, PlanStatus.FAILED, PlanStatus.CANCELLED)
.contains(r.planStatus());
} Try / catch
try { scan.planFiles(); } catch (IllegalStateException e) {
if (e.getMessage().startsWith("Invalid planStatus")) { /* upgrade client / inspect server response */ }
else throw e;
} Prevention
- Pin client and server to the same Iceberg REST spec version
- Avoid proxies that rewrite JSON responses
- Test against the real catalog, not hand-rolled mocks
When it happens
Trigger: planTableScan/planFiles receives a response whose planStatus is null or an unrecognized value — typically a newer/older server version emitting a status the client enum cannot map, or a buggy/misbehaving proxy rewriting the response.
Common situations: Client and server version mismatch (server added a new plan status), hand-rolled mock REST servers returning malformed plan responses, reverse proxies stripping or corrupting JSON fields.
Understand the failure class
Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.
Related errors
- failureMessage(id, response.errorResponse())
- failureMessage(planId, response.errorResponse())
- Invalid scan planning mode
- Invalid snapshot mode
- Remote scan planning cancelled for planId
AI-assisted analysis of apache/iceberg@86d9c8fc54 (2026-09-12).
Data as JSON: /api/errors/fd7b9b5c6eeef5ed.
Report an issue: GitHub.
Appendix: source
Thrown at core/src/main/java/org/apache/iceberg/rest/RESTTableScan.java:224
ErrorHandlers.tableErrorHandler(),
stringStringMap -> {},
parserContext);
this.planId = response.planId();
PlanStatus planStatus = response.planStatus();
this.scanFileIO =
!response.credentials().isEmpty() ? scanFileIO(response.credentials()) : table().io();
switch (planStatus) {
case COMPLETED:
return scanTasksIterable(response.planTasks(), response.fileScanTasks());
case SUBMITTED:
Endpoint.check(supportedEndpoints, Endpoint.V1_FETCH_TABLE_SCAN_PLAN);
return fetchPlanningResult();
case FAILED:
throw new IllegalStateException(failureMessage(planId, response.errorResponse()));
default:
throw new IllegalStateException(
String.format("Invalid planStatus: %s for planId: %s", planStatus, planId));
}
}
private FileIO scanFileIO(List<Credential> storageCredentials) {
ImmutableMap.Builder<String, String> builder =
ImmutableMap.<String, String>builder().putAll(catalogProperties);
if (null != planId) {
builder.put(RESTCatalogProperties.REST_SCAN_PLAN_ID, planId);
}
Map<String, String> properties = builder.buildKeepingLast();
FileIO ioForScan =
CatalogUtil.loadFileIO(
catalogProperties.getOrDefault(CatalogProperties.FILE_IO_IMPL, DEFAULT_FILE_IO_IMPL),
properties,
hadoopConf,
storageCredentials.stream()View on GitHub (pinned to 86d9c8fc54)