[cli][dev] handle no response from edge functions (#8095)

When an edge function has no response during `vc dev`, we were seeing an unhelpful error message:

> The event listener did not respond.

Now, we'll see a much more specific error message:

> Unhandled rejection: Edge Function "api/edge-no-response.ts" did not return a response.
> Error! Failed to complete request to /api/edge-no-response: Error: socket hang up
This commit is contained in:
Sean Massa
2022-07-20 15:09:29 -05:00
committed by GitHub
parent 66c8544e8f
commit 5dc6f48e44
6 changed files with 72 additions and 9 deletions

View File

@@ -0,0 +1,7 @@
export const config = {
runtime: 'experimental-edge',
};
export default async function edge(request, event) {
// nothing returned
}

View File

@@ -0,0 +1,3 @@
export default function serverless(request, response) {
return response.send('hello from a serverless function');
}

View File

@@ -0,0 +1,7 @@
export const config = {
runtime: 'experimental-edge',
};
export default async function edge(request, event) {
// no response
}

View File

@@ -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);

View File

@@ -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) => {

View File

@@ -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<VercelProxyResponse>> {
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<VercelProxyResponse>> {
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<VercelProxyResponse>;