dgtlmoon/changedetection.io · error · ValueError

Backup archive decompressed size ({total_uncompressed // (10

Error message

Backup archive decompressed size ({total_uncompressed // (1024 * 1024)} MB) exceeds the {_MAX_DECOMPRESSED_BYTES // (1024 * 1024)} MB limit

What it means

The multi-line expression validator supports JSONPath only in fields explicitly flagged allow_json. When a submitted line contains 'json:' and the field was constructed with allow_json=False, validation rejects it immediately with this message.

Source

Thrown at changedetectionio/blueprint/backups/restore.py:66

    Returns a dict with counts: restored_groups, skipped_groups, restored_watches, skipped_watches.
    Raises zipfile.BadZipFile if the stream is not a valid zip.
    """
    from changedetectionio.model import Tag

    restored_groups = 0
    skipped_groups = 0
    restored_watches = 0
    skipped_watches = 0

    current_tags = datastore.data['settings']['application'].get('tags', {})
    current_watches = datastore.data['watching']

    with tempfile.TemporaryDirectory() as tmpdir:
        logger.debug(f"Restore: extracting zip to {tmpdir}")
        with zipfile.ZipFile(zip_stream, 'r') as zf:
            total_uncompressed = sum(m.file_size for m in zf.infolist())
            if total_uncompressed > _MAX_DECOMPRESSED_BYTES:
                raise ValueError(
                    f"Backup archive decompressed size ({total_uncompressed // (1024 * 1024)} MB) "
                    f"exceeds the {_MAX_DECOMPRESSED_BYTES // (1024 * 1024)} MB limit"
                )
            resolved_dest = os.path.realpath(tmpdir)
            for member in zf.infolist():
                member_dest = os.path.realpath(os.path.join(resolved_dest, member.filename))
                if not member_dest.startswith(resolved_dest + os.sep) and member_dest != resolved_dest:
                    raise ValueError(f"Zip Slip path traversal detected in backup archive: {member.filename!r}")
                zf.extract(member, tmpdir)
        logger.debug("Restore: zip extracted, scanning UUID directories")

        for entry in os.scandir(tmpdir):
            if not entry.is_dir():
                continue

            uuid = entry.name
            if not _UUID_RE.match(uuid):
                logger.warning(f"Restore: skipping non-UUID directory {uuid!r}")

View on GitHub (pinned to 5d9c7c6da7)

Solutions

  1. Move the json: expression to a field that permits JSONPath (the extract_text / JSON-capable field)
  2. If building a custom form, pass allow_json=True to the validator
  3. Strip the 'json:' prefix if you intended a plain XPath

Example fix

# before
# field: css filter (XPath only) -> json:$.a.b
# after
# field: 'Extract text before/after or JSON' -> json:$.a.b
Defensive patterns

Strategy: validation

Validate before calling

def line_allowed(line: str, allow_json: bool) -> bool:
    if 'json:' in line:
        return allow_json
    return True

Type guard

def is_jsonpath_line(line: str) -> bool:
    return line.strip().lower().startswith('json:')

Prevention

When it happens

Trigger: Entering 'json:$.store.book[0].title' into a field whose ValidateMultiDataUrl/validator instance was created without allow_json=True (e.g. the CSS/filter XPath-only field instead of the 'Extract/Convert' text field).

Common situations: Users pasting JSON extraction rules into the wrong form field; plugin/custom watch forms that reuse the validator without enabling JSON support; UI not making clear which fields accept json: prefixes.

Related errors


AI-assisted analysis of dgtlmoon/changedetection.io@5d9c7c6da7 (2026-08-27). Data as JSON: /api/errors/c9eb5e95f9a810dd. Report an issue: GitHub.