diff --git a/docs/providers/aws/events/http-api.md b/docs/providers/aws/events/http-api.md index 75682d904..049423f51 100644 --- a/docs/providers/aws/events/http-api.md +++ b/docs/providers/aws/events/http-api.md @@ -64,6 +64,32 @@ functions: path: /get/for/any/{param} ``` +### Endpoints timeout + +By default HTTP API will timeout within 5 seconds. Timeout can be restricted to as low value as 50 milliseconds or lifted up to 29 seconds. + +To adjust timeout for all configured endpoints, outline desired value in `provider` settings: + +```yaml +provider: + httpApi: + timeout: 0.5 # Restrict endpoints to timeout in 500ms +``` + +To adjust timeout for specific endpoint, outline it at `httpApi` event configuration: + +```yaml +functions: + withCustomTimeout: + handler: handler.withCustomTimeout + events: + - httpApi: + ... + timeout: 29 +``` + +**Note**: All `httpApi` events for same function should share same timeout setting. + ### CORS Setup With HTTP API we may configure CORS headers that'll be effective for all configured endpoints. diff --git a/docs/providers/aws/guide/serverless.yml.md b/docs/providers/aws/guide/serverless.yml.md index 31fd1b158..5387dcbf9 100644 --- a/docs/providers/aws/guide/serverless.yml.md +++ b/docs/providers/aws/guide/serverless.yml.md @@ -79,6 +79,7 @@ provider: targetGroupPrefix: xxxxxxxxxx # Optional prefix to prepend when generating names for target groups httpApi: id: # If we want to attach to externally created HTTP API its id should be provided here + timeout: 5 # Timeout setting for all endpoints, defaults to 5s, can be set to values ranging from 0.05s to 29s cors: true # Implies default behavior, can be fine tuned with specficic options authorizers: # JWT authorizers to back HTTP API endpoints @@ -239,6 +240,9 @@ functions: - httpApi: # HTTP API endpoint method: GET path: /some-get-path/{param} + # Timeout setting for given endpoint. Defaults to 5s, can be set to values from 0.05s to 29s + # Note: All httpApi events for same function need to share same timeout setting + timeout: 5 authorizer: # Optional name: someJwtAuthorizer # References by name authorizer defined in provider.httpApi.authorizers section scopes: # Optional diff --git a/lib/plugins/aws/package/compile/events/httpApi/index.js b/lib/plugins/aws/package/compile/events/httpApi/index.js index c8558045a..cc3e95e3d 100644 --- a/lib/plugins/aws/package/compile/events/httpApi/index.js +++ b/lib/plugins/aws/package/compile/events/httpApi/index.js @@ -258,8 +258,9 @@ Object.defineProperties( let method; let path; let authorizer; + let timeout; if (_.isObject(event.httpApi)) { - ({ method, path, authorizer } = event.httpApi); + ({ method, path, authorizer, timeout } = event.httpApi); } else { const methodPath = String(event.httpApi); if (methodPath === '*') { @@ -353,6 +354,17 @@ Object.defineProperties( routeConfig.authorizer = authorizers.get(name); if (scopes) routeConfig.authorizationScopes = toSet(scopes); } + if (!timeout) timeout = userConfig.timeout || null; + if (typeof routeTargetData.timeout !== 'undefined') { + if (routeTargetData.timeout !== timeout) { + throw new this.serverless.classes.Error( + `Inconsistent timeout settings for ${functionName} events`, + 'INCONSISTENT_HTTP_API_TIMEOUT' + ); + } + } else { + routeTargetData.timeout = timeout; + } routes.set(routeKey, routeConfig); if (shouldFillCorsMethods) { if (event.resolvedMethod === 'ANY') { @@ -367,16 +379,20 @@ Object.defineProperties( } }), compileIntegration: d(function(routeTargetData) { + const properties = { + ApiId: this.getApiIdConfig(), + IntegrationType: 'AWS_PROXY', + IntegrationUri: resolveTargetConfig(routeTargetData), + PayloadFormatVersion: '1.0', + }; + if (routeTargetData.timeout) { + properties.TimeoutInMillis = Math.round(routeTargetData.timeout * 1000); + } this.cfTemplate.Resources[ this.provider.naming.getHttpApiIntegrationLogicalId(routeTargetData.functionName) ] = { Type: 'AWS::ApiGatewayV2::Integration', - Properties: { - ApiId: this.getApiIdConfig(), - IntegrationType: 'AWS_PROXY', - IntegrationUri: resolveTargetConfig(routeTargetData), - PayloadFormatVersion: '1.0', - }, + Properties: properties, }; }), compileLambdaPermissions: d(function(routeTargetData) { diff --git a/lib/plugins/aws/package/compile/events/httpApi/index.test.js b/lib/plugins/aws/package/compile/events/httpApi/index.test.js index dfdeaa155..e4d6740ae 100644 --- a/lib/plugins/aws/package/compile/events/httpApi/index.test.js +++ b/lib/plugins/aws/package/compile/events/httpApi/index.test.js @@ -452,4 +452,47 @@ describe('HttpApiEvents', () => { expect(resource.Properties.Action).to.equal('lambda:InvokeFunction'); }); }); + + describe('Timeout', () => { + let cfResources; + let naming; + + before(() => + fixtures + .extend('httpApi', { + provider: { httpApi: { timeout: 3 } }, + functions: { + other: { + events: [ + { + httpApi: { + timeout: 20.56, + }, + }, + ], + }, + }, + }) + .then(fixturePath => + runServerless({ + cwd: fixturePath, + cliArgs: ['package'], + }).then(serverless => { + ({ + Resources: cfResources, + } = serverless.service.provider.compiledCloudFormationTemplate); + naming = serverless.getProvider('aws').naming; + }) + ) + ); + + it('Should support timeout set at endpoint', () => { + const resource = cfResources[naming.getHttpApiIntegrationLogicalId('other')]; + expect(resource.Properties.TimeoutInMillis).to.equal(20560); + }); + it('Should support globally set timeout', () => { + const resource = cfResources[naming.getHttpApiIntegrationLogicalId('foo')]; + expect(resource.Properties.TimeoutInMillis).to.equal(3000); + }); + }); }); diff --git a/tests/fixtures/httpApi/index.js b/tests/fixtures/httpApi/index.js index a638afcf6..124ad960f 100644 --- a/tests/fixtures/httpApi/index.js +++ b/tests/fixtures/httpApi/index.js @@ -1,10 +1,14 @@ 'use strict'; -module.exports.handler = (event, context, callback) => - callback(null, { - statusCode: 200, - body: JSON.stringify({ - path: event.path, - method: event.httpMethod, - }), - }); +module.exports.handler = (event, context, callback) => { + const resolve = () => + callback(null, { + statusCode: 200, + body: JSON.stringify({ + path: event.path, + method: event.httpMethod, + }), + }); + if (event.path === '/bar/timeout') setTimeout(resolve, 2000); + else resolve(); +}; diff --git a/tests/integration-all/http-api/tests.js b/tests/integration-all/http-api/tests.js index 9793ceb9c..e8b4f6d2e 100644 --- a/tests/integration-all/http-api/tests.js +++ b/tests/integration-all/http-api/tests.js @@ -89,6 +89,15 @@ describe('HTTP API Integration Test', function() { }, ], }, + other: { + events: [ + { + httpApi: { + timeout: 1, + }, + }, + ], + }, }, }), }); @@ -136,6 +145,13 @@ describe('HTTP API Integration Test', function() { expect(response.status).to.equal(404); }); + it('should respect timeout settings', async () => { + const testEndpoint = `${endpoint}/bar/timeout`; + + const response = await fetch(testEndpoint, { method: 'GET' }); + expect(response.status).to.equal(503); + }); + it('should support CORS when indicated', async () => { const testEndpoint = `${endpoint}/bar/whatever`;