From 2df7c0041a578998cccfde84bf689b72bdf766ac Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Mon, 4 May 2026 12:58:25 +0545 Subject: [PATCH 1/2] fix(OUT-3680): read webhook body before sleep to avoid ECONNRESET The Assembly and Dropbox webhook handlers slept 800ms, authenticated, and queried the DB before consuming the request body. With the upstream tunnel holding the socket open, the body stream was getting aborted (ECONNRESET) by the time `req.json()` ran. Read the body first, then sleep and process. Also tighten Assembly webhook validation: switch to `webhookEvent.data.object` (authoritative source from Copilot) and drop the unused top-level `object` field on `AssemblyWebhookSchema`. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../webhook/assembly/api/webhook.controller.ts | 8 ++++++-- src/features/webhook/assembly/lib/webhook.service.ts | 12 +++++++----- src/features/webhook/assembly/utils/types.ts | 1 - .../webhook/dropbox/api/webhook.controller.ts | 4 ++-- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/features/webhook/assembly/api/webhook.controller.ts b/src/features/webhook/assembly/api/webhook.controller.ts index 09c0c19..7c07a16 100644 --- a/src/features/webhook/assembly/api/webhook.controller.ts +++ b/src/features/webhook/assembly/api/webhook.controller.ts @@ -11,9 +11,13 @@ import logger from '@/lib/logger' import { sleep } from '@/utils/sleep' export const handleWebhookEvent = async (req: NextRequest) => { - await sleep(800) // prevent ping-pong case of webhooks + // Read the body before any awaits so the upstream connection isn't torn down + // (sleep + auth + DB lookup) while we still have an unread request stream. + const rawBody = await req.json() const token = req.nextUrl.searchParams.get('token') + await sleep(800) // prevent ping-pong case of webhooks + const user = await User.authenticate(token) const dropboxConnectionService = new DropboxConnectionsService(user) @@ -32,7 +36,7 @@ export const handleWebhookEvent = async (req: NextRequest) => { accountId: connection.accountId, rootNamespaceId: connection.rootNamespaceId, }) - const webhookEvent = await assemblyWebhookService.parseWebhook(req) + const webhookEvent = assemblyWebhookService.parseWebhook(rawBody) logger.info(`Event triggered. ${JSON.stringify(webhookEvent)}`) const eventType = assemblyWebhookService.validateHandleableEvent(webhookEvent) diff --git a/src/features/webhook/assembly/lib/webhook.service.ts b/src/features/webhook/assembly/lib/webhook.service.ts index 0eb4fbc..0cdd491 100644 --- a/src/features/webhook/assembly/lib/webhook.service.ts +++ b/src/features/webhook/assembly/lib/webhook.service.ts @@ -1,5 +1,4 @@ import { and, eq } from 'drizzle-orm' -import type { NextRequest } from 'next/server' import z from 'zod' import { ObjectType, type ObjectTypeValue } from '@/db/constants' import { channelSync } from '@/db/schema/channelSync.schema' @@ -23,6 +22,8 @@ import { updateAssemblyFileInDropbox, } from '@/trigger/processFileSync' +const SYNCABLE_OBJECT_TYPES: readonly string[] = [ObjectType.FILE, ObjectType.FOLDER] + export class AssemblyWebhookService extends AuthenticatedDropboxService { readonly mapFilesService: MapFilesService constructor(user: User, connectionToken: DropboxConnectionTokens) { @@ -30,8 +31,8 @@ export class AssemblyWebhookService extends AuthenticatedDropboxService { this.mapFilesService = new MapFilesService(user, connectionToken) } - async parseWebhook(req: NextRequest): Promise { - const webhookEvent = AssemblyWebhookSchema.safeParse(await req.json()) + parseWebhook(body: unknown): AssemblyWebhookEvent { + const webhookEvent = AssemblyWebhookSchema.safeParse(body) logger.info('AssemblyWebhookService#parseWebhook :: Parsed webhook event', webhookEvent) if (!webhookEvent.success) { throw new APIError('Failed to parse webhook event') @@ -63,8 +64,9 @@ export class AssemblyWebhookService extends AuthenticatedDropboxService { DISPATCHABLE_HANDLEABLE_EVENT.FolderCreated, DISPATCHABLE_HANDLEABLE_EVENT.FolderDeleted, DISPATCHABLE_HANDLEABLE_EVENT.FolderUpdated, - ].includes(eventType) || - !(webhookEvent.object !== ObjectType.FILE && webhookEvent.object !== ObjectType.FOLDER) // avoid file with object 'link' + ].includes(eventType) && + webhookEvent.data.object && + SYNCABLE_OBJECT_TYPES.includes(webhookEvent.data.object) // avoid non-syncable object types like 'link' // if (isValidWebhook) { // const record = await this.checkNonDuplicateWebhookRecord(webhookEvent) diff --git a/src/features/webhook/assembly/utils/types.ts b/src/features/webhook/assembly/utils/types.ts index c0b858c..3b384dd 100644 --- a/src/features/webhook/assembly/utils/types.ts +++ b/src/features/webhook/assembly/utils/types.ts @@ -13,7 +13,6 @@ export enum DISPATCHABLE_HANDLEABLE_EVENT { export const AssemblyWebhookSchema = z.object({ eventType: z.string(), created: z.string().optional(), - object: z.string().optional(), data: CopilotFileRetrieveSchema, }) diff --git a/src/features/webhook/dropbox/api/webhook.controller.ts b/src/features/webhook/dropbox/api/webhook.controller.ts index ec8cbec..320905d 100644 --- a/src/features/webhook/dropbox/api/webhook.controller.ts +++ b/src/features/webhook/dropbox/api/webhook.controller.ts @@ -25,14 +25,14 @@ export const handleWebhookUrlVerification = (req: NextRequest) => { } export const handleWebhookEvents = async (req: NextRequest) => { - await sleep(800) // prevent ping-pong case of webhooks - const signature = req.headers.get('X-Dropbox-Signature') if (!signature) return NextResponse.json({ error: 'Missing signature' }, { status: status.BAD_REQUEST }) const body = await req.text() + await sleep(800) // prevent ping-pong case of webhooks + const computedSignature = crypto .createHmac('sha256', env.DROPBOX_APP_SECRET) .update(body) From 7d7dc31e221590a69de0a66781cd881a5e3ec041 Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Mon, 4 May 2026 13:21:23 +0545 Subject: [PATCH 2/2] refactor(OUT-3680): remove unwanted condition check --- src/features/webhook/assembly/lib/webhook.service.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/features/webhook/assembly/lib/webhook.service.ts b/src/features/webhook/assembly/lib/webhook.service.ts index 0cdd491..50008d0 100644 --- a/src/features/webhook/assembly/lib/webhook.service.ts +++ b/src/features/webhook/assembly/lib/webhook.service.ts @@ -64,9 +64,7 @@ export class AssemblyWebhookService extends AuthenticatedDropboxService { DISPATCHABLE_HANDLEABLE_EVENT.FolderCreated, DISPATCHABLE_HANDLEABLE_EVENT.FolderDeleted, DISPATCHABLE_HANDLEABLE_EVENT.FolderUpdated, - ].includes(eventType) && - webhookEvent.data.object && - SYNCABLE_OBJECT_TYPES.includes(webhookEvent.data.object) // avoid non-syncable object types like 'link' + ].includes(eventType) && SYNCABLE_OBJECT_TYPES.includes(webhookEvent.data.object) // avoid non-syncable object types like 'link' // if (isValidWebhook) { // const record = await this.checkNonDuplicateWebhookRecord(webhookEvent)