tinyhumansai/openhuman · error · anyhow::Error

patch filter provider '{}' does not match source provider '{

Error message

patch filter provider '{}' does not match source provider '{}'

What it means

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.

Source

Thrown at src/openhuman/integrations/task_sources/store.rs:161

///
/// **Implementation note:** this function opens three separate SQLite
/// connections (read-modify-write + read-back). At settings-panel scale the
/// overhead is acceptable, but there is a theoretical TOCTOU window between
/// the initial `get_source` and the subsequent `UPDATE`. A future refactor
/// could fold all three operations into a single `with_connection` call using
/// a SQL `UPDATE … RETURNING` pattern.
pub fn update_source(config: &Config, id: &str, patch: TaskSourcePatch) -> Result<TaskSource> {
    let mut source = get_source(config, id)?;

    if let Some(name) = patch.name {
        source.name = Some(name).filter(|s| !s.trim().is_empty());
    }
    if let Some(enabled) = patch.enabled {
        source.enabled = enabled;
    }
    if let Some(filter) = patch.filter {
        if filter.provider() != source.provider {
            anyhow::bail!(
                "patch filter provider '{}' does not match source provider '{}'",
                filter.provider().as_str(),
                source.provider.as_str()
            );
        }
        source.filter = filter;
    }
    if let Some(interval_secs) = patch.interval_secs {
        source.interval_secs = interval_secs;
    }
    if let Some(target) = patch.target {
        source.target = target;
    }
    if let Some(max) = patch.max_tasks_per_fetch {
        source.max_tasks_per_fetch = max;
    }
    if let Some(connection_id) = patch.connection_id {
        source.connection_id = Some(connection_id).filter(|s| !s.trim().is_empty());

View on GitHub (pinned to 7491200858)

Solutions

  1. Send the patch filter whose provider matches `source.provider` — fetch the source first and mirror its provider in the new filter.
  2. Omit `filter` from the patch entirely when only renaming, toggling `enabled`, `interval_secs`, or `target`.
  3. In the UI, reset the filter object whenever the provider selector changes instead of mutating it in place.
  4. If you genuinely need a different provider, create a new task source rather than patching the old one.

Example fix

// before — patching a github source with a notion filter
let patch = TaskSourcePatch { filter: Some(notion_filter), ..Default::default() };
store::update_source(&config, "src-1", patch)?;

// after — keep provider, or send no filter
let current = store::get_source(&config, "src-1")?;
let patch = TaskSourcePatch {
    name: Some("renamed".into()),
    filter: None, // unchanged; only patch what you mean to change
    ..Default::default()
};
store::update_source(&config, "src-1", patch)?;
Defensive patterns

Strategy: validation

Validate before calling

let current = task_sources::store::get_source(&config, id)?;
if let Some(filter) = &patch.filter {
    anyhow::ensure!(
        filter.provider() == current.provider,
        "filter provider {} != source provider {}",
        filter.provider().as_str(), current.provider.as_str()
    );
}

Prevention

When it happens

Trigger: `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.

Common situations: 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.

Related errors


AI-assisted analysis of tinyhumansai/openhuman@7491200858 (2026-08-17). Data as JSON: /api/errors/1f35e56b86a56daf. Report an issue: GitHub.