{"record":{"id":"a91fdf0598bda748","repo":"outline/outline","slug":"must-be-a-valid-url","errorCode":null,"errorMessage":"Must be a valid url","messagePattern":"Must be a valid url","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"server/models/validators/IsUrl.ts","lineNumber":21,"sourceCode":"import env from \"@server/env\";\n\n/**\n * A decorator that validates that a string is a valid HTTP(S) url. A top-level\n * domain is only required when cloud hosted, allowing self-hosted installations\n * to use internal hostnames.\n */\nexport default function IsUrl(target: object, propertyName: string) {\n  return addAttributeOptions(target, propertyName, {\n    validate: {\n      validUrl(value: string) {\n        if (\n          !isURL(value, {\n            protocols: [\"http\", \"https\"],\n            require_protocol: true,\n            require_tld: env.isCloudHosted,\n          })\n        ) {\n          throw new Error(\"Must be a valid url\");\n        }\n      },\n    },\n  });\n}\n","sourceCodeStart":3,"sourceCodeEnd":27,"githubUrl":"https://github.com/outline/outline/blob/935a44d4c003429645a956141b2c4eb695f34b6e/server/models/validators/IsUrl.ts#L3-L27","documentation":"The IsUrl decorator validates that a value is a well-formed URL with http/https protocol required, and (in cloud-hosted mode) a TLD required. A value missing the protocol, using a non-http scheme, or lacking a TLD in cloud mode throws at save time.","triggerScenarios":"Saving a URL column (e.g. avatar URL, integration webhook) with a value like 'example.com/path' (no protocol), 'ftp://x', or 'http://localhost' while isCloudHosted is true (no TLD).","commonSituations":"Users pasting URLs without https://; self-hosted dev values pointing at localhost while the cloud-hosted flag is on; webhook URLs with custom schemes.","solutions":["Prefix the value with https:// (or http:// for local) before saving.","In self-hosted deployments where localhost is valid, ensure isCloudHosted is false.","Sanitize/normalize URL input client-side using the URL constructor."],"exampleFix":"// before\nuser.avatarUrl = 'example.com/avatar.png';\n\n// after\nuser.avatarUrl = 'https://example.com/avatar.png';","handlingStrategy":"validation","validationCode":"let normalized = value.trim();\nif (!/^https?:\\/\\//i.test(normalized)) normalized = `https://${normalized}`;\ntry { new URL(normalized); } catch { throw new ValidationError('Must be a valid url'); }","typeGuard":"const isHttpUrl = (v: string): v is string => {\n  try { const u = new URL(v); return u.protocol === 'http:' || u.protocol === 'https:'; } catch { return false; }\n};","tryCatchPattern":null,"preventionTips":["Always require the protocol in user-entered URLs.","On self-hosted, confirm isCloudHosted is false if localhost URLs are valid.","Normalize via the URL constructor before persisting."],"tags":["server","models","validator","url","validation"],"backgroundTag":null,"analyzedSha":"935a44d4c003429645a956141b2c4eb695f34b6e","analyzedAt":"2026-08-12T22:40:11.882Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}