{"record":{"id":"1b304be408a8bab7","repo":"apache/pulsar","slug":"update-contains-no-change-1b304b","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/SourcesImpl.java","lineNumber":324,"sourceCode":"        SourceConfig existingSourceConfig =\n                SourceConfigUtils.convertFromDetails(existingComponent.getFunctionDetails());\n        // The rest end points take precedence over whatever is there in functionconfig\n        sourceConfig.setTenant(tenant);\n        sourceConfig.setNamespace(namespace);\n        sourceConfig.setName(sourceName);\n        SourceConfig mergedConfig;\n        try {\n            mergedConfig = SourceConfigUtils.validateUpdate(existingSourceConfig, sourceConfig);\n        } catch (Exception e) {\n            throw new RestException(Response.Status.BAD_REQUEST, e.getMessage());\n        }\n\n        if (existingSourceConfig.equals(mergedConfig) && isBlank(sourcePkgUrl) && uploadedInputStream == null\n            && (updateOptions == null || !updateOptions.isUpdateAuthData())) {\n            log.error().attr(\"tenant\", tenant).attr(\"namespace\", namespace).attr(\"componentName\", sourceName)\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                        sourcePkgUrl,\n                        existingComponent.getPackageLocation().getPackagePath(),\n                        uploadedInputStream);\n                functionDetails = validateUpdateRequestParams(tenant, namespace, sourceName,\n                        mergedConfig, componentPackageFile);\n                if (existingComponent.getPackageLocation().getPackagePath().startsWith(Utils.BUILTIN)\n                        && !isFunctionCodeBuiltin(functionDetails)\n                        && (componentPackageFile == null || fileDetail == null)) {","sourceCodeStart":306,"sourceCodeEnd":342,"githubUrl":"https://github.com/apache/pulsar/blob/820761864ed8e2a7d2e52dd9763ad2ae117c1395/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/SourcesImpl.java#L306-L342","documentation":"updateSource rejects a request that would produce no effect: the merged SourceConfig equals the existing one, no package URL and no uploaded package file are supplied, and updateOptions either is null or does not request auth-data update. The worker short-circuits with 400 BAD_REQUEST instead of performing a pointless metadata update.","triggerScenarios":"PUT /admin/v3/sources/{tenant}/{namespace}/{sourceName} where the submitted config is byte-for-byte equivalent to the stored config after merging, isBlank(sourcePkgUrl), uploadedInputStream == null, and updateOptions == null || !updateOptions.isUpdateAuthData().","commonSituations":" Re-running an automation/deployment script that applies a desired state already applied; resubmitting the same config fetched from GET; a CI job that always PUTs the source config even when nothing changed.","solutions":["Change something real: modify a mutable field in the SourceConfig (e.g. configs, parallelism, processing guarantees)","Provide a new package via sourcePkgUrl or upload the package in the request","If the update is only about secrets/auth data, pass UpdateOptions with updateAuthData=true","If the no-op is intentional, skip the API call — compare the existing config to the desired one before invoking update"],"exampleFix":"// before\nadmin.sources().updateSource(tenant, ns, name, sameConfig);\n// after\nUpdateOptions opts = new UpdateOptions();\nopts.setUpdateAuthData(true); // if only refreshing auth data\nadmin.sources().updateSource(tenant, ns, name, sameConfig, opts);\n// or skip entirely:\nif (!existing.equals(desired)) { admin.sources().updateSource(...); }","handlingStrategy":"validation","validationCode":"SourceConfig existing = admin.sources().getSourceConfig(tenant, ns, name);\nSourceConfig merged = SourceConfigUtils.validateUpdate(existing, desired);\nboolean authData = opts != null && opts.isUpdateAuthData();\nif (existing.equals(merged) && pkgUrl == null && uploadStream == null && !authData) {\n    log.info(\"Skipping no-op source update for {}\", name);\n} else {\n    admin.sources().updateSource(tenant, ns, name, desired, opts);\n}","typeGuard":null,"tryCatchPattern":"try {\n    admin.sources().updateSource(tenant, ns, name, cfg);\n} catch (PulsarAdminException e) {\n    if (e.getMessage() != null && e.getMessage().contains(\"no change\")) {\n        log.info(\"Source {} already up to date\", name); // treat as success\n    } else { throw e; }\n}","preventionTips":["Compare desired state with current state before issuing updates","Idempotently skip PUTs when the fetched config equals the desired config","Set updateAuthData=true only when secrets actually changed"],"tags":["rest-api","no-op","pulsar-functions","bad-request"],"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"}