Convert non-UTC UNTIL to UTC for CalDAV compatibility#428
Convert non-UTC UNTIL to UTC for CalDAV compatibility#428jens-maus merged 4 commits intojens-maus:masterfrom
Conversation
RFC 5545 requires UNTIL to be UTC when DTSTART has a timezone, but some CalDAV servers send invalid local time values. This causes rrule-temporal to throw "UNTIL must be specified as UTC" errors. Solution: - Detect non-UTC UNTIL (missing 'Z' suffix) - Parse in DTSTART timezone and convert to UTC - Graceful fallback with warning on conversion failure Fixes compatibility with buggy CalDAV servers while maintaining RFC 5545 compliance. No breaking changes.
📝 WalkthroughWalkthroughDetects RRULE UNTIL values that are specified in local (non-UTC) time when DTSTART has a TZID, converts those UNTIL timestamps to UTC using DTSTART's timezone, updates the RRULE string in-place, and protects the conversion with try/catch to avoid parse failures. Changes
Sequence Diagram(s)sequenceDiagram
participant Parser as ICS Parser
participant RRULE as RRULE Parser
participant TZ as tzUtil
Parser->>RRULE: parse RRULE string (contains UNTIL)
RRULE->>Parser: report UNTIL value and DTSTART TZID
alt DTSTART has TZID and UNTIL is non-UTC
Parser->>TZ: resolve TZID and convert local UNTIL -> UTC
TZ-->>Parser: UTC timestamp (YYYYMMDDTHHMMSSZ)
Parser->>RRULE: replace UNTIL in RRULE string with UTC value
else no conversion needed
Parser->>RRULE: continue parsing as-is
end
RRULE->>Parser: produce rrule object (options.until in UTC if converted)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
📜 Recent review detailsConfiguration used: defaults Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🧰 Additional context used🧬 Code graph analysis (1)ical.js (1)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
🔇 Additional comments (1)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (2)
ical.js (2)
708-708: Consider extracting regex patterns as constants to improve maintainability.The UNTIL matching regex
/UNTIL=(\d{8}T\d{6}Z?)/(line 708) and replacement regex/UNTIL=\d{8}T\d{6}/(line 731) are similar but serve different purposes. While this works correctly, extracting them as named constants would improve maintainability and clarify intent:const UNTIL_DATETIME_PATTERN = /UNTIL=(\d{8}T\d{6}Z?)/; const UNTIL_DATETIME_NO_UTC_PATTERN = /UNTIL=\d{8}T\d{6}/;♻️ Optional refactor to use named constants
At the top of the file (near other constants):
+// Regex patterns for RRULE UNTIL normalization +const UNTIL_DATETIME_PATTERN = /UNTIL=(\d{8}T\d{6}Z?)/; +const UNTIL_DATETIME_NO_UTC_PATTERN = /UNTIL=\d{8}T\d{6}/;Then update the usage:
- const untilMatch = rruleOnly.match(/UNTIL=(\d{8}T\d{6}Z?)/); + const untilMatch = rruleOnly.match(UNTIL_DATETIME_PATTERN); if (untilMatch && !untilMatch[1].endsWith('Z')) { // E.g., "20241231T100000" const untilLocal = untilMatch[1]; try { const tzInfo = tzUtil.resolveTZID(curr.start.tz); let untilDate; // Parse UNTIL in the same timezone as DTSTART if (tzInfo.iana && tzUtil.isValidIana(tzInfo.iana)) { untilDate = tzUtil.parseDateTimeInZone(untilLocal, tzInfo.iana); } else if (Number.isFinite(tzInfo.offsetMinutes)) { untilDate = tzUtil.parseWithOffset(untilLocal, tzInfo.offset); } if (untilDate && typeof untilDate.toISOString === 'function') { // Convert to UTC format: 20241231T090000Z const untilUtc = untilDate.toISOString() .replaceAll(/[-:]/g, '') .replace(/\.\d{3}/, ''); // Replace in RRULE string - rruleOnly = rruleOnly.replace(/UNTIL=\d{8}T\d{6}/, `UNTIL=${untilUtc}`); + rruleOnly = rruleOnly.replace(UNTIL_DATETIME_NO_UTC_PATTERN, `UNTIL=${untilUtc}`); } } catch (error) { // If conversion fails, log warning but don't break parsing console.warn(`[node-ical] Failed to convert UNTIL to UTC: ${error.message}`); } }Also applies to: 731-731
704-738:tzUtil.isValidIanaexists and is properly exported—remove the verification concern. Consider enhancing error context as an optional improvement.The UNTIL normalization logic is well-structured with proper guards, timezone resolution, and error handling. One item to address:
The error message on line 735 could optionally include more context for debugging. Rather than:
console.warn(`[node-ical] Failed to convert UNTIL to UTC: ${error.message}`);Consider:
console.warn(`[node-ical] Failed to convert UNTIL to UTC for TZID="${curr.start.tz}", UNTIL="${untilLocal}": ${error.message}`);(Note:
tzUtil.isValidIanais defined in tz-utils.js:473 and properly exported; no verification needed.)
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
ical.jstest/non-utc-until-ny.icstest/non-utc-until.icstest/non-utc-until.test.js
🧰 Additional context used
🧬 Code graph analysis (1)
ical.js (1)
test/non-utc-until.test.js (1)
untilUtc(52-52)
🔇 Additional comments (11)
ical.js (2)
700-702: LGTM: Variable declaration change enables UNTIL normalization.Changing
rruleOnlyfromconsttoletis necessary to allow reassignment when normalizing non-UTC UNTIL values on line 731.
1-13: No action needed. The library versions @js-temporal/polyfill 0.5.1 and rrule-temporal 1.3.0 are at their latest releases, and no security vulnerabilities or compatibility issues exist for either package.test/non-utc-until.ics (1)
1-12: LGTM: Well-structured test fixture for Europe/Berlin timezone.The iCalendar test data is correctly formatted and provides a clear test case for non-UTC UNTIL normalization with the Europe/Berlin timezone.
test/non-utc-until-ny.ics (1)
1-12: LGTM: Good test fixture covering America/New_York timezone.The iCalendar test data correctly represents a daily event with non-UTC UNTIL in America/New_York timezone. The date range (March 1-15, 2024) is particularly valuable as it crosses the DST transition on March 10, 2024.
test/non-utc-until.test.js (7)
12-36: LGTM: Correct UTC conversion test for Europe/Berlin.The test correctly verifies that
20241231T100000in Europe/Berlin (CET, UTC+1 in winter) converts to2024-12-31T09:00:00.000Z. The math is accurate: 10:00 CET - 1 hour offset = 09:00 UTC.
79-101: LGTM: UTC preservation test ensures no double conversion.This test correctly verifies that UNTIL values already in UTC format (ending with 'Z') are not modified by the normalization logic.
104-124: LGTM: Floating time test ensures normalization is skipped without TZID.This test correctly verifies that events without a TZID in DTSTART bypass the UNTIL conversion logic, as expected by the conditional guard on line 707 of ical.js.
155-173: LGTM: Graceful error handling for invalid timezones.This test verifies that parsing doesn't throw even when timezone conversion fails, which aligns with the try-catch error handling in ical.js lines 733-736.
175-199: LGTM: DATE-only UNTIL test confirms correct handling.This test verifies that DATE-only UNTIL values (format:
YYYYMMDDwithout time) are not modified by the normalization logic. This is correct behavior since the regex/UNTIL=(\d{8}T\d{6}Z?)/on ical.js line 708 requires a 'T' separator, so it won't match DATE-only values.
59-76: Verify DST status for America/New_York on March 15, 2024.The test expects
20240315T140000in America/New_York to convert to2024-03-15T18:00:00.000Z. Daylight Saving Time in America/New_York started on March 10, 2024, so March 15, 2024 is in EDT (UTC-4). The conversion is correct: 14:00 EDT + 4 hours = 18:00 UTC.
127-153: The DST transition date and UTC conversion in the test are accurate. Europe/Berlin switched from CET to CEST on 31 March 2024, and the expected conversion (April 1, 2024 10:00 CEST = 08:00 UTC) is correct. No changes needed.
Move regex constants from global scope to local scope near their usage for better locality of reference and code clarity.
Add TZID and UNTIL value to error message for easier debugging when timezone conversion fails.
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (1)
ical.js (1)
704-742: Well-implemented UNTIL normalization with proper fallback handling.The logic correctly:
- Only processes UNTIL when DTSTART has a timezone
- Skips already-UTC values (ending with 'Z')
- Uses the DTSTART timezone to interpret the local UNTIL value
- Falls back gracefully when timezone conversion fails
One minor observation:
UNTIL_DATETIME_NO_UTC_PATTERNon line 710 would technically match the prefix of a UTC timestamp (e.g., matchingUNTIL=20241231T100000withinUNTIL=20241231T100000Z). This doesn't cause a bug because line 713 guards against UTC values, but adding a negative lookahead could make the intent clearer:/UNTIL=\d{8}T\d{6}(?!Z)/This is optional since the current code works correctly.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
ical.js
🧰 Additional context used
🧬 Code graph analysis (1)
ical.js (1)
test/non-utc-until.test.js (1)
untilUtc(52-52)
🔇 Additional comments (2)
ical.js (2)
700-702: LGTM!Changing from
consttoletis appropriate here since the RRULE string may be modified during UNTIL normalization.
722-722: No action needed. TheisValidIanafunction is properly defined intz-utils.js(line 473) and correctly exported (line 658). It is imported intoical.js(line 13) and used correctly at line 722.
matching non-UTC UNTIL values. Makes intent clearer and pattern more self-documenting.
|
@jens-maus PR is ready to merge from my side 🙂 |
Problem
Some CalDAV servers send
RRULEwithUNTILvalues that aren't in UTC format, violating RFC 5545. After updating to node-ical 0.23.0 (which switched from rrule.js to rrule-temporal), these calendars started failing:This worked before because rrule.js was more lenient, but rrule-temporal correctly enforces RFC 5545.
Solution
While rrule-temporal is right to enforce the spec, we should handle this in the parser to maintain backwards compatibility with buggy CalDAV servers. Parse
UNTILin the same timezone asDTSTARTand convert to UTC before passing to rrule-temporal.The fix belongs in node-ical because:
Changes
ical.js: Added UNTIL normalization in RRULE processingSummary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.