{"record":{"id":"848d3c0072e6eb15","repo":"apache/pulsar","slug":"update-contains-no-change","errorCode":null,"errorMessage":"Update contains no change","messagePattern":"Update contains no change","errorType":"http","errorClass":"RestException","httpStatus":400,"severity":"warning","filePath":"pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/FunctionsImpl.java","lineNumber":325,"sourceCode":"        FunctionConfig existingFunctionConfig = FunctionConfigUtils\n                .convertFromDetails(existingComponent.getFunctionDetails());\n        // The rest end points take precedence over whatever is there in function config\n        functionConfig.setTenant(tenant);\n        functionConfig.setNamespace(namespace);\n        functionConfig.setName(functionName);\n        FunctionConfig mergedConfig;\n        try {\n            mergedConfig = FunctionConfigUtils.validateUpdate(existingFunctionConfig, functionConfig);\n        } catch (Exception e) {\n            throw new RestException(Response.Status.BAD_REQUEST, e.getMessage());\n        }\n\n        if (existingFunctionConfig.equals(mergedConfig) && isBlank(functionPkgUrl) && uploadedInputStream == null\n                && (updateOptions == null || !updateOptions.isUpdateAuthData())) {\n            log.error().attr(\"tenant\", tenant).attr(\"namespace\", namespace).attr(\"componentName\", functionName)\n\n                    .log(\"/ / Update contains no changes\");\n            throw new RestException(Response.Status.BAD_REQUEST, \"Update contains no change\");\n        }\n\n        FunctionDetails functionDetails;\n        File componentPackageFile = null;\n        try {\n\n            // validate parameters\n            try {\n                componentPackageFile = getPackageFile(\n                        componentType,\n                        functionPkgUrl,\n                        existingComponent.getPackageLocation().getPackagePath(),\n                        uploadedInputStream);\n                functionDetails = validateUpdateRequestParams(tenant, namespace, functionName,\n                        mergedConfig, componentPackageFile);\n                if (existingComponent.getPackageLocation().getPackagePath().startsWith(Utils.BUILTIN)\n                        && !isFunctionCodeBuiltin(functionDetails)\n                        && (componentPackageFile == null || fileDetail == null)) {","sourceCodeStart":307,"sourceCodeEnd":343,"githubUrl":"https://github.com/apache/pulsar/blob/820761864ed8e2a7d2e52dd9763ad2ae117c1395/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/FunctionsImpl.java#L307-L343","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","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."],"exampleFix":"// before\ntry { admin.functions().updateFunction(t, ns, fn, sameCfg, null); }\ncatch (PulsarAdminException e) { /* retries forever on 'Update contains no change' */ }\n// after\ntry {\n    admin.functions().updateFunction(t, ns, fn, sameCfg, null);\n} catch (PulsarAdminException e) {\n    if (e.getStatusCode() == 400 && e.getMessage().contains(\"no change\")) {\n        return; // idempotent no-op, treat as success\n    }\n    throw e;\n}","handlingStrategy":"try-catch","validationCode":"FunctionConfig current = admin.functions().getFunctionConfig(t, ns, fn);\nif (current.equals(submitted) && pkgUrl == null && !updateAuthData) {\n    return; // skip pointless update entirely\n}","typeGuard":null,"tryCatchPattern":"try {\n    admin.functions().updateFunction(t, ns, fn, cfg, opts);\n} catch (PulsarAdminException e) {\n    if (e.getStatusCode() == 400 && e.getMessage().contains(\"no change\")) {\n        return; // idempotent success — nothing to update\n    }\n    throw e;\n}","preventionTips":["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."],"tags":["rest-api","pulsar-functions","idempotency","http-400"],"backgroundTag":"no-op-update-rejected","analyzedSha":"820761864ed8e2a7d2e52dd9763ad2ae117c1395","analyzedAt":"2026-09-06T00:14:20.138Z","contentChangedAt":"2026-09-06T00:14:20.138Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}