apache/pulsar · warning · RestException
Update contains no change
Error message
Update contains no change
What it means
updateFunction rejects no-op updates: if the merged FunctionConfig equals the existing config AND no package URL, no uploaded input stream, and no updateAuthData flag were supplied, nothing would change in metadata, so the worker throws HTTP 400 'Update contains no change'. This guards against accidental re-PUTs that would otherwise churn the metadata store.
Source
Thrown at pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/FunctionsImpl.java:325
FunctionConfig existingFunctionConfig = FunctionConfigUtils
.convertFromDetails(existingComponent.getFunctionDetails());
// The rest end points take precedence over whatever is there in function config
functionConfig.setTenant(tenant);
functionConfig.setNamespace(namespace);
functionConfig.setName(functionName);
FunctionConfig mergedConfig;
try {
mergedConfig = FunctionConfigUtils.validateUpdate(existingFunctionConfig, functionConfig);
} catch (Exception e) {
throw new RestException(Response.Status.BAD_REQUEST, e.getMessage());
}
if (existingFunctionConfig.equals(mergedConfig) && isBlank(functionPkgUrl) && uploadedInputStream == null
&& (updateOptions == null || !updateOptions.isUpdateAuthData())) {
log.error().attr("tenant", tenant).attr("namespace", namespace).attr("componentName", functionName)
.log("/ / Update contains no changes");
throw new RestException(Response.Status.BAD_REQUEST, "Update contains no change");
}
FunctionDetails functionDetails;
File componentPackageFile = null;
try {
// validate parameters
try {
componentPackageFile = getPackageFile(
componentType,
functionPkgUrl,
existingComponent.getPackageLocation().getPackagePath(),
uploadedInputStream);
functionDetails = validateUpdateRequestParams(tenant, namespace, functionName,
mergedConfig, componentPackageFile);
if (existingComponent.getPackageLocation().getPackagePath().startsWith(Utils.BUILTIN)
&& !isFunctionCodeBuiltin(functionDetails)
&& (componentPackageFile == null || fileDetail == null)) {View on GitHub (pinned to 820761864e)
Solutions
- Confirm the update is intentionally a no-op; if so, catch and treat this 400 as success in your retry logic.
- If you intended a change, verify the merged config differs — compare GET output against your payload field by field.
- If only the auth data/secrets changed, pass UpdateOptions with setUpdateAuthData(true) or provide the new package URL/artifact.
- Ensure your serialization isn't dropping fields (e.g. null defaults) that would make the config compare equal.
Example fix
// before
try { admin.functions().updateFunction(t, ns, fn, sameCfg, null); }
catch (PulsarAdminException e) { /* retries forever on 'Update contains no change' */ }
// after
try {
admin.functions().updateFunction(t, ns, fn, sameCfg, null);
} catch (PulsarAdminException e) {
if (e.getStatusCode() == 400 && e.getMessage().contains("no change")) {
return; // idempotent no-op, treat as success
}
throw e;
} Defensive patterns
Strategy: try-catch
Validate before calling
FunctionConfig current = admin.functions().getFunctionConfig(t, ns, fn);
if (current.equals(submitted) && pkgUrl == null && !updateAuthData) {
return; // skip pointless update entirely
} Try / catch
try {
admin.functions().updateFunction(t, ns, fn, cfg, opts);
} catch (PulsarAdminException e) {
if (e.getStatusCode() == 400 && e.getMessage().contains("no change")) {
return; // idempotent success — nothing to update
}
throw e;
} Prevention
- Compare desired vs current config before issuing updates.
- In retry logic, treat this 400 as a successful (already-applied) update.
- When secrets rotate, set UpdateOptions.setUpdateAuthData(true) or the change will be skipped.
- Verify serialization preserves all fields so intended diffs actually differ.
When it happens
Trigger: Re-submitting the identical FunctionConfig without functionPkgUrl, uploadedInputStream, or UpdateOptions.setUpdateAuthData(true); retry logic blindly replaying a completed update; config fetched, untouched, and PUT back.
Common situations: Idempotent-retry wrappers in deployment pipelines; the previous update actually succeeded and the retry is a genuine no-op; scripts that GET-then-PUT with no modifications; forgetting isUpdateAuthData when credentials actually rotated (the real change was silently skipped).
Related errors
- %s %s cannot be admitted:- %s
- %s %s doesn't exist
- e.getMessage()
- cluster data is required
- RecordSequence needs to be specified for every record while
AI-assisted analysis of apache/pulsar@820761864e (2026-09-06).
Data as JSON: /api/errors/848d3c0072e6eb15.
Report an issue: GitHub.