diff --git a/packages/common/src/errors/http/handlers/node-fetch-http-error-handler.ts b/packages/common/src/errors/http/handlers/node-fetch-http-error-handler.ts new file mode 100644 index 000000000..facfc5a91 --- /dev/null +++ b/packages/common/src/errors/http/handlers/node-fetch-http-error-handler.ts @@ -0,0 +1,44 @@ +import { FetchError } from "node-fetch"; + +import { logger } from "@homarr/log"; + +import { RequestError } from "../request-error"; +import type { AnyRequestError } from "../request-error"; +import type { ResponseError } from "../response-error"; +import { matchErrorCode } from "./fetch-http-error-handler"; +import { HttpErrorHandler } from "./http-error-handler"; + +/** + * node-fetch was a defacto standard to use fetch in nodejs. + * + * It is for example used within the cross-fetch package which is used in tsdav. + */ +export class NodeFetchHttpErrorHandler extends HttpErrorHandler { + constructor(private type = "node-fetch") { + super(); + } + + handleRequestError(error: unknown): AnyRequestError | undefined { + if (!(error instanceof FetchError)) return undefined; + if (error.code === undefined) return undefined; + + logger.debug(`Received ${this.type} request error`, { + code: error.code, + message: error.message, + }); + + const requestErrorInput = matchErrorCode(error.code); + if (!requestErrorInput) return undefined; + + return new RequestError(requestErrorInput, { + cause: error, + }); + } + + /** + * Response errors do not exist for fetch as it does not throw errors for non successful responses. + */ + handleResponseError(_: unknown): ResponseError | undefined { + return undefined; + } +} diff --git a/packages/common/src/errors/http/handlers/tsdav-http-error-handler.ts b/packages/common/src/errors/http/handlers/tsdav-http-error-handler.ts index 9f9c363c5..76817ce0f 100644 --- a/packages/common/src/errors/http/handlers/tsdav-http-error-handler.ts +++ b/packages/common/src/errors/http/handlers/tsdav-http-error-handler.ts @@ -2,12 +2,12 @@ import { logger } from "@homarr/log"; import type { AnyRequestError } from "../request-error"; import { ResponseError } from "../response-error"; -import { FetchHttpErrorHandler } from "./fetch-http-error-handler"; import { HttpErrorHandler } from "./http-error-handler"; +import { NodeFetchHttpErrorHandler } from "./node-fetch-http-error-handler"; export class TsdavHttpErrorHandler extends HttpErrorHandler { handleRequestError(error: unknown): AnyRequestError | undefined { - return new FetchHttpErrorHandler("tsdav").handleRequestError(error); + return new NodeFetchHttpErrorHandler("tsdav").handleRequestError(error); } handleResponseError(error: unknown): ResponseError | undefined { @@ -16,7 +16,7 @@ export class TsdavHttpErrorHandler extends HttpErrorHandler { // https://github.com/natelindev/tsdav/blob/bf33f04b1884694d685ee6f2b43fe9354b12d167/src/account.ts#L86 if (error.message !== "Invalid credentials") return undefined; - logger.debug("Received Tsdav response error", { + logger.debug("Received tsdav response error", { status: 401, }); diff --git a/packages/integrations/src/nextcloud/nextcloud.integration.ts b/packages/integrations/src/nextcloud/nextcloud.integration.ts index 4a9e5d3f5..a855ce297 100644 --- a/packages/integrations/src/nextcloud/nextcloud.integration.ts +++ b/packages/integrations/src/nextcloud/nextcloud.integration.ts @@ -1,8 +1,9 @@ +import type { Agent } from "https"; +import type { RequestInit as NodeFetchRequestInit } from "node-fetch"; import * as ical from "node-ical"; import { DAVClient } from "tsdav"; -import type { Dispatcher, RequestInit as UndiciFetchRequestInit } from "undici"; -import { createCertificateAgentAsync } from "@homarr/certificates/server"; +import { createHttpsAgentAsync } from "@homarr/certificates/server"; import { logger } from "@homarr/log"; import { HandleIntegrationErrors } from "../base/errors/decorator"; @@ -16,7 +17,7 @@ import type { CalendarEvent } from "../interfaces/calendar/calendar-types"; @HandleIntegrationErrors([integrationTsdavHttpErrorHandler]) export class NextcloudIntegration extends Integration implements ICalendarIntegration { protected async testingAsync(input: IntegrationTestingInput): Promise { - const client = await this.createCalendarClientAsync(input.dispatcher); + const client = await this.createCalendarClientAsync(await createHttpsAgentAsync(input.options)); await client.login(); return { success: true }; @@ -80,7 +81,7 @@ export class NextcloudIntegration extends Integration implements ICalendarIntegr }); } - private async createCalendarClientAsync(dispatcher?: Dispatcher) { + private async createCalendarClientAsync(agent?: Agent) { return new DAVClient({ serverUrl: this.integration.url, credentials: { @@ -90,9 +91,10 @@ export class NextcloudIntegration extends Integration implements ICalendarIntegr authMethod: "Basic", defaultAccountType: "caldav", fetchOptions: { - // We can use the undici options as the global fetch is used instead of the polyfilled. - dispatcher: dispatcher ?? (await createCertificateAgentAsync()), - } satisfies UndiciFetchRequestInit as RequestInit, + // tsdav is using cross-fetch which uses node-fetch for nodejs environments. + // There is an agent property that is the same type as the http(s) agents of nodejs + agent: agent ?? (await createHttpsAgentAsync()), + } satisfies NodeFetchRequestInit as RequestInit, }); } }