{"record":{"id":"ed40f28d4a53ccd6","repo":"toeverything/AFFiNE","slug":"caldav-invalid-url","errorCode":"caldav_invalid_url","errorMessage":"CalDAV URL is invalid.","messagePattern":"CalDAV URL is invalid\\.","errorType":"exception","errorClass":"GraphqlBadRequest","httpStatus":400,"severity":"error","filePath":"packages/backend/server/src/plugins/calendar/providers/caldav.ts","lineNumber":563,"sourceCode":"        maxBytes: DEFAULT_MAX_RESPONSE_BYTES,\n        allowedHeaders: CALDAV_SAFE_FETCH_HEADERS,\n        allowedHosts: this.allowedHosts,\n        allowHttp: this.allowInsecureHttp,\n        allowPrivateTargetOrigin: !this.blockPrivateNetwork,\n      });\n    } catch (error) {\n      const ssrfError = this.toGraphqlSsrfError(error);\n      if (ssrfError) throw ssrfError;\n      throw error;\n    }\n  }\n\n  private async assertAllowedUrl(urlValue: string) {\n    let url: URL;\n    try {\n      url = new URL(urlValue);\n    } catch {\n      throw new GraphqlBadRequest({\n        code: 'caldav_invalid_url',\n        message: 'CalDAV URL is invalid.',\n      });\n    }\n\n    if (\n      url.protocol !== 'https:' &&\n      !(url.protocol === 'http:' && this.allowInsecureHttp)\n    ) {\n      throw new GraphqlBadRequest({\n        code: 'caldav_insecure_url',\n        message: 'CalDAV URL must use https.',\n      });\n    }\n\n    const hostname = url.hostname.toLowerCase();\n    if (\n      this.allowedHosts.length &&","sourceCodeStart":545,"sourceCodeEnd":581,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/b4c8548c09da21b2898443559a5b846f0ccf5dd8/packages/backend/server/src/plugins/calendar/providers/caldav.ts#L545-L581","documentation":"The CalDAV provider's assertAllowedUrl (caldav.ts) parses the user-supplied URL with new URL(); a parse failure throws GraphqlBadRequest with code caldav_invalid_url. This is the first gate before the protocol, host-allowlist, and SSRF checks run.","triggerScenarios":"Saving or linking a CalDAV calendar subscription with a URL that cannot be parsed — missing scheme (example.com/dav), stray whitespace or punctuation, or a bare hostname.","commonSituations":"User omits https:// when entering the CalDAV endpoint; copy-paste includes trailing punctuation or invisible whitespace; client sends just a hostname or a malformed user:pass@host construction.","solutions":["Enter a full absolute URL including the scheme, e.g. https://caldav.example.com/user/.","Trim whitespace and strip stray characters before submitting.","Validate with new URL() on the client before calling the API."],"exampleFix":"// before\nawait caldav.connect('caldav.example.com/user'); // no scheme -> caldav_invalid_url\n\n// after\nconst url = 'caldav.example.com/user'.trim();\nconst normalized = /^https?:\\/\\//.test(url) ? url : `https://${url}`;\nawait caldav.connect(normalized);","handlingStrategy":"validation","validationCode":"function isValidHttpUrl(v: string): boolean {\n  try { new URL(v.trim()); return true; } catch { return false; }\n}\nif (!isValidHttpUrl(caldavUrl)) {\n  // show 'Enter a full URL, e.g. https://...' before calling the API\n}","typeGuard":"function isParsableUrl(v: string): v is `${'http' | 'https'}://${string}` {\n  try { new URL(v); return true; } catch { return false; }\n}","tryCatchPattern":"try { await caldav.connect(url); } catch (e) { if (e.code === 'caldav_invalid_url') showUrlFormatError(); else throw e; }","preventionTips":["Trim pasted input and require the scheme in the UI.","Auto-prefix https:// when the user omits the scheme.","Validate with new URL() client-side before submit."],"tags":["calendar","caldav","url","validation","ssrf"],"backgroundTag":"invalid-url-format","analyzedSha":"b4c8548c09da21b2898443559a5b846f0ccf5dd8","analyzedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-08-23T01:17:44.959Z"}