mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-07 21:07:46 +00:00
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
542 lines
15 KiB
TypeScript
542 lines
15 KiB
TypeScript
import ms from 'ms';
|
|
import fs from 'fs-extra';
|
|
import { isIP } from 'net';
|
|
import { join } from 'path';
|
|
|
|
const {
|
|
fetch,
|
|
sleep,
|
|
fixture,
|
|
testFixture,
|
|
testFixtureStdio,
|
|
validateResponseHeaders,
|
|
} = require('./utils.js');
|
|
|
|
test(
|
|
'[vercel dev] temporary directory listing',
|
|
testFixtureStdio(
|
|
'temporary-directory-listing',
|
|
async (_testPath: any, port: any) => {
|
|
const directory = fixture('temporary-directory-listing');
|
|
await fs.unlink(join(directory, 'index.txt')).catch(() => null);
|
|
|
|
await sleep(ms('20s'));
|
|
|
|
const firstResponse = await fetch(`http://localhost:${port}`);
|
|
validateResponseHeaders(firstResponse);
|
|
const body = await firstResponse.text();
|
|
console.log(body);
|
|
expect(firstResponse.status).toBe(404);
|
|
|
|
await fs.writeFile(join(directory, 'index.txt'), 'hello');
|
|
|
|
for (let i = 0; i < 20; i++) {
|
|
const response = await fetch(`http://localhost:${port}`);
|
|
validateResponseHeaders(response);
|
|
|
|
if (response.status === 200) {
|
|
const body = await response.text();
|
|
expect(body).toBe('hello');
|
|
}
|
|
|
|
await sleep(ms('1s'));
|
|
}
|
|
},
|
|
{ skipDeploy: true }
|
|
)
|
|
);
|
|
|
|
test('[vercel dev] add a `package.json` to trigger `@vercel/static-build`', async () => {
|
|
const directory = fixture('trigger-static-build');
|
|
|
|
await fs.unlink(join(directory, 'package.json')).catch(() => null);
|
|
|
|
await fs.unlink(join(directory, 'public', 'index.txt')).catch(() => null);
|
|
|
|
await fs.rmdir(join(directory, 'public')).catch(() => null);
|
|
|
|
const tester = testFixtureStdio(
|
|
'trigger-static-build',
|
|
async (_testPath: any, port: any) => {
|
|
{
|
|
const response = await fetch(`http://localhost:${port}`);
|
|
validateResponseHeaders(response);
|
|
const body = await response.text();
|
|
expect(body.trim()).toBe('hello:index.txt');
|
|
}
|
|
|
|
const rnd = Math.random().toString();
|
|
const pkg = {
|
|
private: true,
|
|
scripts: { build: `mkdir -p public && echo ${rnd} > public/index.txt` },
|
|
};
|
|
|
|
await fs.writeFile(join(directory, 'package.json'), JSON.stringify(pkg));
|
|
|
|
// Wait until file events have been processed
|
|
await sleep(ms('2s'));
|
|
|
|
{
|
|
const response = await fetch(`http://localhost:${port}`);
|
|
validateResponseHeaders(response);
|
|
const body = await response.text();
|
|
expect(body.trim()).toBe(rnd);
|
|
}
|
|
},
|
|
{ skipDeploy: true }
|
|
);
|
|
|
|
await tester();
|
|
});
|
|
|
|
test('[vercel dev] no build matches warning', async () => {
|
|
const directory = fixture('no-build-matches');
|
|
const { dev } = await testFixture(directory, {
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
|
|
try {
|
|
// start `vercel dev` detached in child_process
|
|
dev.unref();
|
|
|
|
dev.stderr.setEncoding('utf8');
|
|
await new Promise<void>(resolve => {
|
|
dev.stderr.on('data', (str: string) => {
|
|
if (str.includes('did not match any source files')) {
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
} finally {
|
|
dev.kill('SIGTERM');
|
|
}
|
|
});
|
|
|
|
test(
|
|
'[vercel dev] do not recursivly check the path',
|
|
testFixtureStdio('handle-filesystem-missing', async (testPath: any) => {
|
|
await testPath(200, '/', /hello/m);
|
|
await testPath(404, '/favicon.txt');
|
|
})
|
|
);
|
|
|
|
test('[vercel dev] render warning for empty cwd dir', async () => {
|
|
const directory = fixture('empty');
|
|
const { dev, port } = await testFixture(directory, {
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
|
|
try {
|
|
dev.unref();
|
|
|
|
// Monitor `stderr` for the warning
|
|
dev.stderr.setEncoding('utf8');
|
|
const msg = 'There are no files inside your deployment.';
|
|
await new Promise<void>(resolve => {
|
|
dev.stderr.on('data', (str: string) => {
|
|
if (str.includes(msg)) {
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
|
|
// Issue a request to ensure a 404 response
|
|
await sleep(ms('3s'));
|
|
const response = await fetch(`http://localhost:${port}`);
|
|
validateResponseHeaders(response);
|
|
expect(response.status).toBe(404);
|
|
} finally {
|
|
dev.kill('SIGTERM');
|
|
}
|
|
});
|
|
|
|
test('[vercel dev] do not rebuild for changes in the output directory', async () => {
|
|
const directory = fixture('output-is-source');
|
|
|
|
const { dev, port } = await testFixture(directory, {
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
|
|
try {
|
|
dev.unref();
|
|
|
|
let stderr: any = [];
|
|
const start = Date.now();
|
|
|
|
dev.stderr.on('data', (str: any) => stderr.push(str));
|
|
|
|
while (stderr.join('').includes('Ready') === false) {
|
|
await sleep(ms('3s'));
|
|
|
|
if (Date.now() - start > ms('30s')) {
|
|
console.log('stderr:', stderr.join(''));
|
|
break;
|
|
}
|
|
}
|
|
|
|
const resp1 = await fetch(`http://localhost:${port}`);
|
|
const text1 = await resp1.text();
|
|
expect(text1.trim()).toBe('hello first');
|
|
|
|
await fs.writeFile(join(directory, 'public', 'index.html'), 'hello second');
|
|
|
|
await sleep(ms('3s'));
|
|
|
|
const resp2 = await fetch(`http://localhost:${port}`);
|
|
const text2 = await resp2.text();
|
|
expect(text2.trim()).toBe('hello second');
|
|
} finally {
|
|
dev.kill('SIGTERM');
|
|
}
|
|
});
|
|
|
|
test(
|
|
'[vercel dev] 25-nextjs-src-dir',
|
|
testFixtureStdio('25-nextjs-src-dir', async (testPath: any) => {
|
|
await testPath(200, '/', /Next.js \+ Node.js API/m);
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] 27-zero-config-env',
|
|
testFixtureStdio(
|
|
'27-zero-config-env',
|
|
async (testPath: any) => {
|
|
await testPath(200, '/api/print', /build-and-runtime/m);
|
|
await testPath(200, '/', /build-and-runtime/m);
|
|
},
|
|
{ skipDeploy: true }
|
|
)
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] 28-vercel-json-and-ignore',
|
|
testFixtureStdio('28-vercel-json-and-ignore', async (testPath: any) => {
|
|
await testPath(200, '/api/one', 'One');
|
|
await testPath(404, '/api/two');
|
|
await testPath(200, '/api/three', 'One');
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] 30-next-image-optimization',
|
|
testFixtureStdio('30-next-image-optimization', async (testPath: any) => {
|
|
const toUrl = (url: any, w: any, q: any) => {
|
|
// @ts-ignore
|
|
const query = new URLSearchParams();
|
|
query.append('url', url);
|
|
query.append('w', w);
|
|
query.append('q', q);
|
|
return `/_next/image?${query}`;
|
|
};
|
|
|
|
const expectHeader = (accept: any) => ({
|
|
'content-type': accept,
|
|
'cache-control': 'public, max-age=0, must-revalidate',
|
|
});
|
|
const fetchOpts = (accept: any) => ({ method: 'GET', headers: { accept } });
|
|
await testPath(200, '/', /Home Page/m);
|
|
await testPath(
|
|
200,
|
|
toUrl('/test.jpg', 64, 100),
|
|
null,
|
|
expectHeader('image/webp'),
|
|
fetchOpts('image/webp')
|
|
);
|
|
await testPath(
|
|
200,
|
|
toUrl('/test.png', 64, 90),
|
|
null,
|
|
expectHeader('image/webp'),
|
|
fetchOpts('image/webp')
|
|
);
|
|
/*
|
|
* Disabled gif in https://github.com/vercel/next.js/pull/22253
|
|
* Eventually we should enable again when `next dev` supports it
|
|
await testPath(
|
|
200,
|
|
toUrl('/test.gif', 64, 80),
|
|
null,
|
|
expectHeader('image/webp'),
|
|
fetchOpts('image/webp')
|
|
);
|
|
*/
|
|
/*
|
|
* Disabled svg in https://github.com/vercel/next.js/pull/34431
|
|
* We can test for 400 status since config option is not enabled.
|
|
*/
|
|
await testPath(400, toUrl('/test.svg', 64, 70));
|
|
/* Disabled bmp because `next dev` bypasses
|
|
* and production will convert. Eventually
|
|
* we can enable once `next dev` supports it.
|
|
await testPath(
|
|
200,
|
|
toUrl('/test.bmp', 64, 50),
|
|
null,
|
|
expectHeader('image/bmp'),
|
|
fetchOpts('image/webp')
|
|
);
|
|
*/
|
|
// animated gif should bypass: serve as-is
|
|
await testPath(
|
|
200,
|
|
toUrl('/animated.gif', 64, 60),
|
|
null,
|
|
expectHeader('image/gif'),
|
|
fetchOpts('image/webp')
|
|
);
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] 40-mixed-modules',
|
|
testFixtureStdio('40-mixed-modules', async (testPath: any) => {
|
|
await testPath(200, '/entrypoint.js', 'mixed-modules:js');
|
|
await testPath(200, '/entrypoint.mjs', 'mixed-modules:mjs');
|
|
await testPath(200, '/entrypoint.ts', 'mixed-modules:ts');
|
|
await testPath(
|
|
200,
|
|
'/type-module-package-json/auto.js',
|
|
'mixed-modules:auto'
|
|
);
|
|
await testPath(
|
|
200,
|
|
'/type-module-package-json/nested/also.js',
|
|
'mixed-modules:also'
|
|
);
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] 41-tsconfig-jsx',
|
|
testFixtureStdio('41-tsconfig-jsx', async (testPath: any) => {
|
|
await testPath(200, '/', /Solid App/m);
|
|
await testPath(200, '/api/test', 'working');
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] 42-dynamic-esm-ext',
|
|
testFixtureStdio('42-dynamic-esm-ext', async (testPath: any) => {
|
|
await testPath(200, '/api/cjs/foo', 'found .js');
|
|
await testPath(200, '/api/esm/foo', 'found .mjs');
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] Use `@vercel/python` with Flask requirements.txt',
|
|
testFixtureStdio('python-flask', async (testPath: any) => {
|
|
const name = 'Alice';
|
|
const year = new Date().getFullYear();
|
|
await testPath(200, `/api/user?name=${name}`, new RegExp(`Hello ${name}`));
|
|
await testPath(200, `/api/date`, new RegExp(`Current date is ${year}`));
|
|
await testPath(200, `/api/date.py`, new RegExp(`Current date is ${year}`));
|
|
await testPath(200, `/api/headers`, (body: any, res: any) => {
|
|
// @ts-ignore
|
|
const { host } = new URL(res.url);
|
|
expect(body).toBe(host);
|
|
});
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] Use custom runtime from the "functions" property',
|
|
testFixtureStdio('custom-runtime', async (testPath: any) => {
|
|
await testPath(200, `/api/user`, /Hello, from Bash!/m);
|
|
await testPath(200, `/api/user.sh`, /Hello, from Bash!/m);
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] Should work with nested `tsconfig.json` files',
|
|
testFixtureStdio('nested-tsconfig', async (testPath: any) => {
|
|
await testPath(200, `/`, /Nested tsconfig.json test page/);
|
|
await testPath(200, `/api`, 'Nested `tsconfig.json` API endpoint');
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] Should force `tsc` option "module: commonjs" for `startDevServer()`',
|
|
testFixtureStdio('force-module-commonjs', async (testPath: any) => {
|
|
await testPath(200, `/`, /Force "module: commonjs" test page/);
|
|
await testPath(
|
|
200,
|
|
`/api`,
|
|
'Force "module: commonjs" JavaScript with ES Modules API endpoint'
|
|
);
|
|
await testPath(
|
|
200,
|
|
`/api/ts`,
|
|
'Force "module: commonjs" TypeScript API endpoint'
|
|
);
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] should prioritize index.html over other file named index.*',
|
|
testFixtureStdio('index-html-priority', async (testPath: any) => {
|
|
await testPath(200, '/', 'This is index.html');
|
|
await testPath(200, '/index.css', 'This is index.css');
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] Should support `*.go` API serverless functions',
|
|
testFixtureStdio('go', async (testPath: any) => {
|
|
await testPath(200, `/api`, 'This is the index page');
|
|
await testPath(200, `/api/index`, 'This is the index page');
|
|
await testPath(200, `/api/index.go`, 'This is the index page');
|
|
await testPath(200, `/api/another`, 'This is another page');
|
|
await testPath(200, '/api/another.go', 'This is another page');
|
|
await testPath(200, `/api/foo`, 'Req Path: /api/foo');
|
|
await testPath(200, `/api/bar`, 'Req Path: /api/bar');
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] Should set the `ts-node` "target" to match Node.js version',
|
|
testFixtureStdio('node-ts-node-target', async (testPath: any) => {
|
|
await testPath(200, `/api/subclass`, '{"ok":true}');
|
|
await testPath(
|
|
200,
|
|
`/api/array`,
|
|
'{"months":[1,2,3,4,5,6,7,8,9,10,11,12]}'
|
|
);
|
|
|
|
await testPath(200, `/api/dump`, (body: any, res: any, isDev: any) => {
|
|
// @ts-ignore
|
|
const { host } = new URL(res.url);
|
|
const { env, headers } = JSON.parse(body);
|
|
|
|
// Test that the API endpoint receives the Vercel proxy request headers
|
|
expect(headers['x-forwarded-host']).toBe(host);
|
|
expect(headers['x-vercel-deployment-url']).toBe(host);
|
|
expect(isIP(headers['x-real-ip'])).toBeTruthy();
|
|
expect(isIP(headers['x-forwarded-for'])).toBeTruthy();
|
|
expect(isIP(headers['x-vercel-forwarded-for'])).toBeTruthy();
|
|
|
|
// Test that the API endpoint has the Vercel platform env vars defined.
|
|
expect(env.NOW_REGION).toMatch(/^[a-z]{3}\d$/);
|
|
if (isDev) {
|
|
// Only dev is tested because in production these are opt-in.
|
|
expect(env.VERCEL_URL).toBe(host);
|
|
expect(env.VERCEL_REGION).toBe('dev1');
|
|
}
|
|
});
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] Do not fail if `src` is missing',
|
|
testFixtureStdio('missing-src-property', async (testPath: any) => {
|
|
await testPath(200, '/', /hello:index.txt/m);
|
|
await testPath(404, '/i-do-not-exist');
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] Middleware that returns a 200 response',
|
|
testFixtureStdio('middleware-response', async (testPath: any) => {
|
|
await testPath(200, '/', 'hi from middleware');
|
|
await testPath(200, '/another', 'hi from middleware');
|
|
})
|
|
);
|
|
|
|
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) => {
|
|
await testPath(200, '/', '<h1>Index</h1>');
|
|
await testPath(200, '/index', '<h1>Another</h1>');
|
|
await testPath(200, '/another', '<h1>Another</h1>');
|
|
await testPath(200, '/another.html', '<h1>Another</h1>');
|
|
await testPath(200, '/foo', '<h1>Another</h1>');
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] Middleware that rewrites with custom query params',
|
|
testFixtureStdio('middleware-rewrite-query', async (testPath: any) => {
|
|
await testPath(200, '/?foo=bar', '{"url":"/?from-middleware=true"}');
|
|
await testPath(
|
|
200,
|
|
'/another?foo=bar',
|
|
'{"url":"/another?from-middleware=true"}'
|
|
);
|
|
await testPath(
|
|
200,
|
|
'/api/fn?foo=bar',
|
|
'{"url":"/api/fn?from-middleware=true"}'
|
|
);
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] Middleware that redirects',
|
|
testFixtureStdio('middleware-redirect', async (testPath: any) => {
|
|
await testPath(302, '/', null, {
|
|
location: 'https://vercel.com/',
|
|
});
|
|
await testPath(302, '/home', null, {
|
|
location: 'https://vercel.com/home',
|
|
});
|
|
await testPath(302, '/?foo=bar', null, {
|
|
location: 'https://vercel.com/?foo=bar',
|
|
});
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] Middleware with error in function handler',
|
|
testFixtureStdio('middleware-error-in-handler', async (testPath: any) => {
|
|
await testPath(500, '/', /EDGE_FUNCTION_INVOCATION_FAILED/);
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] Middleware with error at init',
|
|
testFixtureStdio('middleware-error-at-init', async (testPath: any) => {
|
|
await testPath(500, '/', /EDGE_FUNCTION_INVOCATION_FAILED/);
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] Middleware with an explicit 500 response',
|
|
testFixtureStdio('middleware-500-response', async (testPath: any) => {
|
|
await testPath(500, '/', /EDGE_FUNCTION_INVOCATION_FAILED/);
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] Middleware with `matchers` config',
|
|
testFixtureStdio('middleware-matchers', async (testPath: any) => {
|
|
await testPath(404, '/');
|
|
await testPath(404, '/another');
|
|
await testPath(
|
|
200,
|
|
'/about/page',
|
|
'{"pathname":"/about/page","search":"","fromMiddleware":true}'
|
|
);
|
|
await testPath(
|
|
200,
|
|
'/dashboard/home',
|
|
'{"pathname":"/dashboard/home","search":"","fromMiddleware":true}'
|
|
);
|
|
await testPath(
|
|
200,
|
|
'/dashboard/home?a=b',
|
|
'{"pathname":"/dashboard/home","search":"?a=b","fromMiddleware":true}'
|
|
);
|
|
})
|
|
);
|