{"record":{"id":"21428f1b8409bb2d","repo":"calcom/cal.diy","slug":"listed-cals-and-urls-mismatch-listedcals-length","errorCode":null,"errorMessage":"Listed cals and URLs mismatch: ${listedCals.length} vs. ${urls.length}","messagePattern":"Listed cals and URLs mismatch: (.+?) vs\\. (.+?)","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"apps/api/v2/src/platform/calendars/services/ics-feed.service.ts","lineNumber":56,"sourceCode":"      userId: userId,\n      teamId: null,\n      appId: ICS_CALENDAR,\n      invalid: false,\n      delegationCredentialId: null,\n      encryptedKey: null,\n    };\n\n    try {\n      const dav = BuildIcsFeedCalendarService({\n        id: 0,\n        ...data,\n        user: { email: userEmail },\n      });\n\n      const listedCals = await dav.listCalendars();\n\n      if (listedCals.length !== urls.length) {\n        throw new BadRequestException(\n          `Listed cals and URLs mismatch: ${listedCals.length} vs. ${urls.length}`\n        );\n      }\n\n      const credential = await this.credentialRepository.upsertUserAppCredential(\n        ICS_CALENDAR_TYPE,\n        data.key,\n        userId\n      );\n\n      await this.calendarsCacheService.deleteConnectedAndDestinationCalendarsCache(userId);\n\n      return {\n        status: SUCCESS_STATUS,\n        data: {\n          id: credential.id,\n          type: credential.type,\n          userId: credential.userId,","sourceCodeStart":38,"sourceCodeEnd":74,"githubUrl":"https://github.com/calcom/cal.diy/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/api/v2/src/platform/calendars/services/ics-feed.service.ts#L38-L74","documentation":"Raised inside IcsFeedService.save after BuildIcsFeedCalendarService(...).listCalendars() returns. The service is given N ICS feed URLs (urls.length) and expects the DAV/ICS adapter to enumerate exactly that many calendars; a count mismatch means one or more feeds could not be resolved (unreachable, unparseable, or merged), so the whole credential-upsert is aborted with BadRequestException. The error message echoes the two counts so you can see which side is short.","triggerScenarios":"POSTing an array of ICS URLs where at least one URL 404s, returns non-ICS content, requires auth that wasn't supplied, or is a duplicate that the DAV library collapses into one entry; a URL that is reachable but contains zero VEVENT calendars; a network/timeout during listCalendars that returns a partial list without throwing.","commonSituations":"Public Google/Outlook ICS links that require an authenticated session, or that rate-limit the server IP; feed URLs behind a CDN that serve an HTML login page instead of .ics; copy-paste of the calendar's HTML page URL instead of the 'secret address in iCal format'; feed temporarily down at save time.","solutions":["Fetch each URL in urls[] with curl -I and confirm a 200 with content-type text/calendar before calling /v2/calendars/ics/save.","Use the 'private'/'secret address in iCal format' link for every entry (Google: calendar Settings -> Integrate calendar -> Secret address).","Reduce urls[] to a single URL to isolate which feed is shorting the count, then add the rest back one by one.","If all feeds are valid but still mismatch, retry once; a transient network error during listCalendars can shrink listedCals without throwing."],"exampleFix":"// before\nawait api.post('/v2/calendars/ics/save', { urls: [gcalPublicPageUrl, privateIcsUrl] });\n\n// after\nawait api.post('/v2/calendars/ics/save', { urls: [privateIcsUrl] });","handlingStrategy":"validation","validationCode":"async function preflightIcsUrls(urls: string[]) {\n  for (const u of urls) {\n    const res = await fetch(u, { method: 'GET' });\n    const ct = res.headers.get('content-type') || '';\n    if (!res.ok || !/text\\/calendar|ics/.test(ct)) {\n      throw new Error(`Bad ICS feed ${u}: status=${res.status} content-type=${ct}`);\n    }\n  }\n}\nawait preflightIcsUrls(urls);","typeGuard":"function isIcsUrlList(urls: unknown): urls is string[] {\n  return Array.isArray(urls) && urls.length > 0 && urls.every(u => typeof u === 'string' && /^https?:\\/\\//.test(u));\n}","tryCatchPattern":"try {\n  await icsFeedService.save(userId, userEmail, urls, readonly);\n} catch (e) {\n  if (e instanceof BadRequestException && e.message.startsWith('Listed cals and URLs mismatch')) {\n    // isolate the failing URL by retrying one at a time\n  } else throw e;\n}","preventionTips":["Always use the provider's 'secret address in iCal format' link.","Preflight each URL with a HEAD/GET to confirm 200 + text/calendar before saving.","Save one URL at a time when debugging a mismatch."],"tags":["ics","calendar","validation","api-v2"],"backgroundTag":null,"analyzedSha":"176037d0afbe572f870a3c702985e7cd83fe6c0c","analyzedAt":"2026-08-12T19:12:41.464Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}