diff --git a/packages/cli/test/dev/fixtures/edge-function-error/api/edge-no-response.js b/packages/cli/test/dev/fixtures/edge-function-error/api/edge-no-response.js new file mode 100644 index 000000000..875d1c14d --- /dev/null +++ b/packages/cli/test/dev/fixtures/edge-function-error/api/edge-no-response.js @@ -0,0 +1,7 @@ +export const config = { + runtime: 'experimental-edge', +}; + +export default async function edge(request, event) { + // nothing returned +} diff --git a/packages/cli/test/dev/fixtures/middleware-no-response/api/hello.js b/packages/cli/test/dev/fixtures/middleware-no-response/api/hello.js new file mode 100644 index 000000000..5db3250fb --- /dev/null +++ b/packages/cli/test/dev/fixtures/middleware-no-response/api/hello.js @@ -0,0 +1,3 @@ +export default function serverless(request, response) { + return response.send('hello from a serverless function'); +} diff --git a/packages/cli/test/dev/fixtures/middleware-no-response/middleware.js b/packages/cli/test/dev/fixtures/middleware-no-response/middleware.js new file mode 100644 index 000000000..a4ffc11e1 --- /dev/null +++ b/packages/cli/test/dev/fixtures/middleware-no-response/middleware.js @@ -0,0 +1,7 @@ +export const config = { + runtime: 'experimental-edge', +}; + +export default async function edge(request, event) { + // no response +} diff --git a/packages/cli/test/dev/integration-1.test.ts b/packages/cli/test/dev/integration-1.test.ts index 385a9ddd1..af62d1cab 100644 --- a/packages/cli/test/dev/integration-1.test.ts +++ b/packages/cli/test/dev/integration-1.test.ts @@ -56,6 +56,31 @@ test( }) ); +test('[vercel dev] throws an error when an edge function has no response', async () => { + const dir = fixture('edge-function-error'); + const { dev, port, readyResolver } = await testFixture(dir); + + try { + await readyResolver; + + let res = await fetch(`http://localhost:${port}/api/edge-no-response`); + validateResponseHeaders(res); + + const { stdout, stderr } = await dev.kill('SIGTERM'); + + expect(await res.status).toBe(500); + expect(await res.text()).toMatch('FUNCTION_INVOCATION_FAILED'); + expect(stdout).toMatch( + /Unhandled rejection: Edge Function "api\/edge-no-response.js" did not return a response./g + ); + expect(stderr).toMatch( + /Failed to complete request to \/api\/edge-no-response: Error: socket hang up/g + ); + } finally { + await dev.kill('SIGTERM'); + } +}); + test('[vercel dev] should support edge functions returning intentional 500 responses', async () => { const dir = fixture('edge-function'); const { dev, port, readyResolver } = await testFixture(dir); diff --git a/packages/cli/test/dev/integration-4.test.ts b/packages/cli/test/dev/integration-4.test.ts index 4fb045f78..7cfbead3e 100644 --- a/packages/cli/test/dev/integration-4.test.ts +++ b/packages/cli/test/dev/integration-4.test.ts @@ -442,6 +442,17 @@ test( }) ); +test( + '[vercel dev] Middleware that has no response', + testFixtureStdio('middleware-no-response', async (testPath: any) => { + await testPath( + 500, + '/api/hello', + 'A server error has occurred\n\nEDGE_FUNCTION_INVOCATION_FAILED' + ); + }) +); + test( '[vercel dev] Middleware that does basic rewrite', testFixtureStdio('middleware-rewrite', async (testPath: any) => { diff --git a/packages/node/src/dev-server.ts b/packages/node/src/dev-server.ts index 7c9b818ac..4d2f50082 100644 --- a/packages/node/src/dev-server.ts +++ b/packages/node/src/dev-server.ts @@ -148,21 +148,26 @@ async function serializeRequest(message: IncomingMessage) { }); } -async function compileUserCode(entrypoint: string) { +async function compileUserCode( + entrypointPath: string, + entrypointLabel: string +) { try { const result = await esbuild.build({ platform: 'node', target: 'node14', sourcemap: 'inline', bundle: true, - entryPoints: [entrypoint], + entryPoints: [entrypointPath], write: false, // operate in memory format: 'cjs', }); const compiledFile = result.outputFiles?.[0]; if (!compiledFile) { - throw new Error(`Compilation of ${entrypoint} produced no output files.`); + throw new Error( + `Compilation of ${entrypointLabel} produced no output files.` + ); } const userCode = new TextDecoder().decode(compiledFile.contents); @@ -198,6 +203,10 @@ async function compileUserCode(entrypoint: string) { let response = await edgeHandler(event.request, event); + if (!response) { + throw new Error('Edge Function "${entrypointLabel}" did not return a response.'); + } + return event.respondWith(response); } catch (error) { // we can't easily show a meaningful stack trace @@ -252,9 +261,10 @@ async function createEdgeRuntime(userCode: string | undefined) { } async function createEdgeEventHandler( - entrypoint: string + entrypointPath: string, + entrypointLabel: string ): Promise<(request: IncomingMessage) => Promise> { - const userCode = await compileUserCode(entrypoint); + const userCode = await compileUserCode(entrypointPath, entrypointLabel); const server = await createEdgeRuntime(userCode); return async function (request: IncomingMessage) { @@ -317,17 +327,17 @@ async function createEventHandler( config: Config, options: { shouldAddHelpers: boolean } ): Promise<(request: IncomingMessage) => Promise> { - const entryPointPath = join(process.cwd(), entrypoint!); - const runtime = parseRuntime(entrypoint, entryPointPath); + const entrypointPath = join(process.cwd(), entrypoint!); + const runtime = parseRuntime(entrypoint, entrypointPath); // `middleware.js`/`middleware.ts` file is always run as // an Edge Function, otherwise needs to be opted-in via // `export const config = { runtime: 'experimental-edge' }` if (config.middleware === true || runtime === 'experimental-edge') { - return createEdgeEventHandler(entryPointPath); + return createEdgeEventHandler(entrypointPath, entrypoint); } - return createServerlessEventHandler(entryPointPath, options); + return createServerlessEventHandler(entrypointPath, options); } let handleEvent: (request: IncomingMessage) => Promise;