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

  1. Confirm the update is intentionally a no-op; if so, catch and treat this 400 as success in your retry logic.
  2. If you intended a change, verify the merged config differs — compare GET output against your payload field by field.
  3. If only the auth data/secrets changed, pass UpdateOptions with setUpdateAuthData(true) or provide the new package URL/artifact.
  4. 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

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


AI-assisted analysis of apache/pulsar@820761864e (2026-09-06). Data as JSON: /api/errors/848d3c0072e6eb15. Report an issue: GitHub.