{"record":{"id":"a25e91f39124314d","repo":"louislam/uptime-kuma","slug":"domain-expiry-unsupported-monitor-type","errorCode":null,"errorMessage":"domain_expiry_unsupported_monitor_type","messagePattern":"domain_expiry_unsupported_monitor_type","errorType":"validation","errorClass":"TranslatableError","httpStatus":null,"severity":"warning","filePath":"server/model/domain_expiry.js","lineNumber":213,"sourceCode":"    parseName() {\n        return parseTld(this.domain);\n    }\n\n    /**\n     * @returns {(null|object)} parsed domain tld\n     */\n    get tld() {\n        return this.parseName().publicSuffix;\n    }\n\n    /**\n     * @param {Monitor} monitor Monitor object\n     * @throws {TranslatableError} Throws an error if the monitor type is unsupported or missing target.\n     * @returns {Promise<{ domain: string, tld: string }>} Domain expiry support info\n     */\n    static async checkSupport(monitor) {\n        if (!(monitor.type in TYPES_WITH_DOMAIN_EXPIRY_SUPPORT_VIA_FIELD)) {\n            throw new TranslatableError(\"domain_expiry_unsupported_monitor_type\");\n        }\n        const targetField = TYPES_WITH_DOMAIN_EXPIRY_SUPPORT_VIA_FIELD[monitor.type];\n        const target = monitor[targetField];\n        if (typeof target !== \"string\" || target.length === 0) {\n            throw new TranslatableError(\"domain_expiry_unsupported_missing_target\");\n        }\n\n        const tld = parseTld(target);\n\n        // It must be checked first, filter out non-ICANN domains.\n        if (!tld.isIcann) {\n            throw new TranslatableError(\"domain_expiry_unsupported_is_icann\", {\n                // If domain is null, use hostname as fallback for better error message.\n                domain: tld.domain ?? tld.hostname ?? \"EMPTY DOMAIN\",\n                publicSuffix: tld.publicSuffix,\n            });\n        }\n","sourceCodeStart":195,"sourceCodeEnd":231,"githubUrl":"https://github.com/louislam/uptime-kuma/blob/6b5ea0155793e666666745fb8d6fef1e829543a2/server/model/domain_expiry.js#L195-L231","documentation":"DomainExpiry.checkSupport(monitor) throws this TranslatableError when monitor.type is not a key in TYPES_WITH_DOMAIN_EXPIRY_SUPPORT_VIA_FIELD (src/util.ts:778). Supported types are: http, keyword, json-query, real-browser, websocket-upgrade (via 'url'), port, ping, dns, smtp, snmp (via 'hostname'), and grpc-keyword (via 'grpcUrl'). Because it extends TranslatableError, the frontend treats the message as a translation key (msgi18n=true) rather than a literal string.","triggerScenarios":"Calling checkSupport with a monitor whose type is one of the unsupported kinds — push, docker, gamedig, mongodb, mysql, mqtt, radius, kahoot, selenium, ntfy, icmp-ping, dns-ping, steam-db, or any future type not in the map. The EditMonitor.vue gates the UI on the same map, so reaching this error usually means a direct API call or a type that was added without updating the map.","commonSituations":"Adding a new monitor type to the codebase but forgetting to extend TYPES_WITH_DOMAIN_EXPIRY_SUPPORT_VIA_FIELD; a custom plugin monitor type; calling the domain-expiry API endpoint with an arbitrary monitor id whose type is unsupported.","solutions":["Switch the monitor to a supported type if you genuinely need domain-expiry tracking (http/keyword/json-query/port/ping/dns/smtp/snmp/grpc-keyword/real-browser/websocket-upgrade).","If you are developing a new monitor type that has a hostname, add it to TYPES_WITH_DOMAIN_EXPIRY_SUPPORT_VIA_FIELD in src/util.ts with the correct field name and recompile.","In the UI, the domain-expiry option is hidden for unsupported types — if you are seeing this from the API, reproduce via the UI to confirm support status.","Catch TranslatableError at the call site and show the i18n key to the user instead of crashing."],"exampleFix":"// before\nif (!(monitor.type in TYPES_WITH_DOMAIN_EXPIRY_SUPPORT_VIA_FIELD)) {\n    throw new TranslatableError(\"domain_expiry_unsupported_monitor_type\");\n}\n\n// after — when adding a new type, extend the map first\n// src/util.ts\n// exports.TYPES_WITH_DOMAIN_EXPIRY_SUPPORT_VIA_FIELD[\"my-type\"] = \"hostname\";","handlingStrategy":"type-guard","validationCode":"const { TYPES_WITH_DOMAIN_EXPIRY_SUPPORT_VIA_FIELD } = require(\"../src/util\");\nfunction assertMonitorSupportsDomainExpiry(monitor) {\n    if (!(monitor.type in TYPES_WITH_DOMAIN_EXPIRY_SUPPORT_VIA_FIELD)) {\n        throw new Error(`Monitor type '${monitor.type}' does not support domain-expiry tracking`);\n    }\n}","typeGuard":"function monitorSupportsDomainExpiry(monitor) {\n    return monitor?.type in TYPES_WITH_DOMAIN_EXPIRY_SUPPORT_VIA_FIELD;\n}","tryCatchPattern":"try {\n    await DomainExpiry.checkSupport(monitor);\n} catch (e) {\n    if (e.msgi18n && e.message === \"domain_expiry_unsupported_monitor_type\") {\n        // hide/disable the domain-expiry option in the UI; not an error condition\n    }\n}","preventionTips":["When adding a new monitor type, update TYPES_WITH_DOMAIN_EXPIRY_SUPPORT_VIA_FIELD in the same PR.","Gate the UI option on the same map so users never see an unsupported combination.","Treat TranslatableError as an expected control-flow signal, not a crash."],"tags":["domain-expiry","validation","monitor","i18n"],"backgroundTag":null,"analyzedSha":"6b5ea0155793e666666745fb8d6fef1e829543a2","analyzedAt":"2026-08-12T23:42:12.959Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}