fix(server): caldav compatibility (#14930)

fix #14411
fix #14909 

Some CalDAV servers do not implement standard responses; add
compatibility for these servers.


#### PR Dependency Tree


* **PR #14930** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved CalDAV discovery error handling to gracefully fall back when
the server returns certain error statuses.

* **New Features**
* CalDAV account linking now returns the number of discovered calendars
associated with the account.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-05-09 02:40:47 +08:00
committed by GitHub
parent bcbde16c04
commit fcc45a3f44
3 changed files with 29 additions and 3 deletions
@@ -147,6 +147,7 @@ const calendarQueryResponse = `<?xml version="1.0" encoding="UTF-8"?>
</multistatus>`;
const createCalDAVServer = async (options?: {
discoveryGetStatus?: number;
syncCollectionStatus?: number;
}) => {
const requests: Array<{ method: string; url: string; body: string }> = [];
@@ -169,6 +170,16 @@ const createCalDAVServer = async (options?: {
}
if (req.url === '/.well-known/caldav') {
if (options?.discoveryGetStatus) {
res.writeHead(options.discoveryGetStatus, {
'Content-Type': 'application/xml',
});
res.end(`<?xml version="1.0" encoding="utf-8"?>
<d:error xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns">
<s:exception>Sabre\\DAV\\Exception\\NotImplemented</s:exception>
</d:error>`);
return;
}
res.writeHead(302, { Location: '/caldav/' });
res.end();
return;
@@ -275,7 +286,7 @@ const createCalendarModule = async (caldavConfig: Record<string, unknown>) => {
};
test('linkCalDAVAccount discovers calendars and parses events', async t => {
const server = await createCalDAVServer();
const server = await createCalDAVServer({ discoveryGetStatus: 501 });
t.teardown(() => server.server.close());
const module = await createCalendarModule({
@@ -311,6 +322,18 @@ test('linkCalDAVAccount discovers calendars and parses events', async t => {
account.id
);
t.is(subscriptions.length, 1);
t.is(account.calendarsCount, 1);
t.true(
server.requests.some(
request =>
request.method === 'GET' && request.url === '/.well-known/caldav'
)
);
t.true(
server.requests.some(
request => request.method === 'PROPFIND' && request.url === '/caldav/'
)
);
const events = await models.calendarEvent.listBySubscriptionsInRange(
[subscriptions[0].id],
@@ -1014,7 +1014,7 @@ export class CalDAVProvider extends CalendarProvider {
if (response.ok) {
return response.url;
}
if ([400, 404, 405].includes(response.status)) {
if ([400, 404, 405, 501].includes(response.status)) {
return serverUrl;
}
const text = await response.text();
@@ -272,7 +272,10 @@ export class CalendarService {
throw error;
}
return account;
const calendars = await this.models.calendarSubscription.listByAccount(
account.id
);
return { ...account, calendarsCount: calendars.length };
}
async syncAccountCalendars(accountId: string) {