{"record":{"id":"1f35e56b86a56daf","repo":"tinyhumansai/openhuman","slug":"patch-filter-provider-does-not-match-source-p","errorCode":null,"errorMessage":"patch filter provider '{}' does not match source provider '{}'","messagePattern":"patch filter provider '(.+?)' does not match source provider '(.+?)'","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"src/openhuman/integrations/task_sources/store.rs","lineNumber":161,"sourceCode":"///\n/// **Implementation note:** this function opens three separate SQLite\n/// connections (read-modify-write + read-back). At settings-panel scale the\n/// overhead is acceptable, but there is a theoretical TOCTOU window between\n/// the initial `get_source` and the subsequent `UPDATE`. A future refactor\n/// could fold all three operations into a single `with_connection` call using\n/// a SQL `UPDATE … RETURNING` pattern.\npub fn update_source(config: &Config, id: &str, patch: TaskSourcePatch) -> Result<TaskSource> {\n    let mut source = get_source(config, id)?;\n\n    if let Some(name) = patch.name {\n        source.name = Some(name).filter(|s| !s.trim().is_empty());\n    }\n    if let Some(enabled) = patch.enabled {\n        source.enabled = enabled;\n    }\n    if let Some(filter) = patch.filter {\n        if filter.provider() != source.provider {\n            anyhow::bail!(\n                \"patch filter provider '{}' does not match source provider '{}'\",\n                filter.provider().as_str(),\n                source.provider.as_str()\n            );\n        }\n        source.filter = filter;\n    }\n    if let Some(interval_secs) = patch.interval_secs {\n        source.interval_secs = interval_secs;\n    }\n    if let Some(target) = patch.target {\n        source.target = target;\n    }\n    if let Some(max) = patch.max_tasks_per_fetch {\n        source.max_tasks_per_fetch = max;\n    }\n    if let Some(connection_id) = patch.connection_id {\n        source.connection_id = Some(connection_id).filter(|s| !s.trim().is_empty());","sourceCodeStart":143,"sourceCodeEnd":179,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/749120085864ce16e0f273c7b86fac7740b39c5b/src/openhuman/integrations/task_sources/store.rs#L143-L179","documentation":"Thrown by `task_sources::store::update_source` when a `TaskSourcePatch` carries a filter whose provider slug (`filter.provider()`) differs from the stored source's `provider`. A task source is bound to exactly one provider (GitHub, Notion, ...), so its filter payload must stay provider-tagged consistently. The check runs before any write, so the row is left untouched.","triggerScenarios":"`config.update_task_source` / `update_source(config, id, patch)` where `patch.filter` was serialized from a different provider's filter spec than the source was created with — e.g. patching a GitHub source with a `notion`-tagged filter, or a client that sends the whole filter object from a different source row as 'unchanged' filler.","commonSituations":"Frontend edit form reusing the filter object of the previously selected provider after the user switched providers; copy-pasting a patch payload between sources of different providers; API clients that always send `filter` even when unchanged, with a default/hardcoded value.","solutions":["Send the patch filter whose provider matches `source.provider` — fetch the source first and mirror its provider in the new filter.","Omit `filter` from the patch entirely when only renaming, toggling `enabled`, `interval_secs`, or `target`.","In the UI, reset the filter object whenever the provider selector changes instead of mutating it in place.","If you genuinely need a different provider, create a new task source rather than patching the old one."],"exampleFix":"// before — patching a github source with a notion filter\nlet patch = TaskSourcePatch { filter: Some(notion_filter), ..Default::default() };\nstore::update_source(&config, \"src-1\", patch)?;\n\n// after — keep provider, or send no filter\nlet current = store::get_source(&config, \"src-1\")?;\nlet patch = TaskSourcePatch {\n    name: Some(\"renamed\".into()),\n    filter: None, // unchanged; only patch what you mean to change\n    ..Default::default()\n};\nstore::update_source(&config, \"src-1\", patch)?;","handlingStrategy":"validation","validationCode":"let current = task_sources::store::get_source(&config, id)?;\nif let Some(filter) = &patch.filter {\n    anyhow::ensure!(\n        filter.provider() == current.provider,\n        \"filter provider {} != source provider {}\",\n        filter.provider().as_str(), current.provider.as_str()\n    );\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Only include fields you intend to change in a patch; leave `filter` as None when untouched.","Reset the filter object in edit forms whenever the provider selection changes.","Never copy a filter payload between sources of different providers."],"tags":["task-sources","validation","provider-mismatch","patch"],"backgroundTag":"provider-mismatch","analyzedSha":"749120085864ce16e0f273c7b86fac7740b39c5b","analyzedAt":"2026-08-17T21:21:45.363Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}