{"record":{"id":"d05b5b63f5d0dd7f","repo":"apache/pulsar","slug":"update-contains-no-change-d05b5b","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/SinksImpl.java","lineNumber":331,"sourceCode":"        SinkConfig existingSinkConfig = SinkConfigUtils.convertFromDetails(existingComponent.getFunctionDetails());\n        // The rest end points take precedence over whatever is there in functionconfig\n        sinkConfig.setTenant(tenant);\n        sinkConfig.setNamespace(namespace);\n        sinkConfig.setName(sinkName);\n\n        SinkConfig mergedConfig;\n        try {\n            mergedConfig = SinkConfigUtils.validateUpdate(existingSinkConfig, sinkConfig);\n        } catch (Exception e) {\n            throw new RestException(Response.Status.BAD_REQUEST, e.getMessage());\n        }\n\n        if (existingSinkConfig.equals(mergedConfig) && isBlank(sinkPkgUrl) && uploadedInputStream == null\n                && (updateOptions == null || !updateOptions.isUpdateAuthData())) {\n            log.error().attr(\"tenant\", tenant).attr(\"namespace\", namespace).attr(\"componentName\", sinkName)\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                        sinkPkgUrl,\n                        existingComponent.getPackageLocation().getPackagePath(),\n                        uploadedInputStream);\n                functionDetails = validateUpdateRequestParams(tenant, namespace, sinkName,\n                        mergedConfig, componentPackageFile);\n                if (existingComponent.getPackageLocation().getPackagePath().startsWith(Utils.BUILTIN)\n                        && !isFunctionCodeBuiltin(functionDetails)\n                        && (componentPackageFile == null || fileDetail == null)) {","sourceCodeStart":313,"sourceCodeEnd":349,"githubUrl":"https://github.com/apache/pulsar/blob/820761864ed8e2a7d2e52dd9763ad2ae117c1395/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/SinksImpl.java#L313-L349","documentation":"After merging the submitted SinkConfig with the existing one, updateSink rejects the request with HTTP 400 \"Update contains no change\" when the merged config equals the existing config AND no new package URL, no uploaded package file, and no UpdateOptions.isUpdateAuthData flag were supplied. The worker refuses no-op updates because they would needlessly bump metadata versions.","triggerScenarios":"PUT /admin/v3/sinks/{tenant}/{namespace}/{name} with a body identical to the current sink config, no sinkPkgUrl/uploadedInputStream, and updateOptions null or updateAuthData=false.","commonSituations":"Idempotent deployment scripts re-applying the same config; accidentally posting the config fetched from GET unchanged; retry after a failed update that had already taken effect.","solutions":["Verify the update actually changes something; skip the call if the config is unchanged.","Pass UpdateOptions with updateAuthData=true if the intent is to refresh auth data.","Provide a new packageUrl or upload a new archive if the code changed.","Diff your payload against GET /admin/v3/sinks/{tenant}/{namespace}/{name} before submitting."],"exampleFix":"// before: PUT with unchanged config -> 400\nadmin.sinks().updateSink(tenant, ns, name, sameConfig, null, null);\n// after: signal an intentional no-config update\nUpdateOptions opts = UpdateOptions.builder().updateAuthData(true).build();\nadmin.sinks().updateSink(tenant, ns, name, sameConfig, null, null, opts);","handlingStrategy":"validation","validationCode":"SinkConfig current = admin.sinks().getSinkConfig(tenant, ns, name);\nif (current.equals(newConfig)) {\n    return; // skip no-op update\n}","typeGuard":null,"tryCatchPattern":"try {\n    admin.sinks().updateSink(tenant, ns, name, cfg, pkgUrl, null);\n} catch (PulsarAdminException e) {\n    if (\"Update contains no change\".equals(e.getMessage())) {\n        log.info(\"No-op update skipped\");\n    }\n}","preventionTips":["Diff payloads against the live config in deployment scripts.","Make pipelines idempotent by skipping identical configs.","Pass UpdateOptions.updateAuthData(true) when refreshing credentials intentionally."],"tags":["rest-api","bad-request","pulsar-functions","no-op-update"],"backgroundTag":"update-contains-no-change","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"}