2
0

(webhook) Add custom timeout option

Closes #1128
This commit is contained in:
Baptiste Arnaud
2024-01-04 08:08:00 +01:00
parent d247e02cad
commit 34917b00ef
9 changed files with 241 additions and 15 deletions

View File

@ -32,9 +32,12 @@ import { VariableForTestInputs } from './VariableForTestInputs'
import { SwitchWithRelatedSettings } from '@/components/SwitchWithRelatedSettings' import { SwitchWithRelatedSettings } from '@/components/SwitchWithRelatedSettings'
import { import {
HttpMethod, HttpMethod,
defaultTimeout,
defaultWebhookAttributes, defaultWebhookAttributes,
defaultWebhookBlockOptions, defaultWebhookBlockOptions,
maxTimeout,
} from '@typebot.io/schemas/features/blocks/integrations/webhook/constants' } from '@typebot.io/schemas/features/blocks/integrations/webhook/constants'
import { NumberInput } from '@/components/inputs'
type Props = { type Props = {
blockId: string blockId: string
@ -81,6 +84,9 @@ export const WebhookAdvancedConfigForm = ({
const updateIsCustomBody = (isCustomBody: boolean) => const updateIsCustomBody = (isCustomBody: boolean) =>
onOptionsChange({ ...options, isCustomBody }) onOptionsChange({ ...options, isCustomBody })
const updateTimeout = (timeout: number | undefined) =>
onOptionsChange({ ...options, timeout })
const executeTestRequest = async () => { const executeTestRequest = async () => {
if (!typebot) return if (!typebot) return
setIsTestResponseLoading(true) setIsTestResponseLoading(true)
@ -196,6 +202,22 @@ export const WebhookAdvancedConfigForm = ({
)} )}
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>
<AccordionItem>
<AccordionButton justifyContent="space-between">
Advanced parameters
<AccordionIcon />
</AccordionButton>
<AccordionPanel pt="4">
<NumberInput
label="Timeout (s)"
defaultValue={options?.timeout ?? defaultTimeout}
min={1}
max={maxTimeout}
onValueChange={updateTimeout}
withVariableButton={false}
/>
</AccordionPanel>
</AccordionItem>
<AccordionItem> <AccordionItem>
<AccordionButton justifyContent="space-between"> <AccordionButton justifyContent="space-between">
Variable values for test Variable values for test

View File

@ -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' })
}

View File

@ -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. 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 ## 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 👍 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 👍

View File

@ -1411,6 +1411,11 @@
"required": [ "required": [
"id" "id"
] ]
},
"timeout": {
"type": "number",
"minimum": 1,
"maximum": 120
} }
} }
}, },
@ -1863,6 +1868,11 @@
"required": [ "required": [
"id" "id"
] ]
},
"timeout": {
"type": "number",
"minimum": 1,
"maximum": 120
} }
} }
}, },
@ -2077,6 +2087,11 @@
"required": [ "required": [
"id" "id"
] ]
},
"timeout": {
"type": "number",
"minimum": 1,
"maximum": 120
} }
} }
}, },
@ -2226,6 +2241,11 @@
"required": [ "required": [
"id" "id"
] ]
},
"timeout": {
"type": "number",
"minimum": 1,
"maximum": 120
} }
} }
}, },
@ -7595,6 +7615,11 @@
"required": [ "required": [
"id" "id"
] ]
},
"timeout": {
"type": "number",
"minimum": 1,
"maximum": 120
} }
} }
}, },
@ -8047,6 +8072,11 @@
"required": [ "required": [
"id" "id"
] ]
},
"timeout": {
"type": "number",
"minimum": 1,
"maximum": 120
} }
} }
}, },
@ -8261,6 +8291,11 @@
"required": [ "required": [
"id" "id"
] ]
},
"timeout": {
"type": "number",
"minimum": 1,
"maximum": 120
} }
} }
}, },
@ -8410,6 +8445,11 @@
"required": [ "required": [
"id" "id"
] ]
},
"timeout": {
"type": "number",
"minimum": 1,
"maximum": 120
} }
} }
}, },
@ -12096,6 +12136,11 @@
"required": [ "required": [
"id" "id"
] ]
},
"timeout": {
"type": "number",
"minimum": 1,
"maximum": 120
} }
} }
}, },
@ -12548,6 +12593,11 @@
"required": [ "required": [
"id" "id"
] ]
},
"timeout": {
"type": "number",
"minimum": 1,
"maximum": 120
} }
} }
}, },
@ -12762,6 +12812,11 @@
"required": [ "required": [
"id" "id"
] ]
},
"timeout": {
"type": "number",
"minimum": 1,
"maximum": 120
} }
} }
}, },
@ -12911,6 +12966,11 @@
"required": [ "required": [
"id" "id"
] ]
},
"timeout": {
"type": "number",
"minimum": 1,
"maximum": 120
} }
} }
}, },
@ -25701,6 +25761,11 @@
"type": "string" "type": "string"
} }
} }
},
"timeout": {
"type": "number",
"minimum": 1,
"maximum": 120
} }
} }
} }
@ -26144,6 +26209,11 @@
"type": "string" "type": "string"
} }
} }
},
"timeout": {
"type": "number",
"minimum": 1,
"maximum": 120
} }
} }
} }
@ -26349,6 +26419,11 @@
"type": "string" "type": "string"
} }
} }
},
"timeout": {
"type": "number",
"minimum": 1,
"maximum": 120
} }
} }
} }
@ -26489,6 +26564,11 @@
"type": "string" "type": "string"
} }
} }
},
"timeout": {
"type": "number",
"minimum": 1,
"maximum": 120
} }
} }
} }
@ -28977,6 +29057,11 @@
"required": [ "required": [
"id" "id"
] ]
},
"timeout": {
"type": "number",
"minimum": 1,
"maximum": 120
} }
} }
}, },
@ -29429,6 +29514,11 @@
"required": [ "required": [
"id" "id"
] ]
},
"timeout": {
"type": "number",
"minimum": 1,
"maximum": 120
} }
} }
}, },
@ -29643,6 +29733,11 @@
"required": [ "required": [
"id" "id"
] ]
},
"timeout": {
"type": "number",
"minimum": 1,
"maximum": 120
} }
} }
}, },
@ -29792,6 +29887,11 @@
"required": [ "required": [
"id" "id"
] ]
},
"timeout": {
"type": "number",
"minimum": 1,
"maximum": 120
} }
} }
}, },
@ -31766,6 +31866,11 @@
"required": [ "required": [
"id" "id"
] ]
},
"timeout": {
"type": "number",
"minimum": 1,
"maximum": 120
} }
} }
}, },
@ -32218,6 +32323,11 @@
"required": [ "required": [
"id" "id"
] ]
},
"timeout": {
"type": "number",
"minimum": 1,
"maximum": 120
} }
} }
}, },
@ -32432,6 +32542,11 @@
"required": [ "required": [
"id" "id"
] ]
},
"timeout": {
"type": "number",
"minimum": 1,
"maximum": 120
} }
} }
}, },
@ -32581,6 +32696,11 @@
"required": [ "required": [
"id" "id"
] ]
},
"timeout": {
"type": "number",
"minimum": 1,
"maximum": 120
} }
} }
}, },

