{"record":{"id":"1e4bf7c6905b507e","repo":"koala73/worldmonitor","slug":"callbackurl-is-not-a-valid-url","errorCode":null,"errorMessage":"callbackUrl is not a valid URL","messagePattern":"callbackUrl is not a valid URL","errorType":"validation","errorClass":"Error","httpStatus":400,"severity":"warning","filePath":"server/worldmonitor/shipping/v2/webhook-shared.ts","lineNumber":121,"sourceCode":"    return (data.Answer ?? [])\n      .filter(answer => answer.type === expectedType && typeof answer.data === 'string')\n      .map(answer => answer.data!);\n  };\n  const records = await Promise.all([resolveRecordType('A'), resolveRecordType('AAAA')]);\n  return records.flat();\n}\n\n/**\n * Validate the current DNS answer before storing a webhook. Delivery makes the\n * same check immediately before send and pins the resulting socket, which\n * keeps this fail-fast check from becoming the only SSRF control.\n */\nexport async function assertCallbackUrlRegistrationSafe(\n  callbackUrl: string,\n  resolveHostname: ResolveHostname = defaultResolveHostname,\n): Promise<void> {\n  const staticError = isBlockedCallbackUrl(callbackUrl);\n  if (staticError) throw new Error(staticError);\n\n  const hostname = new URL(callbackUrl).hostname.toLowerCase();\n  if (isIpLiteral(hostname)) return;\n  let resolvedAddresses: string[];\n  try {\n    resolvedAddresses = await resolveHostname(hostname);\n  } catch (error) {\n    const message = error instanceof Error ? error.message : String(error);\n    throw new Error(`callbackUrl DNS resolution failed: ${message}`);\n  }\n  if (!resolvedAddresses.length) throw new Error('callbackUrl DNS resolution returned no addresses');\n  const blocked = resolvedAddresses.find(isBlockedResolvedAddress);\n  if (blocked) throw new Error('callbackUrl resolves to a private/reserved address');\n}\n\nexport async function generateSecret(): Promise<string> {\n  const bytes = new Uint8Array(32);\n  crypto.getRandomValues(bytes);","sourceCodeStart":103,"sourceCodeEnd":139,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/server/worldmonitor/shipping/v2/webhook-shared.ts#L103-L139","documentation":"First static check inside assertCallbackUrlRegistrationSafe: isBlockedCallbackUrl runs new URL(rawUrl), and a parse throw returns 'callbackUrl is not a valid URL', which line 121 rethrows and registerWebhook surfaces as a 400 ValidationError on callbackUrl. It catches structurally unparseable URLs before any protocol, hostname, or DNS check runs.","triggerScenarios":"callbackUrl lacking a scheme ('example.com/cb'), containing spaces or illegal characters, being a relative path ('/callback'), or interpolating 'null'/'undefined' into the string. Thrown during RegisterWebhook (and re-checked at delivery) entirely offline — no DNS query happens yet.","commonSituations":"Building the URL by concatenating a host without https://; template strings with undefined variables; copy-paste introducing whitespace; config values read as empty or undefined and stringified.","solutions":["Always build the callback URL with an explicit scheme: `https://${host}${path}`","Validate with new URL(url) client-side before calling RegisterWebhook","Sanitize config-sourced URLs (trim, reject empty/undefined) at startup or form-submit time"],"exampleFix":"// before\nregisterWebhook(ctx, { callbackUrl: `${HOST}/cb`, chokepointIds }); // HOST='api.example.com' -> no scheme\n// after\nregisterWebhook(ctx, { callbackUrl: `https://${HOST}/cb`, chokepointIds });","handlingStrategy":"validation","validationCode":"try { new URL(callbackUrl); } catch { throw new RangeError('callbackUrl is not a valid URL'); }","typeGuard":"const isParsableUrl = (v: unknown): v is string => { if (typeof v !== 'string') return false; try { new URL(v); return true; } catch { return false; } };","tryCatchPattern":"catch (e) { if (e?.details?.[0]?.description === 'callbackUrl is not a valid URL') { fix URL construction (scheme, whitespace) and re-submit } else throw e; }","preventionTips":["Always build URLs as `https://${host}${path}`","new URL() the value client-side before every register call","Validate config-sourced URLs at startup"],"tags":["validation","url","webhooks","ssrf","http-400","callback-url"],"backgroundTag":"invalid-url-format","analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","contentChangedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-09-08T15:18:49.778Z"}