{"record":{"id":"c9d7072c7217f66c","repo":"dgtlmoon/changedetection.io","slug":"invalid-notification-format-value","errorCode":null,"errorMessage":"Invalid notification format: \"{value}\"","messagePattern":"Invalid notification format: \"(.+?)\"","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"changedetectionio/notification_service.py","lineNumber":274,"sourceCode":"        n_format = self.get('notification_format')\n        if n_format and not valid_notification_formats.get(n_format):\n            raise ValueError(f'Invalid notification format: \"{n_format}\"')\n\n    def set_random_for_validation(self):\n        import random, string\n        \"\"\"Randomly fills all dict keys with random strings (for validation/testing). \n        So we can test the output in the notification body\n        \"\"\"\n        for key in self.keys():\n            if key in ['uuid', 'time', 'watch_uuid', 'change_datetime'] or key.startswith('diff'):\n                continue\n            rand_str = 'RANDOM-PLACEHOLDER-'+''.join(random.choices(string.ascii_letters + string.digits, k=12))\n            self[key] = rand_str\n\n    def __setitem__(self, key, value):\n        if key == 'notification_format' and isinstance(value, str) and not value.startswith('RANDOM-PLACEHOLDER-'):\n            if not valid_notification_formats.get(value):\n                raise ValueError(f'Invalid notification format: \"{value}\"')\n\n        super().__setitem__(key, value)\n\ndef add_rendered_diff_to_notification_vars(notification_scan_text:str, prev_snapshot:str, current_snapshot:str, word_diff:bool, escape_output:bool=False):\n    \"\"\"\n    Efficiently renders only the diff placeholders that are actually used in the notification text.\n\n    Scans the notification template for diff placeholder usage (diff, diff_added, diff_clean, etc.)\n    and only renders those specific variants, avoiding expensive render_diff() calls for unused placeholders.\n    Uses LRU caching to avoid duplicate renders when multiple placeholders share the same arguments.\n\n    Args:\n        notification_scan_text: The notification template text to scan for placeholders\n        prev_snapshot: Previous version of content for diff comparison\n        current_snapshot: Current version of content for diff comparison\n        word_diff: Whether to use word-level (True) or line-level (False) diffing\n        escape_output: If True, the rendered diff output is HTML-escaped. Used for HTML-format\n            notifications so attacker-controlled page content can't inject live markup.","sourceCodeStart":256,"sourceCodeEnd":292,"githubUrl":"https://github.com/dgtlmoon/changedetection.io/blob/5d9c7c6da76340597243e8163c4f2439237fa0e8/changedetectionio/notification_service.py#L256-L292","documentation":"The same notification dict also validates on every __setitem__: assigning notification_format to an unknown value raises ValueError immediately, unless the value starts with 'RANDOM-PLACEHOLDER-' (used by test fixture filling).","triggerScenarios":"d['notification_format'] = 'html' (wrong case or unofficial name); programmatically copying config between objects where the format string came from user input or an unvalidated source; merge/update of dict-like data into the notification object.","commonSituations":"Case-sensitivity mistakes ('html' vs 'HTML'); importing watch URLs/JSON from another instance with a format name the current version doesn't recognise; scripts that set the key from unvalidated HTTP input.","solutions":["Assign only keys present in valid_notification_formats (verify exact casing)","Validate/normalize user-supplied format strings against valid_notification_formats.keys() before assignment","When bulk-loading external configs, wrap the update in try/except and coerce or drop invalid formats"],"exampleFix":"# before\nnotification_obj['notification_format'] = fmt_from_user\n# after\nfrom changedetectionio.notification_service import valid_notification_formats\nif fmt_from_user in valid_notification_formats:\n    notification_obj['notification_format'] = fmt_from_user\nelse:\n    notification_obj['notification_format'] = 'Text'","handlingStrategy":"validation","validationCode":"from changedetectionio.notification_service import valid_notification_formats\nif new_format in valid_notification_formats:\n    d['notification_format'] = new_format\nelse:\n    d.pop('notification_format', None)  # fall back to default","typeGuard":"def is_assignable_format(value) -> bool:\n    from changedetectionio.notification_service import valid_notification_formats\n    return not isinstance(value, str) or value.startswith('RANDOM-PLACEHOLDER-') or value in valid_notification_formats","tryCatchPattern":"try:\n    d['notification_format'] = fmt\nexcept ValueError:\n    d['notification_format'] = 'Text'","preventionTips":["Never assign unvalidated user input directly to notification_format","Watch casing: formats are case-sensitive keys","When bulk-merging dicts, validate keys against the enum first"],"tags":["notification","config","validation","dict-setitem"],"backgroundTag":"invalid-config-value","analyzedSha":"5d9c7c6da76340597243e8163c4f2439237fa0e8","analyzedAt":"2026-08-27T19:41:16.067Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}