View File

@ -5529,6 +5529,11 @@
"required": [ "required": [
"id" "id"
] ]
},
"timeout": {
"type": "number",
"minimum": 1,
"maximum": 120
} }
} }
}, },
@ -5981,6 +5986,11 @@
"required": [ "required": [
"id" "id"
] ]
},
"timeout": {
"type": "number",
"minimum": 1,
"maximum": 120
} }
} }
}, },
@ -6195,6 +6205,11 @@
"required": [ "required": [
"id" "id"
] ]
},
"timeout": {
"type": "number",
"minimum": 1,
"maximum": 120
} }
} }
}, },
@ -6344,6 +6359,11 @@
"required": [ "required": [
"id" "id"
] ]
},
"timeout": {
"type": "number",
"minimum": 1,
"maximum": 120
} }
} }
}, },
@ -9211,6 +9231,11 @@
"type": "string" "type": "string"
} }
} }
},
"timeout": {
"type": "number",
"minimum": 1,
"maximum": 120
} }
} }
} }
@ -9654,6 +9679,11 @@
"type": "string" "type": "string"
} }
} }
},
"timeout": {
"type": "number",
"minimum": 1,
"maximum": 120
} }
} }
} }
@ -9859,6 +9889,11 @@
"type": "string" "type": "string"
} }
} }
},
"timeout": {
"type": "number",
"minimum": 1,
"maximum": 120
} }
} }
} }
@ -9999,6 +10034,11 @@
"type": "string" "type": "string"
} }
} }
},
"timeout": {
"type": "number",
"minimum": 1,
"maximum": 120
} }
} }
} }

