From 18af33603aecb3d620010340a2867b0ae7a54fc6 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 09:19:06 +0000 Subject: [PATCH 1/2] feat: Pass abort signal to fetch in email validation provider - Update IEmailValidationProviderService interface to accept optional signal parameter - Pass signal through to fetch call in ZeroBounceEmailValidationProviderService - Wire signal from AbortController in EmailValidationService to provider This prevents network requests from continuing after timeout, avoiding resource leaks. Addresses review comment from Udit-takkar on PR #24196 Co-Authored-By: hariom@cal.com --- .../emailValidation/lib/service/EmailValidationService.ts | 2 +- .../lib/service/IEmailValidationProviderService.interface.ts | 2 +- .../lib/service/ZeroBounceEmailValidationProviderService.ts | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/features/emailValidation/lib/service/EmailValidationService.ts b/packages/features/emailValidation/lib/service/EmailValidationService.ts index d92d9f31630283..d86739999f6559 100644 --- a/packages/features/emailValidation/lib/service/EmailValidationService.ts +++ b/packages/features/emailValidation/lib/service/EmailValidationService.ts @@ -230,7 +230,7 @@ export class EmailValidationService implements IEmailValidationService { try { // Race between provider validation and timeout const result = await Promise.race([ - this.deps.emailValidationProvider.validateEmail(request), + this.deps.emailValidationProvider.validateEmail(request, controller.signal), new Promise((_, reject) => { controller.signal.addEventListener("abort", () => { reject(new Error(`Email validation provider timeout after ${this.providerTimeout}ms`)); diff --git a/packages/features/emailValidation/lib/service/IEmailValidationProviderService.interface.ts b/packages/features/emailValidation/lib/service/IEmailValidationProviderService.interface.ts index eaaeeea1a82cb7..4f646086611b34 100644 --- a/packages/features/emailValidation/lib/service/IEmailValidationProviderService.interface.ts +++ b/packages/features/emailValidation/lib/service/IEmailValidationProviderService.interface.ts @@ -12,5 +12,5 @@ import type { EmailValidationRequest, EmailValidationResult } from "../dto/types * have universal meanings, and blocking decisions are centralized in EmailValidationService. */ export interface IEmailValidationProviderService { - validateEmail(request: EmailValidationRequest): Promise; + validateEmail(request: EmailValidationRequest, signal?: AbortSignal): Promise; } diff --git a/packages/features/emailValidation/lib/service/ZeroBounceEmailValidationProviderService.ts b/packages/features/emailValidation/lib/service/ZeroBounceEmailValidationProviderService.ts index ee061d49b509aa..4c30e1be234bd9 100644 --- a/packages/features/emailValidation/lib/service/ZeroBounceEmailValidationProviderService.ts +++ b/packages/features/emailValidation/lib/service/ZeroBounceEmailValidationProviderService.ts @@ -23,7 +23,7 @@ export class ZeroBounceEmailValidationProviderService implements IEmailValidatio this.apiKey = process.env.ZEROBOUNCE_API_KEY || ""; } - async validateEmail(request: EmailValidationRequest): Promise { + async validateEmail(request: EmailValidationRequest, signal?: AbortSignal): Promise { const { email, ipAddress } = request; if (!this.apiKey) { @@ -46,6 +46,7 @@ export class ZeroBounceEmailValidationProviderService implements IEmailValidatio Accept: "application/json", "User-Agent": "Cal.com", }, + signal, }); if (!response.ok) { From 240ff4da5a247370ddbe23c0c11858b3b161ce28 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 11:05:59 +0000 Subject: [PATCH 2/2] refactor: Rename signal to abortSignal for clarity - Rename parameter from 'signal' to 'abortSignal' to clearly convey it's for aborting fetch requests - Update interface, implementation, and call site - Update tests to match new object parameter syntax Co-Authored-By: hariom@cal.com --- .../lib/service/EmailValidationService.ts | 2 +- ...mailValidationProviderService.interface.ts | 5 ++++- ...unceEmailValidationProviderService.test.ts | 20 +++++++++++-------- ...eroBounceEmailValidationProviderService.ts | 10 ++++++++-- 4 files changed, 25 insertions(+), 12 deletions(-) diff --git a/packages/features/emailValidation/lib/service/EmailValidationService.ts b/packages/features/emailValidation/lib/service/EmailValidationService.ts index d86739999f6559..22651cb2b77ce1 100644 --- a/packages/features/emailValidation/lib/service/EmailValidationService.ts +++ b/packages/features/emailValidation/lib/service/EmailValidationService.ts @@ -230,7 +230,7 @@ export class EmailValidationService implements IEmailValidationService { try { // Race between provider validation and timeout const result = await Promise.race([ - this.deps.emailValidationProvider.validateEmail(request, controller.signal), + this.deps.emailValidationProvider.validateEmail({ request, abortSignal: controller.signal }), new Promise((_, reject) => { controller.signal.addEventListener("abort", () => { reject(new Error(`Email validation provider timeout after ${this.providerTimeout}ms`)); diff --git a/packages/features/emailValidation/lib/service/IEmailValidationProviderService.interface.ts b/packages/features/emailValidation/lib/service/IEmailValidationProviderService.interface.ts index 4f646086611b34..aca4fae7299700 100644 --- a/packages/features/emailValidation/lib/service/IEmailValidationProviderService.interface.ts +++ b/packages/features/emailValidation/lib/service/IEmailValidationProviderService.interface.ts @@ -12,5 +12,8 @@ import type { EmailValidationRequest, EmailValidationResult } from "../dto/types * have universal meanings, and blocking decisions are centralized in EmailValidationService. */ export interface IEmailValidationProviderService { - validateEmail(request: EmailValidationRequest, signal?: AbortSignal): Promise; + validateEmail(params: { + request: EmailValidationRequest; + abortSignal?: AbortSignal; + }): Promise; } diff --git a/packages/features/emailValidation/lib/service/ZeroBounceEmailValidationProviderService.test.ts b/packages/features/emailValidation/lib/service/ZeroBounceEmailValidationProviderService.test.ts index f853b74778599d..e6de80bf22f043 100644 --- a/packages/features/emailValidation/lib/service/ZeroBounceEmailValidationProviderService.test.ts +++ b/packages/features/emailValidation/lib/service/ZeroBounceEmailValidationProviderService.test.ts @@ -39,7 +39,7 @@ describe("ZeroBounceEmailValidationProviderService", () => { mockFetch.mockResolvedValue(mockResponse); - const result = await service.validateEmail({ email: "test@example.com" }); + const result = await service.validateEmail({ request: { email: "test@example.com" } }); expect(result).toEqual({ status: "valid", @@ -52,9 +52,9 @@ describe("ZeroBounceEmailValidationProviderService", () => { delete process.env.ZEROBOUNCE_API_KEY; const serviceWithoutKey = new ZeroBounceEmailValidationProviderService(); - await expect(serviceWithoutKey.validateEmail({ email: "test@example.com" })).rejects.toThrow( - "ZeroBounce API key not configured" - ); + await expect( + serviceWithoutKey.validateEmail({ request: { email: "test@example.com" } }) + ).rejects.toThrow("ZeroBounce API key not configured"); expect(mockFetch).not.toHaveBeenCalled(); }); @@ -62,7 +62,9 @@ describe("ZeroBounceEmailValidationProviderService", () => { it("should propagate error when ZeroBounce API is unreachable or returns network error", async () => { mockFetch.mockRejectedValue(new Error("Network error")); - await expect(service.validateEmail({ email: "test@example.com" })).rejects.toThrow("Network error"); + await expect(service.validateEmail({ request: { email: "test@example.com" } })).rejects.toThrow( + "Network error" + ); }); it("should reject validation request when ZeroBounce API returns server error", async () => { @@ -74,7 +76,7 @@ describe("ZeroBounceEmailValidationProviderService", () => { mockFetch.mockResolvedValue(mockResponse); - await expect(service.validateEmail({ email: "test@example.com" })).rejects.toThrow( + await expect(service.validateEmail({ request: { email: "test@example.com" } })).rejects.toThrow( "ZeroBounce API returned 500: Internal Server Error" ); }); @@ -92,8 +94,10 @@ describe("ZeroBounceEmailValidationProviderService", () => { mockFetch.mockResolvedValue(mockResponse); await service.validateEmail({ - email: "test@example.com", - ipAddress: "192.168.1.1", + request: { + email: "test@example.com", + ipAddress: "192.168.1.1", + }, }); expect(mockFetch).toHaveBeenCalledWith( diff --git a/packages/features/emailValidation/lib/service/ZeroBounceEmailValidationProviderService.ts b/packages/features/emailValidation/lib/service/ZeroBounceEmailValidationProviderService.ts index 4c30e1be234bd9..ffb463740cf649 100644 --- a/packages/features/emailValidation/lib/service/ZeroBounceEmailValidationProviderService.ts +++ b/packages/features/emailValidation/lib/service/ZeroBounceEmailValidationProviderService.ts @@ -23,7 +23,13 @@ export class ZeroBounceEmailValidationProviderService implements IEmailValidatio this.apiKey = process.env.ZEROBOUNCE_API_KEY || ""; } - async validateEmail(request: EmailValidationRequest, signal?: AbortSignal): Promise { + async validateEmail({ + request, + abortSignal, + }: { + request: EmailValidationRequest; + abortSignal?: AbortSignal; + }): Promise { const { email, ipAddress } = request; if (!this.apiKey) { @@ -46,7 +52,7 @@ export class ZeroBounceEmailValidationProviderService implements IEmailValidatio Accept: "application/json", "User-Agent": "Cal.com", }, - signal, + signal: abortSignal, }); if (!response.ok) {