diff --git a/apps/builder/src/features/blocks/integrations/webhook/components/WebhookAdvancedConfigForm.tsx b/apps/builder/src/features/blocks/integrations/webhook/components/WebhookAdvancedConfigForm.tsx index 0d0e5e1732..7453803d80 100644 --- a/apps/builder/src/features/blocks/integrations/webhook/components/WebhookAdvancedConfigForm.tsx +++ b/apps/builder/src/features/blocks/integrations/webhook/components/WebhookAdvancedConfigForm.tsx @@ -32,9 +32,12 @@ import { VariableForTestInputs } from './VariableForTestInputs' import { SwitchWithRelatedSettings } from '@/components/SwitchWithRelatedSettings' import { HttpMethod, + defaultTimeout, defaultWebhookAttributes, defaultWebhookBlockOptions, + maxTimeout, } from '@typebot.io/schemas/features/blocks/integrations/webhook/constants' +import { NumberInput } from '@/components/inputs' type Props = { blockId: string @@ -81,6 +84,9 @@ export const WebhookAdvancedConfigForm = ({ const updateIsCustomBody = (isCustomBody: boolean) => onOptionsChange({ ...options, isCustomBody }) + const updateTimeout = (timeout: number | undefined) => + onOptionsChange({ ...options, timeout }) + const executeTestRequest = async () => { if (!typebot) return setIsTestResponseLoading(true) @@ -196,6 +202,22 @@ export const WebhookAdvancedConfigForm = ({ )} + + + Advanced parameters + + + + + + Variable values for test diff --git a/apps/builder/src/pages/api/test.ts b/apps/builder/src/pages/api/test.ts new file mode 100644 index 0000000000..1657e5469b --- /dev/null +++ b/apps/builder/src/pages/api/test.ts @@ -0,0 +1,9 @@ +import { NextApiRequest, NextApiResponse } from 'next' + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + await new Promise((resolve) => setTimeout(resolve, 11000)) + res.status(200).json({ name: 'John Doe' }) +} diff --git a/apps/docs/editor/blocks/integrations/webhook.mdx b/apps/docs/editor/blocks/integrations/webhook.mdx index d2a657e184..8fda4288a5 100644 --- a/apps/docs/editor/blocks/integrations/webhook.mdx +++ b/apps/docs/editor/blocks/integrations/webhook.mdx @@ -108,6 +108,10 @@ Possibilities are endless when it comes to API calls, you can litteraly call any Feel free to ask the [community](https://typebot.io/discord) for help if you struggle setting up a Webhook block. +## Timeout + +By default, the Webhook block will wait 10 seconds for the 3rd party service to respond. If it doesn't respond in time, the block will fail. You can customize this timeout value in the "Advanced params" section of your Webhook block settings. + ## Troubleshooting The Webhook block request fail or didn't seem to trigger? Make sure to check the [logs](/results/overview#logs). If you still can't figure out what went wrong, shoot me a message using the chat button directly in the tool 👍 diff --git a/apps/docs/openapi/builder.json b/apps/docs/openapi/builder.json index 35a29d70e2..e8f8438a0d 100644 --- a/apps/docs/openapi/builder.json +++ b/apps/docs/openapi/builder.json @@ -1411,6 +1411,11 @@ "required": [ "id" ] + }, + "timeout": { + "type": "number", + "minimum": 1, + "maximum": 120 } } }, @@ -1863,6 +1868,11 @@ "required": [ "id" ] + }, + "timeout": { + "type": "number", + "minimum": 1, + "maximum": 120 } } }, @@ -2077,6 +2087,11 @@ "required": [ "id" ] + }, + "timeout": { + "type": "number", + "minimum": 1, + "maximum": 120 } } }, @@ -2226,6 +2241,11 @@ "required": [ "id" ] + }, + "timeout": { + "type": "number", + "minimum": 1, + "maximum": 120 } } }, @@ -7595,6 +7615,11 @@ "required": [ "id" ] + }, + "timeout": { + "type": "number", + "minimum": 1, + "maximum": 120 } } }, @@ -8047,6 +8072,11 @@ "required": [ "id" ] + }, + "timeout": { + "type": "number", + "minimum": 1, + "maximum": 120 } } }, @@ -8261,6 +8291,11 @@ "required": [ "id" ] + }, + "timeout": { + "type": "number", + "minimum": 1, + "maximum": 120 } } }, @@ -8410,6 +8445,11 @@ "required": [ "id" ] + }, + "timeout": { + "type": "number", + "minimum": 1, + "maximum": 120 } } }, @@ -12096,6 +12136,11 @@ "required": [ "id" ] + }, + "timeout": { + "type": "number", + "minimum": 1, + "maximum": 120 } } }, @@ -12548,6 +12593,11 @@ "required": [ "id" ] + }, + "timeout": { + "type": "number", + "minimum": 1, + "maximum": 120 } } }, @@ -12762,6 +12812,11 @@ "required": [ "id" ] + }, + "timeout": { + "type": "number", + "minimum": 1, + "maximum": 120 } } }, @@ -12911,6 +12966,11 @@ "required": [ "id" ] + }, + "timeout": { + "type": "number", + "minimum": 1, + "maximum": 120 } } }, @@ -25701,6 +25761,11 @@ "type": "string" } } + }, + "timeout": { + "type": "number", + "minimum": 1, + "maximum": 120 } } } @@ -26144,6 +26209,11 @@ "type": "string" } } + }, + "timeout": { + "type": "number", + "minimum": 1, + "maximum": 120 } } } @@ -26349,6 +26419,11 @@ "type": "string" } } + }, + "timeout": { + "type": "number", + "minimum": 1, + "maximum": 120 } } } @@ -26489,6 +26564,11 @@ "type": "string" } } + }, + "timeout": { + "type": "number", + "minimum": 1, + "maximum": 120 } } } @@ -28977,6 +29057,11 @@ "required": [ "id" ] + }, + "timeout": { + "type": "number", + "minimum": 1, + "maximum": 120 } } }, @@ -29429,6 +29514,11 @@ "required": [ "id" ] + }, + "timeout": { + "type": "number", + "minimum": 1, + "maximum": 120 } } }, @@ -29643,6 +29733,11 @@ "required": [ "id" ] + }, + "timeout": { + "type": "number", + "minimum": 1, + "maximum": 120 } } }, @@ -29792,6 +29887,11 @@ "required": [ "id" ] + }, + "timeout": { + "type": "number", + "minimum": 1, + "maximum": 120 } } }, @@ -31766,6 +31866,11 @@ "required": [ "id" ] + }, + "timeout": { + "type": "number", + "minimum": 1, + "maximum": 120 } } }, @@ -32218,6 +32323,11 @@ "required": [ "id" ] + }, + "timeout": { + "type": "number", + "minimum": 1, + "maximum": 120 } } }, @@ -32432,6 +32542,11 @@ "required": [ "id" ] + }, + "timeout": { + "type": "number", + "minimum": 1, + "maximum": 120 } } }, @@ -32581,6 +32696,11 @@ "required": [ "id" ] + }, + "timeout": { + "type": "number", + "minimum": 1, + "maximum": 120 } } }, diff --git a/apps/docs/openapi/viewer.json b/apps/docs/openapi/viewer.json index 881e3d8c80..b356858496 100644 --- a/apps/docs/openapi/viewer.json +++ b/apps/docs/openapi/viewer.json @@ -5529,6 +5529,11 @@ "required": [ "id" ] + }, + "timeout": { + "type": "number", + "minimum": 1, + "maximum": 120 } } }, @@ -5981,6 +5986,11 @@ "required": [ "id" ] + }, + "timeout": { + "type": "number", + "minimum": 1, + "maximum": 120 } } }, @@ -6195,6 +6205,11 @@ "required": [ "id" ] + }, + "timeout": { + "type": "number", + "minimum": 1, + "maximum": 120 } } }, @@ -6344,6 +6359,11 @@ "required": [ "id" ] + }, + "timeout": { + "type": "number", + "minimum": 1, + "maximum": 120 } } }, @@ -9211,6 +9231,11 @@ "type": "string" } } + }, + "timeout": { + "type": "number", + "minimum": 1, + "maximum": 120 } } } @@ -9654,6 +9679,11 @@ "type": "string" } } + }, + "timeout": { + "type": "number", + "minimum": 1, + "maximum": 120 } } } @@ -9859,6 +9889,11 @@ "type": "string" } } + }, + "timeout": { + "type": "number", + "minimum": 1, + "maximum": 120 } } } @@ -9999,6 +10034,11 @@ "type": "string" } } + }, + "timeout": { + "type": "number", + "minimum": 1, + "maximum": 120 } } } diff --git a/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/executeWebhook.ts b/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/executeWebhook.ts index c5428a5ba5..f9b5c55658 100644 --- a/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/executeWebhook.ts +++ b/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/executeWebhook.ts @@ -23,14 +23,14 @@ import { saveSuccessLog } from '@typebot.io/bot-engine/logs/saveSuccessLog' import { parseSampleResult } from '@typebot.io/bot-engine/blocks/integrations/webhook/parseSampleResult' import { HttpMethod, + defaultTimeout, defaultWebhookAttributes, + maxTimeout, } from '@typebot.io/schemas/features/blocks/integrations/webhook/constants' import { getBlockById } from '@typebot.io/lib/getBlockById' import { convertKeyValueTableToObject, longReqTimeoutWhitelist, - longRequestTimeout, - responseDefaultTimeout, } from '@typebot.io/bot-engine/blocks/integrations/webhook/executeWebhookBlock' const cors = initMiddleware(Cors()) @@ -184,7 +184,7 @@ export const executeWebhook = : undefined, body: body && !isJson ? body : undefined, timeout: { - response: isLongRequest ? longRequestTimeout : responseDefaultTimeout, + response: isLongRequest ? maxTimeout : defaultTimeout, }, } try { diff --git a/packages/bot-engine/blocks/integrations/webhook/executeWebhookBlock.ts b/packages/bot-engine/blocks/integrations/webhook/executeWebhookBlock.ts index c095f1d87d..699e60b97a 100644 --- a/packages/bot-engine/blocks/integrations/webhook/executeWebhookBlock.ts +++ b/packages/bot-engine/blocks/integrations/webhook/executeWebhookBlock.ts @@ -22,7 +22,9 @@ import { parseVariables } from '@typebot.io/variables/parseVariables' import prisma from '@typebot.io/lib/prisma' import { HttpMethod, + defaultTimeout, defaultWebhookAttributes, + maxTimeout, } from '@typebot.io/schemas/features/blocks/integrations/webhook/constants' type ParsedWebhook = ExecutableWebhook & { @@ -30,9 +32,6 @@ type ParsedWebhook = ExecutableWebhook & { isJson: boolean } -export const responseDefaultTimeout = 10000 -export const longRequestTimeout = 120000 - export const longReqTimeoutWhitelist = [ 'https://api.openai.com', 'https://retune.so', @@ -44,7 +43,7 @@ export const longReqTimeoutWhitelist = [ export const webhookSuccessDescription = `Webhook successfuly executed.` export const webhookErrorDescription = `Webhook returned an error.` -type Params = { disableRequestTimeout?: boolean } +type Params = { disableRequestTimeout?: boolean; timeout?: number } export const executeWebhookBlock = async ( state: SessionState, @@ -86,7 +85,10 @@ export const executeWebhookBlock = async ( response: webhookResponse, logs: executeWebhookLogs, startTimeShouldBeUpdated, - } = await executeWebhook(parsedWebhook, params) + } = await executeWebhook(parsedWebhook, { + ...params, + timeout: block.options?.timeout, + }) return { ...resumeWebhookExecution({ @@ -196,7 +198,12 @@ export const executeWebhook = async ( contentType?.includes('x-www-form-urlencoded') && body ? body : undefined, body: body && !isJson ? (body as string) : undefined, timeout: { - response: isLongRequest ? longRequestTimeout : responseDefaultTimeout, + response: + params.timeout && params.timeout !== defaultTimeout + ? Math.min(params.timeout, maxTimeout) * 1000 + : isLongRequest + ? maxTimeout * 1000 + : defaultTimeout * 1000, }, } satisfies OptionsInit @@ -207,8 +214,8 @@ export const executeWebhook = async ( description: webhookSuccessDescription, details: { statusCode: response.statusCode, - request, response: safeJsonParse(response.body).data, + request, }, }) return { @@ -217,7 +224,7 @@ export const executeWebhook = async ( data: safeJsonParse(response.body).data, }, logs, - startTimeShouldBeUpdated: isLongRequest, + startTimeShouldBeUpdated: true, } } catch (error) { if (error instanceof HTTPError) { @@ -234,7 +241,27 @@ export const executeWebhook = async ( response, }, }) - return { response, logs, startTimeShouldBeUpdated: isLongRequest } + return { response, logs, startTimeShouldBeUpdated: true } + } + if ( + typeof error === 'object' && + error && + 'code' in error && + error.code === 'ETIMEDOUT' + ) { + const response = { + statusCode: 408, + data: { message: `Request timed out.` }, + } + logs.push({ + status: 'error', + description: `Webhook request timed out. (${request.timeout.response}ms)`, + details: { + response, + request, + }, + }) + return { response, logs, startTimeShouldBeUpdated: true } } const response = { statusCode: 500, @@ -245,11 +272,11 @@ export const executeWebhook = async ( status: 'error', description: `Webhook failed to execute.`, details: { - request, response, + request, }, }) - return { response, logs, startTimeShouldBeUpdated: isLongRequest } + return { response, logs, startTimeShouldBeUpdated: true } } } diff --git a/packages/schemas/features/blocks/integrations/webhook/constants.ts b/packages/schemas/features/blocks/integrations/webhook/constants.ts index fe1dbc4d37..8bc208c171 100644 --- a/packages/schemas/features/blocks/integrations/webhook/constants.ts +++ b/packages/schemas/features/blocks/integrations/webhook/constants.ts @@ -21,3 +21,6 @@ export const defaultWebhookBlockOptions = { isCustomBody: false, isExecutedOnClient: false, } as const satisfies WebhookBlockV6['options'] + +export const defaultTimeout = 10 +export const maxTimeout = 120 diff --git a/packages/schemas/features/blocks/integrations/webhook/schema.ts b/packages/schemas/features/blocks/integrations/webhook/schema.ts index 2fa71fb850..d4c473693d 100644 --- a/packages/schemas/features/blocks/integrations/webhook/schema.ts +++ b/packages/schemas/features/blocks/integrations/webhook/schema.ts @@ -1,7 +1,7 @@ import { z } from '../../../../zod' import { blockBaseSchema } from '../../shared' import { IntegrationBlockType } from '../constants' -import { HttpMethod } from './constants' +import { HttpMethod, maxTimeout } from './constants' const variableForTestSchema = z.object({ id: z.string(), @@ -46,6 +46,7 @@ export const webhookOptionsV5Schema = z.object({ isCustomBody: z.boolean().optional(), isExecutedOnClient: z.boolean().optional(), webhook: webhookSchemas.v5.optional(), + timeout: z.number().min(1).max(maxTimeout).optional(), }) const webhookOptionsSchemas = {