{"record":{"id":"fb11b103994775ce","repo":"louislam/uptime-kuma","slug":"domain-expiry-unsupported-missing-target","errorCode":null,"errorMessage":"domain_expiry_unsupported_missing_target","messagePattern":"domain_expiry_unsupported_missing_target","errorType":"validation","errorClass":"TranslatableError","httpStatus":null,"severity":"warning","filePath":"server/model/domain_expiry.js","lineNumber":218,"sourceCode":"     * @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\n        const publicSuffix = tld.publicSuffix;\n        const rootTld = publicSuffix.split(\".\").pop();\n        const rdap = await getRdapServer(publicSuffix);\n        if (!rdap) {\n            throw new TranslatableError(\"domain_expiry_unsupported_unsupported_tld_no_rdap_endpoint\", {","sourceCodeStart":200,"sourceCodeEnd":236,"githubUrl":"https://github.com/louislam/uptime-kuma/blob/6b5ea0155793e666666745fb8d6fef1e829543a2/server/model/domain_expiry.js#L200-L236","documentation":"After the type check passes, checkSupport looks up the relevant field (url, hostname, or grpcUrl) on the monitor and reads `monitor[targetField]`. If that value is not a string or is empty, this TranslatableError is thrown. So the monitor type is fine, but the specific target cell is blank or wrong-typed (e.g. null, undefined, a number).","triggerScenarios":"An http monitor with an empty url; a ping monitor whose hostname is null because the row was created before the column existed; a grpc-keyword monitor with grpcUrl unset; a monitor whose url field is an object instead of a string due to a malformed API payload.","commonSituations":"Monitors created via the API with a missing required field; older monitors upgraded across schema versions where a new column is null; frontend bug that sent an empty string for the target.","solutions":["Open the monitor in the Edit Monitor UI and fill in the URL/hostname field relevant to its type, then save.","If scripting, ensure the payload includes the right field: url for http/keyword/json-query/real-browser/websocket-upgrade, hostname for port/ping/dns/smtp/snmp, grpcUrl for grpc-keyword.","Run a DB check for empty targets: `SELECT id,type,url,hostname,grpc_url FROM monitor WHERE type IN ('http','ping','port','dns','smtp','snmp','grpc-keyword','keyword','json-query','real-browser','websocket-upgrade') AND (url IS NULL OR url='') ...`.","Validate on the API boundary before persistence so a monitor is never saved without its target."],"exampleFix":"// before\nconst target = monitor[targetField];\nif (typeof target !== \"string\" || target.length === 0) {\n    throw new TranslatableError(\"domain_expiry_unsupported_missing_target\");\n}\n\n// after — also surface which field was empty\nthrow new TranslatableError(\"domain_expiry_unsupported_missing_target\", { field: targetField });","handlingStrategy":"validation","validationCode":"const { TYPES_WITH_DOMAIN_EXPIRY_SUPPORT_VIA_FIELD } = require(\"../src/util\");\nfunction assertTargetPresent(monitor) {\n    const field = TYPES_WITH_DOMAIN_EXPIRY_SUPPORT_VIA_FIELD[monitor.type];\n    const v = monitor[field];\n    if (typeof v !== \"string\" || v.trim().length === 0) {\n        throw new Error(`Monitor ${monitor.id} (${monitor.type}) has no ${field}`);\n    }\n}","typeGuard":"function monitorHasDomainTarget(monitor) {\n    const field = TYPES_WITH_DOMAIN_EXPIRY_SUPPORT_VIA_FIELD[monitor.type];\n    return typeof monitor?.[field] === \"string\" && monitor[field].length > 0;\n}","tryCatchPattern":"try {\n    await DomainExpiry.checkSupport(monitor);\n} catch (e) {\n    if (e.msgi18n && e.message === \"domain_expiry_unsupported_missing_target\") {\n        // prompt the user to fill in the URL/hostname field\n    }\n}","preventionTips":["Make the target field required in the UI when domain-expiry is enabled.","Validate on the API boundary so a monitor without a target cannot be saved.","Periodically scan the DB for monitors with NULL target fields."],"tags":["domain-expiry","validation","monitor"],"backgroundTag":null,"analyzedSha":"6b5ea0155793e666666745fb8d6fef1e829543a2","analyzedAt":"2026-08-12T23:42:12.959Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}