diff --git a/src/commands/functions/invoke.js b/src/commands/functions/invoke.js index 09bcfcfa61d..6e650f45edc 100644 --- a/src/commands/functions/invoke.js +++ b/src/commands/functions/invoke.js @@ -8,11 +8,11 @@ const inquirer = require('inquirer') const fetch = require('node-fetch') const Command = require('../../utils/command') -const { getFunctions } = require('../../utils/get-functions') +const { getFunctions, BACKGROUND } = require('../../utils/get-functions') const { NETLIFYDEVWARN } = require('../../utils/logo') // https://www.netlify.com/docs/functions/#event-triggered-functions -const eventTriggeredFunctions = new Set([ +const events = [ 'deploy-building', 'deploy-succeeded', 'deploy-failed', @@ -25,7 +25,8 @@ const eventTriggeredFunctions = new Set([ 'identity-validate', 'identity-signup', 'identity-login', -]) +] +const eventTriggeredFunctions = new Set([...events, ...events.map((name) => `${name}${BACKGROUND}`)]) const DEFAULT_PORT = 8888 diff --git a/src/utils/get-functions.js b/src/utils/get-functions.js index 5b36e925916..fd1874b8b9f 100644 --- a/src/utils/get-functions.js +++ b/src/utils/get-functions.js @@ -44,4 +44,4 @@ const getFunctionsAndWatchDirs = async (functionsSrcDir) => { return { functions: functionsWithProps, watchDirs } } -module.exports = { getFunctions, getFunctionsAndWatchDirs } +module.exports = { getFunctions, getFunctionsAndWatchDirs, BACKGROUND } diff --git a/src/utils/serve-functions.js b/src/utils/serve-functions.js index cc8ed46f88c..53fc75b3785 100644 --- a/src/utils/serve-functions.js +++ b/src/utils/serve-functions.js @@ -21,7 +21,7 @@ const winston = require('winston') const { getLogMessage } = require('../lib/log') const { detectFunctionsBuilder } = require('./detect-functions-builder') -const { getFunctionsAndWatchDirs } = require('./get-functions') +const { getFunctionsAndWatchDirs, BACKGROUND } = require('./get-functions') const { NETLIFYDEVLOG, NETLIFYDEVERR } = require('./logo') const formatLambdaLocalError = (err) => `${err.errorType}: ${err.errorMessage}\n ${err.stackTrace.join('\n ')}` @@ -322,7 +322,25 @@ const setupDefaultFunctionHandler = async ({ capabilities, directory, warn }) => return { getFunctionByName } } -const createFormSubmissionHandler = function ({ siteUrl, warn }) { +const getFormHandler = function ({ getFunctionByName, warn }) { + const handlers = ['submission-created', `submission-created${BACKGROUND}`] + .map((name) => getFunctionByName(name)) + .filter(Boolean) + .map(({ name }) => name) + + if (handlers.length === 0) { + warn(`Missing form submission function handler`) + return + } + + if (handlers.length === 2) { + warn(`Detected both '${handlers[0]}' and '${handlers[1]}' form submission functions handlers, using ${handlers[0]}`) + } + + return handlers[0] +} + +const createFormSubmissionHandler = function ({ getFunctionByName, siteUrl, warn }) { return async function formSubmissionHandler(req, res, next) { if (req.url.startsWith('/.netlify/') || req.method !== 'POST') return next() @@ -334,8 +352,13 @@ const createFormSubmissionHandler = function ({ siteUrl, warn }) { }) fakeRequest.headers = req.headers + const handlerName = getFormHandler({ getFunctionByName, warn }) + if (!handlerName) { + return next() + } + const originalUrl = new URL(req.url, 'http://localhost') - req.url = `/.netlify/functions/submission-created${originalUrl.search}` + req.url = `/.netlify/functions/${handlerName}${originalUrl.search}` const ct = parseContentType(req) let fields = {} @@ -450,7 +473,7 @@ const getFunctionsServer = async function ({ getFunctionByName, siteUrl, warn, t }), ) app.use(bodyParser.raw({ limit: '6mb', type: '*/*' })) - app.use(createFormSubmissionHandler({ siteUrl, warn })) + app.use(createFormSubmissionHandler({ getFunctionByName, siteUrl, warn })) app.use( expressLogging(console, { blacklist: ['/favicon.ico'], diff --git a/tests/command.dev.test.js b/tests/command.dev.test.js index 59e50320d8b..111acaa7b8d 100644 --- a/tests/command.dev.test.js +++ b/tests/command.dev.test.js @@ -694,6 +694,39 @@ testMatrix.forEach(({ args }) => { }) }) + test(testName('should handle form submission with a background function', args), async (t) => { + await withSiteBuilder('site-with-form-background-function', async (builder) => { + await builder + .withContentFile({ + path: 'index.html', + content: '

⊂◉‿◉つ

', + }) + .withNetlifyToml({ + config: { + functions: { directory: 'functions' }, + }, + }) + .withFunction({ + path: 'submission-created-background.js', + handler: async (event) => ({ + statusCode: 200, + body: JSON.stringify(event), + }), + }) + .buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const form = new FormData() + form.append('some', 'thing') + const response = await got.post(`${server.url}/?ding=dong`, { + body: form, + }) + t.is(response.statusCode, 202) + t.is(response.body, '') + }) + }) + }) + test(testName('should not handle form submission when content type is `text/plain`', args), async (t) => { await withSiteBuilder('site-with-form-text-plain', async (builder) => { builder diff --git a/tests/command.functions.test.js b/tests/command.functions.test.js index f5117d1bc29..2c7929c4575 100644 --- a/tests/command.functions.test.js +++ b/tests/command.functions.test.js @@ -247,4 +247,30 @@ test('should use settings from netlify.toml dev', async (t) => { }) }) +test('should trigger background function from event', async (t) => { + await withSiteBuilder('site-with-ping-function', async (builder) => { + await builder + .withNetlifyToml({ config: { functions: { directory: 'functions' } } }) + .withFunction({ + path: 'identity-validate-background.js', + handler: async (event) => ({ + statusCode: 200, + body: JSON.stringify(event.body), + }), + }) + .buildAsync() + + await withDevServer({ cwd: builder.directory }, async (server) => { + const stdout = await callCli( + ['functions:invoke', 'identity-validate-background', '--identity', `--port=${server.port}`], + { + cwd: builder.directory, + }, + ) + // background functions always return an empty response + t.is(stdout, '') + }) + }) +}) + /* eslint-enable require-await */