View File

@ -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 { parseSampleResult } from '@typebot.io/bot-engine/blocks/integrations/webhook/parseSampleResult'
import { import {
HttpMethod, HttpMethod,
defaultTimeout,
defaultWebhookAttributes, defaultWebhookAttributes,
maxTimeout,
} from '@typebot.io/schemas/features/blocks/integrations/webhook/constants' } from '@typebot.io/schemas/features/blocks/integrations/webhook/constants'
import { getBlockById } from '@typebot.io/lib/getBlockById' import { getBlockById } from '@typebot.io/lib/getBlockById'
import { import {
convertKeyValueTableToObject, convertKeyValueTableToObject,
longReqTimeoutWhitelist, longReqTimeoutWhitelist,
longRequestTimeout,
responseDefaultTimeout,
} from '@typebot.io/bot-engine/blocks/integrations/webhook/executeWebhookBlock' } from '@typebot.io/bot-engine/blocks/integrations/webhook/executeWebhookBlock'
const cors = initMiddleware(Cors()) const cors = initMiddleware(Cors())
@ -184,7 +184,7 @@ export const executeWebhook =
: undefined, : undefined,
body: body && !isJson ? body : undefined, body: body && !isJson ? body : undefined,
timeout: { timeout: {
response: isLongRequest ? longRequestTimeout : responseDefaultTimeout, response: isLongRequest ? maxTimeout : defaultTimeout,
}, },
} }
try { try {

View File

@ -22,7 +22,9 @@ import { parseVariables } from '@typebot.io/variables/parseVariables'
import prisma from '@typebot.io/lib/prisma' import prisma from '@typebot.io/lib/prisma'
import { import {
HttpMethod, HttpMethod,
defaultTimeout,
defaultWebhookAttributes, defaultWebhookAttributes,
maxTimeout,
} from '@typebot.io/schemas/features/blocks/integrations/webhook/constants' } from '@typebot.io/schemas/features/blocks/integrations/webhook/constants'
type ParsedWebhook = ExecutableWebhook & { type ParsedWebhook = ExecutableWebhook & {
@ -30,9 +32,6 @@ type ParsedWebhook = ExecutableWebhook & {
isJson: boolean isJson: boolean
} }
export const responseDefaultTimeout = 10000
export const longRequestTimeout = 120000
export const longReqTimeoutWhitelist = [ export const longReqTimeoutWhitelist = [
'https://api.openai.com', 'https://api.openai.com',
'https://retune.so', 'https://retune.so',
@ -44,7 +43,7 @@ export const longReqTimeoutWhitelist = [
export const webhookSuccessDescription = `Webhook successfuly executed.` export const webhookSuccessDescription = `Webhook successfuly executed.`
export const webhookErrorDescription = `Webhook returned an error.` export const webhookErrorDescription = `Webhook returned an error.`
type Params = { disableRequestTimeout?: boolean } type Params = { disableRequestTimeout?: boolean; timeout?: number }
export const executeWebhookBlock = async ( export const executeWebhookBlock = async (
state: SessionState, state: SessionState,
@ -86,7 +85,10 @@ export const executeWebhookBlock = async (
response: webhookResponse, response: webhookResponse,
logs: executeWebhookLogs, logs: executeWebhookLogs,
startTimeShouldBeUpdated, startTimeShouldBeUpdated,
} = await executeWebhook(parsedWebhook, params) } = await executeWebhook(parsedWebhook, {
...params,
timeout: block.options?.timeout,
})
return { return {
...resumeWebhookExecution({ ...resumeWebhookExecution({
@ -196,7 +198,12 @@ export const executeWebhook = async (
contentType?.includes('x-www-form-urlencoded') && body ? body : undefined, contentType?.includes('x-www-form-urlencoded') && body ? body : undefined,
body: body && !isJson ? (body as string) : undefined, body: body && !isJson ? (body as string) : undefined,
timeout: { timeout: {
response: isLongRequest ? longRequestTimeout : responseDefaultTimeout, response:
params.timeout && params.timeout !== defaultTimeout
? Math.min(params.timeout, maxTimeout) * 1000
: isLongRequest
? maxTimeout * 1000
: defaultTimeout * 1000,
}, },
} satisfies OptionsInit } satisfies OptionsInit
@ -207,8 +214,8 @@ export const executeWebhook = async (
description: webhookSuccessDescription, description: webhookSuccessDescription,
details: { details: {
statusCode: response.statusCode, statusCode: response.statusCode,
request,
response: safeJsonParse(response.body).data, response: safeJsonParse(response.body).data,
request,
}, },
}) })
return { return {
@ -217,7 +224,7 @@ export const executeWebhook = async (
data: safeJsonParse(response.body).data, data: safeJsonParse(response.body).data,
}, },
logs, logs,
startTimeShouldBeUpdated: isLongRequest, startTimeShouldBeUpdated: true,
} }
} catch (error) { } catch (error) {
if (error instanceof HTTPError) { if (error instanceof HTTPError) {
@ -234,7 +241,27 @@ export const executeWebhook = async (
response, 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 = { const response = {
statusCode: 500, statusCode: 500,
@ -245,11 +272,11 @@ export const executeWebhook = async (
status: 'error', status: 'error',
description: `Webhook failed to execute.`, description: `Webhook failed to execute.`,
details: { details: {
request,
response, response,
request,
}, },
}) })
return { response, logs, startTimeShouldBeUpdated: isLongRequest } return { response, logs, startTimeShouldBeUpdated: true }
} }
} }

View File

@ -21,3 +21,6 @@ export const defaultWebhookBlockOptions = {
isCustomBody: false, isCustomBody: false,
isExecutedOnClient: false, isExecutedOnClient: false,
} as const satisfies WebhookBlockV6['options'] } as const satisfies WebhookBlockV6['options']
export const defaultTimeout = 10
export const maxTimeout = 120

View File

@ -1,7 +1,7 @@
import { z } from '../../../../zod' import { z } from '../../../../zod'
import { blockBaseSchema } from '../../shared' import { blockBaseSchema } from '../../shared'
import { IntegrationBlockType } from '../constants' import { IntegrationBlockType } from '../constants'
import { HttpMethod } from './constants' import { HttpMethod, maxTimeout } from './constants'
const variableForTestSchema = z.object({ const variableForTestSchema = z.object({
id: z.string(), id: z.string(),
@ -46,6 +46,7 @@ export const webhookOptionsV5Schema = z.object({
isCustomBody: z.boolean().optional(), isCustomBody: z.boolean().optional(),
isExecutedOnClient: z.boolean().optional(), isExecutedOnClient: z.boolean().optional(),
webhook: webhookSchemas.v5.optional(), webhook: webhookSchemas.v5.optional(),
timeout: z.number().min(1).max(maxTimeout).optional(),
}) })
const webhookOptionsSchemas = { const webhookOptionsSchemas = {