dgtlmoon/changedetection.io · error

abort(404)

Error message

abort(404)

What it means

Straightforward abort(404) when building the per-watch data package zip: the uuid is not present in datastore.data['watching']. The watch lookup happens before any zip work, so this only means 'no such watch'.

Source

Thrown at changedetectionio/blueprint/ui/edit.py:412

            return send_file(buffer, as_attachment=True, download_name=f"{latest_filename}.html", mimetype='text/html')

        # Return a 500 error
        abort(500)

    @edit_blueprint.route("/edit/<uuid_str:uuid>/get-data-package", methods=['GET'])
    @login_optionally_required
    def watch_get_data_package(uuid):
        """Download all data for a single watch as a zip file"""
        from io import BytesIO
        from flask import send_file
        import zipfile
        from pathlib import Path
        import datetime

        watch = datastore.data['watching'].get(uuid)
        if not watch:
            abort(404)

        # Create zip in memory
        memory_file = BytesIO()

        with zipfile.ZipFile(memory_file, 'w',
                           compression=zipfile.ZIP_DEFLATED,
                           compresslevel=8) as zipObj:

            # Add the watch's JSON file if it exists
            watch_json_path = os.path.join(watch.data_dir, 'watch.json')
            if os.path.isfile(watch_json_path):
                zipObj.write(watch_json_path,
                           arcname=os.path.join(uuid, 'watch.json'),
                           compress_type=zipfile.ZIP_DEFLATED,
                           compresslevel=8)

            # Add all files in the watch data directory
            if os.path.isdir(watch.data_dir):

View on GitHub (pinned to 5d9c7c6da7)

Solutions

  1. List current watches (GET /api/watch) and use a valid uuid
  2. Refresh the UI tab — it may reference a deleted watch
  3. Confirm you are talking to the correct instance/datastore if watches were imported elsewhere

Example fix

# before
requests.get(f'{base}/edit/{bad_uuid}/get-data-package').raise_for_status()

# after
watch_uuids = requests.get(f'{base}/api/watch').json().keys()
assert uuid in watch_uuids, 'watch no longer exists'
requests.get(f'{base}/edit/{uuid}/get-data-package').raise_for_status()
Defensive patterns

Strategy: validation

Validate before calling

import requests
def watch_exists(base, uuid, auth=None):
    return uuid in requests.get(f'{base}/api/watch', auth=auth).json()

Try / catch

try:
    r = requests.get(f'{base}/edit/{uuid}/get-data-package')
    r.raise_for_status()
except requests.HTTPError as e:
    if e.response.status_code == 404:
        raise KeyError(f'watch {uuid} not found') from e
    raise

Prevention

When it happens

Trigger: GET /edit/<uuid>/get-data-package with a deleted watch uuid, a typo'd uuid, or a uuid from a different datastore instance.

Common situations: Stale UI tabs/bookmarks pointing at deleted watches; scripts holding cached uuids after watches were removed and re-imported; uuid copied with extra characters or wrong case.

Related errors


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