diff --git a/packages/dev/src/main.ts b/packages/dev/src/main.ts index f85a8504..3325edd8 100644 --- a/packages/dev/src/main.ts +++ b/packages/dev/src/main.ts @@ -555,7 +555,6 @@ export class NetlifyDev { projectRoot: this.#projectRoot, settings: {}, siteId: this.#siteID, - timeouts: {}, userFunctionsPath: userFunctionsPathExists ? userFunctionsPath : undefined, }) } diff --git a/packages/functions/dev/src/registry.test.ts b/packages/functions/dev/src/registry.test.ts new file mode 100644 index 00000000..21271dbe --- /dev/null +++ b/packages/functions/dev/src/registry.test.ts @@ -0,0 +1,152 @@ +import { describe, expect, test } from 'vitest' + +import { SYNCHRONOUS_FUNCTION_TIMEOUT, BACKGROUND_FUNCTION_TIMEOUT } from '@netlify/functions' +import { FunctionsRegistry } from './registry.js' + +describe('FunctionsRegistry timeout configuration', () => { + test('uses default timeouts when no config or override provided', () => { + const registry = new FunctionsRegistry({ + config: {}, + destPath: '/tmp/test', + projectRoot: '/tmp/project', + settings: {}, + }) + + expect(registry.timeouts).toEqual({ + syncFunctions: SYNCHRONOUS_FUNCTION_TIMEOUT, + backgroundFunctions: BACKGROUND_FUNCTION_TIMEOUT, + }) + }) + + test('uses functions_timeout from siteInfo for sync functions only', () => { + const registry = new FunctionsRegistry({ + config: { + siteInfo: { + functions_timeout: 60, + }, + }, + destPath: '/tmp/test', + projectRoot: '/tmp/project', + settings: {}, + }) + + expect(registry.timeouts).toEqual({ + syncFunctions: 60, + backgroundFunctions: BACKGROUND_FUNCTION_TIMEOUT, + }) + }) + + test('uses functions_config.timeout from siteInfo for sync functions only', () => { + const registry = new FunctionsRegistry({ + config: { + siteInfo: { + functions_config: { + timeout: 45, + }, + }, + }, + destPath: '/tmp/test', + projectRoot: '/tmp/project', + settings: {}, + }) + + expect(registry.timeouts).toEqual({ + syncFunctions: 45, + backgroundFunctions: BACKGROUND_FUNCTION_TIMEOUT, + }) + }) + + test('prefers functions_timeout over functions_config.timeout for sync functions', () => { + const registry = new FunctionsRegistry({ + config: { + siteInfo: { + functions_timeout: 60, + functions_config: { + timeout: 45, + }, + }, + }, + destPath: '/tmp/test', + projectRoot: '/tmp/project', + settings: {}, + }) + + expect(registry.timeouts).toEqual({ + syncFunctions: 60, + backgroundFunctions: BACKGROUND_FUNCTION_TIMEOUT, + }) + }) + + test('uses override timeouts when provided', () => { + const registry = new FunctionsRegistry({ + config: { + siteInfo: { + functions_timeout: 60, + }, + }, + destPath: '/tmp/test', + projectRoot: '/tmp/project', + settings: {}, + timeouts: { + syncFunctions: 120, + backgroundFunctions: 1800, + }, + }) + + expect(registry.timeouts).toEqual({ + syncFunctions: 120, + backgroundFunctions: 1800, + }) + }) + + test('allows partial override of timeouts', () => { + const registry = new FunctionsRegistry({ + config: { + siteInfo: { + functions_timeout: 60, + }, + }, + destPath: '/tmp/test', + projectRoot: '/tmp/project', + settings: {}, + timeouts: { + syncFunctions: 120, + }, + }) + + expect(registry.timeouts).toEqual({ + syncFunctions: 120, + backgroundFunctions: BACKGROUND_FUNCTION_TIMEOUT, + }) + }) + + test('falls back to defaults when siteInfo is undefined', () => { + const registry = new FunctionsRegistry({ + config: { + siteInfo: undefined, + }, + destPath: '/tmp/test', + projectRoot: '/tmp/project', + settings: {}, + }) + + expect(registry.timeouts).toEqual({ + syncFunctions: SYNCHRONOUS_FUNCTION_TIMEOUT, + backgroundFunctions: BACKGROUND_FUNCTION_TIMEOUT, + }) + }) + + test('falls back to defaults when config is empty object', () => { + const registry = new FunctionsRegistry({ + config: {}, + destPath: '/tmp/test', + projectRoot: '/tmp/project', + settings: {}, + }) + + expect(registry.timeouts).toEqual({ + syncFunctions: SYNCHRONOUS_FUNCTION_TIMEOUT, + backgroundFunctions: BACKGROUND_FUNCTION_TIMEOUT, + }) + }) +}) diff --git a/packages/functions/dev/src/registry.ts b/packages/functions/dev/src/registry.ts index 8f0d3234..c6b4cdf2 100644 --- a/packages/functions/dev/src/registry.ts +++ b/packages/functions/dev/src/registry.ts @@ -5,6 +5,7 @@ import { env } from 'node:process' import type { EnvironmentContext as BlobsContext } from '@netlify/blobs' import { DevEventHandler, watchDebounced } from '@netlify/dev-utils' +import { SYNCHRONOUS_FUNCTION_TIMEOUT, BACKGROUND_FUNCTION_TIMEOUT } from '@netlify/functions' import { ListedFunction, listFunctions, Manifest } from '@netlify/zip-it-and-ship-it' import extractZip from 'extract-zip' @@ -37,7 +38,7 @@ export interface FunctionRegistryOptions { manifest?: Manifest projectRoot: string settings: any - timeouts: any + timeouts?: { syncFunctions?: number; backgroundFunctions?: number } watch?: boolean } @@ -100,7 +101,16 @@ export class FunctionsRegistry { this.handleEvent = eventHandler ?? (() => {}) this.internalFunctionsPath = internalFunctionsPath this.projectRoot = projectRoot - this.timeouts = timeouts + + // Calculate timeouts from config if not provided as override + const siteTimeout = config?.siteInfo?.functions_timeout ?? config?.siteInfo?.functions_config?.timeout + this.timeouts = { + syncFunctions: timeouts?.syncFunctions ?? siteTimeout ?? SYNCHRONOUS_FUNCTION_TIMEOUT, + // NOTE: This isn't documented, but the generically named "functions timeout" config fields only + // apply to synchronous Netlify Functions. + backgroundFunctions: timeouts?.backgroundFunctions ?? BACKGROUND_FUNCTION_TIMEOUT, + } + this.settings = settings this.watch = watch === true diff --git a/packages/functions/prod/src/lib/consts.test.ts b/packages/functions/prod/src/lib/consts.test.ts new file mode 100644 index 00000000..6a52df1a --- /dev/null +++ b/packages/functions/prod/src/lib/consts.test.ts @@ -0,0 +1,10 @@ +import { describe, expect, test } from 'vitest' + +import { BACKGROUND_FUNCTION_TIMEOUT, SYNCHRONOUS_FUNCTION_TIMEOUT } from './consts.js' + +describe('Function timeout constants', () => { + test('exports correct timeout values', () => { + expect(SYNCHRONOUS_FUNCTION_TIMEOUT).toBe(30) + expect(BACKGROUND_FUNCTION_TIMEOUT).toBe(900) + }) +}) diff --git a/packages/functions/prod/src/lib/consts.ts b/packages/functions/prod/src/lib/consts.ts index b128f6e4..aa620278 100644 --- a/packages/functions/prod/src/lib/consts.ts +++ b/packages/functions/prod/src/lib/consts.ts @@ -3,4 +3,21 @@ const HTTP_STATUS_METHOD_NOT_ALLOWED = 405 const HTTP_STATUS_OK = 200 const METADATA_VERSION = 1 -export { BUILDER_FUNCTIONS_FLAG, HTTP_STATUS_METHOD_NOT_ALLOWED, HTTP_STATUS_OK, METADATA_VERSION } +/** + * Default timeout for synchronous functions in seconds + */ +const SYNCHRONOUS_FUNCTION_TIMEOUT = 30 + +/** + * Default timeout for background functions in seconds + */ +const BACKGROUND_FUNCTION_TIMEOUT = 900 + +export { + BUILDER_FUNCTIONS_FLAG, + HTTP_STATUS_METHOD_NOT_ALLOWED, + HTTP_STATUS_OK, + METADATA_VERSION, + SYNCHRONOUS_FUNCTION_TIMEOUT, + BACKGROUND_FUNCTION_TIMEOUT, +} diff --git a/packages/functions/prod/src/main.ts b/packages/functions/prod/src/main.ts index ac79a7fd..86ad080c 100644 --- a/packages/functions/prod/src/main.ts +++ b/packages/functions/prod/src/main.ts @@ -10,4 +10,5 @@ export { builder } from './lib/builder.js' export { purgeCache } from './lib/purge_cache.js' export { schedule } from './lib/schedule.js' export { stream } from './lib/stream.js' +export { SYNCHRONOUS_FUNCTION_TIMEOUT, BACKGROUND_FUNCTION_TIMEOUT } from './lib/consts.js' export * from './function/index.js' diff --git a/packages/types/src/main.ts b/packages/types/src/main.ts index 6166a756..637d21b7 100644 --- a/packages/types/src/main.ts +++ b/packages/types/src/main.ts @@ -2,3 +2,4 @@ export type { Context } from './lib/context/context.js' export type { Cookie } from './lib/context/cookies.js' export type { EnvironmentVariables } from './lib/environment-variables.js' export type { NetlifyGlobal } from './lib/globals.js' +export type { Site } from './lib/context/site.js'