mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-10 04:22:12 +00:00
Middleware server setup wasn't logging errors the same way that dev server setup was. This meant that middleware instantiation errors (like invalid config) would cause requests to 500, but no errors to be logged to the console. This PR updates the invalid config error, makes sure errors in this area are logged out, and adds a test for this behavior. **It may be appropriate to fail the deploy (and crash `vc dev`) in this case instead, though. What do others think?** --- During `vc dev` with middleware that has an invalid `config.matcher` value... Before: You see a 500 response in the browser and no output in the terminal. After: You see a 500 response in the browser and this output in the terminal: ``` Error! Middleware's `config.matcher` values must start with "/". Received: not-a-valid-matcher ``` --- Related Issue: https://github.com/vercel/edge-functions/issues/220
1048 lines
30 KiB
TypeScript
1048 lines
30 KiB
TypeScript
import os from 'os';
|
|
import url from 'url';
|
|
import fs from 'fs-extra';
|
|
import { join } from 'path';
|
|
import listen from 'async-listen';
|
|
import { createServer } from 'http';
|
|
|
|
const {
|
|
exec,
|
|
fetch,
|
|
fixture,
|
|
testFixture,
|
|
testFixtureStdio,
|
|
validateResponseHeaders,
|
|
} = require('./utils.js');
|
|
|
|
test('[vercel dev] should support edge functions', async () => {
|
|
const dir = fixture('edge-function');
|
|
const { dev, port, readyResolver } = await testFixture(dir, {
|
|
env: {
|
|
ENV_VAR_IN_EDGE: '1',
|
|
},
|
|
});
|
|
|
|
try {
|
|
await readyResolver;
|
|
|
|
const body = { hello: 'world' };
|
|
|
|
let res = await fetch(`http://localhost:${port}/api/edge-success`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
},
|
|
body: JSON.stringify(body),
|
|
});
|
|
validateResponseHeaders(res);
|
|
|
|
// support for edge functions has to manually ensure that these properties
|
|
// are set up; so, we test that they are all passed through properly
|
|
expect(await res.json()).toMatchObject({
|
|
headerContentType: 'application/json',
|
|
url: `http://localhost:${port}/api/edge-success`,
|
|
method: 'POST',
|
|
body: '{"hello":"world"}',
|
|
snakeCase: 'some_camel_case_thing',
|
|
upperCase: 'SOMETHING',
|
|
optionalChaining: 'fallback',
|
|
ENV_VAR_IN_EDGE: '1',
|
|
});
|
|
} finally {
|
|
await dev.kill('SIGTERM');
|
|
}
|
|
});
|
|
|
|
test(
|
|
'[vercel dev] edge functions respond properly the same as production',
|
|
testFixtureStdio('edge-function', async (testPath: any) => {
|
|
await testPath(500, '/api/edge-500-response');
|
|
await testPath(200, '/api/edge-success');
|
|
})
|
|
);
|
|
|
|
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);
|
|
|
|
try {
|
|
await readyResolver;
|
|
|
|
const body = { hello: 'world' };
|
|
|
|
let res = await fetch(`http://localhost:${port}/api/edge-500-response`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
},
|
|
body: JSON.stringify(body),
|
|
});
|
|
validateResponseHeaders(res);
|
|
|
|
expect(await res.status).toBe(500);
|
|
expect(await res.text()).toBe(
|
|
'responding with intentional 500 from user code'
|
|
);
|
|
} finally {
|
|
await dev.kill('SIGTERM');
|
|
}
|
|
});
|
|
|
|
test('[vercel dev] should handle runtime errors thrown in edge functions', 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-error-runtime`, {
|
|
method: 'GET',
|
|
headers: {
|
|
Accept:
|
|
'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
},
|
|
});
|
|
validateResponseHeaders(res);
|
|
|
|
const { stdout, stderr } = await dev.kill('SIGTERM');
|
|
|
|
expect(await res.text()).toMatch(
|
|
/<strong>500<\/strong>: INTERNAL_SERVER_ERROR/g
|
|
);
|
|
expect(stdout).toMatch(/Unhandled rejection: intentional runtime error/g);
|
|
expect(stderr).toMatch(
|
|
/Failed to complete request to \/api\/edge-error-runtime: Error: socket hang up/g
|
|
);
|
|
} finally {
|
|
await dev.kill('SIGTERM');
|
|
}
|
|
});
|
|
|
|
test('[vercel dev] should handle config errors thrown in edge functions', 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-error-config`, {
|
|
method: 'GET',
|
|
headers: {
|
|
Accept:
|
|
'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
},
|
|
});
|
|
validateResponseHeaders(res);
|
|
|
|
const { stderr } = await dev.kill('SIGTERM');
|
|
|
|
expect(await res.text()).toMatch(
|
|
/<strong>500<\/strong>: INTERNAL_SERVER_ERROR/g
|
|
);
|
|
expect(stderr).toMatch(
|
|
/Invalid function runtime "invalid-runtime-value" for "api\/edge-error-config.js". Valid runtimes are: \["experimental-edge"\]/g
|
|
);
|
|
expect(stderr).toMatch(
|
|
/Failed to complete request to \/api\/edge-error-config: Error: socket hang up/g
|
|
);
|
|
} finally {
|
|
await dev.kill('SIGTERM');
|
|
}
|
|
});
|
|
|
|
test('[vercel dev] should handle startup errors thrown in edge functions', 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-error-startup`, {
|
|
method: 'GET',
|
|
headers: {
|
|
Accept:
|
|
'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
},
|
|
});
|
|
validateResponseHeaders(res);
|
|
|
|
const { stderr } = await dev.kill('SIGTERM');
|
|
|
|
expect(await res.text()).toMatch(
|
|
/<strong>500<\/strong>: INTERNAL_SERVER_ERROR/g
|
|
);
|
|
expect(stderr).toMatch(/Failed to instantiate edge runtime./g);
|
|
expect(stderr).toMatch(/intentional startup error/g);
|
|
expect(stderr).toMatch(
|
|
/Failed to complete request to \/api\/edge-error-startup: Error: socket hang up/g
|
|
);
|
|
} finally {
|
|
await dev.kill('SIGTERM');
|
|
}
|
|
});
|
|
|
|
test('[vercel dev] should handle syntax errors thrown in edge functions', 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-error-syntax`, {
|
|
method: 'GET',
|
|
headers: {
|
|
Accept:
|
|
'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
},
|
|
});
|
|
validateResponseHeaders(res);
|
|
|
|
const { stderr } = await dev.kill('SIGTERM');
|
|
|
|
expect(await res.text()).toMatch(
|
|
/<strong>500<\/strong>: INTERNAL_SERVER_ERROR/g
|
|
);
|
|
expect(stderr).toMatch(/Failed to compile user code for edge runtime./g);
|
|
expect(stderr).toMatch(/Unexpected end of file/g);
|
|
expect(stderr).toMatch(
|
|
/Failed to complete request to \/api\/edge-error-syntax: Error: socket hang up/g
|
|
);
|
|
} finally {
|
|
await dev.kill('SIGTERM');
|
|
}
|
|
});
|
|
|
|
test('[vercel dev] should handle import errors thrown in edge functions', 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-error-unknown-import`,
|
|
{
|
|
method: 'GET',
|
|
headers: {
|
|
Accept:
|
|
'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
},
|
|
}
|
|
);
|
|
validateResponseHeaders(res);
|
|
|
|
const { stderr } = await dev.kill('SIGTERM');
|
|
|
|
expect(await res.text()).toMatch(
|
|
/<strong>500<\/strong>: INTERNAL_SERVER_ERROR/g
|
|
);
|
|
expect(stderr).toMatch(
|
|
/Could not resolve "unknown-module-893427589372458934795843"/g
|
|
);
|
|
expect(stderr).toMatch(
|
|
/Failed to complete request to \/api\/edge-error-unknown-import: Error: socket hang up/g
|
|
);
|
|
} finally {
|
|
await dev.kill('SIGTERM');
|
|
}
|
|
});
|
|
|
|
test('[vercel dev] should handle missing handler errors thrown in edge functions', 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-error-no-handler`,
|
|
{
|
|
method: 'GET',
|
|
headers: {
|
|
Accept:
|
|
'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
},
|
|
}
|
|
);
|
|
validateResponseHeaders(res);
|
|
|
|
const { stdout, stderr } = await dev.kill('SIGTERM');
|
|
|
|
expect(await res.text()).toMatch(
|
|
/<strong>500<\/strong>: INTERNAL_SERVER_ERROR/g
|
|
);
|
|
expect(stdout).toMatch(
|
|
/No default export was found. Add a default export to handle requests./g
|
|
);
|
|
expect(stderr).toMatch(
|
|
/Failed to complete request to \/api\/edge-error-no-handler: Error: socket hang up/g
|
|
);
|
|
} finally {
|
|
await dev.kill('SIGTERM');
|
|
}
|
|
});
|
|
|
|
test('[vercel dev] should handle invalid middleware config', async () => {
|
|
const dir = fixture('middleware-matchers-invalid');
|
|
const { dev, port, readyResolver } = await testFixture(dir);
|
|
|
|
try {
|
|
await readyResolver;
|
|
|
|
let res = await fetch(`http://localhost:${port}/api/whatever`, {
|
|
method: 'GET',
|
|
headers: {
|
|
Accept:
|
|
'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
},
|
|
});
|
|
validateResponseHeaders(res);
|
|
|
|
const { stderr } = await dev.kill('SIGTERM');
|
|
|
|
expect(await res.text()).toMatch(
|
|
/<strong>500<\/strong>: INTERNAL_SERVER_ERROR/g
|
|
);
|
|
expect(stderr).toMatch(
|
|
/Middleware's `config.matcher` .+ Received: not-a-valid-matcher/g
|
|
);
|
|
} finally {
|
|
await dev.kill('SIGTERM');
|
|
}
|
|
});
|
|
|
|
test('[vercel dev] should support request body', async () => {
|
|
const dir = fixture('node-request-body');
|
|
const { dev, port, readyResolver } = await testFixture(dir);
|
|
|
|
try {
|
|
await readyResolver;
|
|
|
|
const body = { hello: 'world' };
|
|
|
|
// Test that `req.body` works in dev
|
|
let res = await fetch(`http://localhost:${port}/api/req-body`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
},
|
|
body: JSON.stringify(body),
|
|
});
|
|
validateResponseHeaders(res);
|
|
expect(await res.json()).toMatchObject(body);
|
|
|
|
// Test that `req` "data" events work in dev
|
|
res = await fetch(`http://localhost:${port}/api/data-events`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
},
|
|
body: JSON.stringify(body),
|
|
});
|
|
} finally {
|
|
dev.kill('SIGTERM');
|
|
}
|
|
});
|
|
|
|
test('[vercel dev] should maintain query when invoking serverless function', async () => {
|
|
const dir = fixture('node-query-invoke');
|
|
const { dev, port, readyResolver } = await testFixture(dir);
|
|
|
|
try {
|
|
await readyResolver;
|
|
|
|
const res = await fetch(`http://localhost:${port}/something?url-param=a`);
|
|
validateResponseHeaders(res);
|
|
|
|
const text = await res.text();
|
|
const parsed = url.parse(text, true);
|
|
expect(parsed.pathname).toEqual('/something');
|
|
expect(parsed.query['url-param']).toEqual('a');
|
|
expect(parsed.query['route-param']).toEqual('b');
|
|
} finally {
|
|
dev.kill('SIGTERM');
|
|
}
|
|
});
|
|
|
|
test('[vercel dev] should maintain query when proxy passing', async () => {
|
|
const dir = fixture('query-proxy');
|
|
const { dev, port, readyResolver } = await testFixture(dir);
|
|
const dest = createServer((req, res) => {
|
|
res.end(req.url);
|
|
});
|
|
|
|
try {
|
|
await Promise.all([readyResolver, listen(dest, 0)]);
|
|
|
|
const destAddr = dest.address();
|
|
if (!destAddr || typeof destAddr === 'string') {
|
|
throw new Error('Unexpected HTTP address');
|
|
}
|
|
|
|
const res = await fetch(
|
|
`http://localhost:${port}/${destAddr.port}?url-param=a`
|
|
);
|
|
validateResponseHeaders(res);
|
|
|
|
const text = await res.text();
|
|
const parsed = url.parse(text, true);
|
|
expect(parsed.pathname).toEqual('/something');
|
|
expect(parsed.query['url-param']).toEqual('a');
|
|
expect(parsed.query['route-param']).toEqual('b');
|
|
} finally {
|
|
dest.close();
|
|
dev.kill('SIGTERM');
|
|
}
|
|
});
|
|
|
|
test('[vercel dev] should maintain query when dev server defines routes', async () => {
|
|
const dir = fixture('dev-server-query');
|
|
const { dev, port, readyResolver } = await testFixture(dir, {
|
|
env: {
|
|
VERCEL_DEV_COMMAND: 'next dev --port $PORT',
|
|
},
|
|
});
|
|
|
|
try {
|
|
await readyResolver;
|
|
|
|
const res = await fetch(`http://localhost:${port}/test?url-param=a`);
|
|
validateResponseHeaders(res);
|
|
|
|
const text = await res.text();
|
|
|
|
// Hacky way of getting the page payload from the response
|
|
// HTML since we don't have a HTML parser handy.
|
|
const json = text
|
|
.match(/<pre>(.*)<\/pre>/)![1]
|
|
.replace('</pre>', '')
|
|
.replace('<!-- -->', '')
|
|
.replace(/&/g, '&')
|
|
.replace(/"/g, '"');
|
|
const parsed = JSON.parse(json);
|
|
const query = url.parse(parsed.url, true).query;
|
|
|
|
expect(query['url-param']).toEqual('a');
|
|
expect(query['route-param']).toEqual('b');
|
|
} finally {
|
|
dev.kill('SIGTERM');
|
|
}
|
|
});
|
|
|
|
test('[vercel dev] should allow `cache-control` to be overwritten', async () => {
|
|
const dir = fixture('headers');
|
|
const { dev, port, readyResolver } = await testFixture(dir);
|
|
|
|
try {
|
|
await readyResolver;
|
|
|
|
const res = await fetch(
|
|
`http://localhost:${port}/?name=cache-control&value=immutable`
|
|
);
|
|
expect(res.headers.get('cache-control')).toEqual('immutable');
|
|
} finally {
|
|
dev.kill('SIGTERM');
|
|
}
|
|
});
|
|
|
|
test('[vercel dev] should send `etag` header for static files', async () => {
|
|
const dir = fixture('headers');
|
|
const { dev, port, readyResolver } = await testFixture(dir);
|
|
|
|
try {
|
|
await readyResolver;
|
|
|
|
const res = await fetch(`http://localhost:${port}/foo.txt`);
|
|
const expected = 'd263af8ab880c0b97eb6c5c125b5d44f9e5addd9';
|
|
expect(res.headers.get('etag')).toEqual(`"${expected}"`);
|
|
const body = await res.text();
|
|
expect(body.trim()).toEqual('hi');
|
|
} finally {
|
|
dev.kill('SIGTERM');
|
|
}
|
|
});
|
|
|
|
test('[vercel dev] should frontend dev server and routes', async () => {
|
|
const dir = fixture('dev-server-and-routes');
|
|
const { dev, port, readyResolver } = await testFixture(dir, {
|
|
env: {
|
|
VERCEL_DEV_COMMAND: 'next dev --port $PORT',
|
|
},
|
|
});
|
|
|
|
try {
|
|
await readyResolver;
|
|
|
|
let podId: string;
|
|
|
|
let res = await fetch(`http://localhost:${port}/`);
|
|
validateResponseHeaders(res);
|
|
podId = res.headers.get('x-vercel-id')!.match(/:(\w+)-/)![1];
|
|
let body = await res.text();
|
|
expect(body.includes('hello, this is the frontend')).toBeTruthy();
|
|
|
|
res = await fetch(`http://localhost:${port}/api/users`);
|
|
validateResponseHeaders(res, podId);
|
|
body = await res.text();
|
|
expect(body).toEqual('users');
|
|
|
|
res = await fetch(`http://localhost:${port}/api/users/1`);
|
|
validateResponseHeaders(res, podId);
|
|
body = await res.text();
|
|
expect(body).toEqual('users/1');
|
|
|
|
res = await fetch(`http://localhost:${port}/api/welcome`);
|
|
validateResponseHeaders(res, podId);
|
|
body = await res.text();
|
|
expect(body).toEqual('hello and welcome');
|
|
} finally {
|
|
dev.kill('SIGTERM');
|
|
}
|
|
});
|
|
|
|
test('[vercel dev] should support `@vercel/static` routing', async () => {
|
|
const dir = fixture('static-routes');
|
|
const { dev, port, readyResolver } = await testFixture(dir);
|
|
|
|
try {
|
|
await readyResolver;
|
|
|
|
const res = await fetch(`http://localhost:${port}/`);
|
|
expect(res.status).toEqual(200);
|
|
const body = await res.text();
|
|
expect(body.trim()).toEqual('<body>Hello!</body>');
|
|
} finally {
|
|
dev.kill('SIGTERM');
|
|
}
|
|
});
|
|
|
|
test('[vercel dev] should support `@vercel/static-build` routing', async () => {
|
|
const dir = fixture('static-build-routing');
|
|
const { dev, port, readyResolver } = await testFixture(dir);
|
|
|
|
try {
|
|
await readyResolver;
|
|
|
|
const res = await fetch(`http://localhost:${port}/api/date`);
|
|
expect(res.status).toEqual(200);
|
|
const body = await res.text();
|
|
expect(body.startsWith('The current date:')).toBeTruthy();
|
|
} finally {
|
|
dev.kill('SIGTERM');
|
|
}
|
|
});
|
|
|
|
test('[vercel dev] should support directory listing', async () => {
|
|
const dir = fixture('directory-listing');
|
|
const { dev, port, readyResolver } = await testFixture(dir);
|
|
|
|
try {
|
|
await readyResolver;
|
|
|
|
// Get directory listing
|
|
let res = await fetch(`http://localhost:${port}/`);
|
|
let body = await res.text();
|
|
expect(res.status).toEqual(200);
|
|
expect(body.includes('Index of')).toBeTruthy();
|
|
|
|
// Get a file
|
|
res = await fetch(`http://localhost:${port}/file.txt`);
|
|
body = await res.text();
|
|
expect(res.status).toEqual(200);
|
|
expect(body.trim()).toEqual('Hello from file!');
|
|
|
|
// Invoke a lambda
|
|
res = await fetch(`http://localhost:${port}/lambda.js`);
|
|
body = await res.text();
|
|
expect(res.status).toEqual(200);
|
|
expect(body).toEqual('Hello from Lambda!');
|
|
|
|
// Trigger a 404
|
|
res = await fetch(`http://localhost:${port}/does-not-exist`);
|
|
expect(res.status).toEqual(404);
|
|
} finally {
|
|
dev.kill('SIGTERM');
|
|
}
|
|
});
|
|
|
|
test('[vercel dev] should respond with 404 listing with Accept header support', async () => {
|
|
const dir = fixture('directory-listing');
|
|
const { dev, port, readyResolver } = await testFixture(dir);
|
|
|
|
try {
|
|
await readyResolver;
|
|
|
|
// HTML response
|
|
let res = await fetch(`http://localhost:${port}/does-not-exist`, {
|
|
headers: {
|
|
Accept: 'text/html',
|
|
},
|
|
});
|
|
expect(res.status).toEqual(404);
|
|
expect(res.headers.get('content-type')).toEqual('text/html; charset=utf-8');
|
|
let body = await res.text();
|
|
expect(body.startsWith('<!DOCTYPE html>')).toBeTruthy();
|
|
|
|
// JSON response
|
|
res = await fetch(`http://localhost:${port}/does-not-exist`, {
|
|
headers: {
|
|
Accept: 'application/json',
|
|
},
|
|
});
|
|
expect(res.status).toEqual(404);
|
|
expect(res.headers.get('content-type')).toEqual('application/json');
|
|
body = await res.text();
|
|
expect(body).toEqual(
|
|
'{"error":{"code":404,"message":"The page could not be found."}}\n'
|
|
);
|
|
|
|
// Plain text response
|
|
res = await fetch(`http://localhost:${port}/does-not-exist`);
|
|
expect(res.status).toEqual(404);
|
|
body = await res.text();
|
|
expect(res.headers.get('content-type')).toEqual(
|
|
'text/plain; charset=utf-8'
|
|
);
|
|
expect(body).toEqual('The page could not be found.\n\nNOT_FOUND\n');
|
|
} finally {
|
|
dev.kill('SIGTERM');
|
|
}
|
|
});
|
|
|
|
test('[vercel dev] should support `public` directory with zero config', async () => {
|
|
const dir = fixture('api-with-public');
|
|
const { dev, port, readyResolver } = await testFixture(dir);
|
|
|
|
try {
|
|
await readyResolver;
|
|
|
|
let res = await fetch(`http://localhost:${port}/api/user`);
|
|
let body = await res.text();
|
|
expect(body).toEqual('hello:user');
|
|
|
|
res = await fetch(`http://localhost:${port}/`);
|
|
body = await res.text();
|
|
expect(body.startsWith('<h1>hello world</h1>')).toBeTruthy();
|
|
} finally {
|
|
dev.kill('SIGTERM');
|
|
}
|
|
});
|
|
|
|
test('[vercel dev] should support static files with zero config', async () => {
|
|
const dir = fixture('api-with-static');
|
|
const { dev, port, readyResolver } = await testFixture(dir);
|
|
|
|
try {
|
|
await readyResolver;
|
|
|
|
let res = await fetch(`http://localhost:${port}/api/user`);
|
|
let body = await res.text();
|
|
expect(body).toEqual('bye:user');
|
|
|
|
res = await fetch(`http://localhost:${port}/`);
|
|
body = await res.text();
|
|
expect(body.startsWith('<h1>goodbye world</h1>')).toBeTruthy();
|
|
} finally {
|
|
dev.kill('SIGTERM');
|
|
}
|
|
});
|
|
|
|
test('[vercel dev] should support custom 404 routes', async () => {
|
|
const dir = fixture('custom-404');
|
|
const { dev, port, readyResolver } = await testFixture(dir);
|
|
|
|
try {
|
|
await readyResolver;
|
|
|
|
// Test custom 404 with static dest
|
|
let res = await fetch(`http://localhost:${port}/error.html`);
|
|
expect(res.status).toEqual(404);
|
|
let body = await res.text();
|
|
expect(body.trim()).toEqual('<div>Custom 404 page</div>');
|
|
|
|
// Test custom 404 with lambda dest
|
|
res = await fetch(`http://localhost:${port}/error.js`);
|
|
expect(res.status).toEqual(404);
|
|
body = await res.text();
|
|
expect(body).toEqual('Custom 404 Lambda\n');
|
|
|
|
// Test regular 404 still works
|
|
res = await fetch(`http://localhost:${port}/does-not-exist`);
|
|
expect(res.status).toEqual(404);
|
|
body = await res.text();
|
|
expect(body).toEqual('The page could not be found.\n\nNOT_FOUND\n');
|
|
} finally {
|
|
dev.kill('SIGTERM');
|
|
}
|
|
});
|
|
|
|
test('[vercel dev] prints `npm install` errors', async () => {
|
|
const dir = fixture('runtime-not-installed');
|
|
const result = await exec(dir);
|
|
expect(result.stderr.includes('npm ERR! 404')).toBeTruthy();
|
|
expect(
|
|
result.stderr.includes('Failed to install `vercel dev` dependencies')
|
|
).toBeTruthy();
|
|
expect(
|
|
result.stderr.includes('https://vercel.link/npm-install-failed-dev')
|
|
).toBeTruthy();
|
|
});
|
|
|
|
test('[vercel dev] `vercel.json` should be invalidated if deleted', async () => {
|
|
const dir = fixture('invalidate-vercel-config');
|
|
const configPath = join(dir, 'vercel.json');
|
|
const originalConfig = await fs.readJSON(configPath);
|
|
const { dev, port, readyResolver } = await testFixture(dir);
|
|
|
|
try {
|
|
await readyResolver;
|
|
|
|
{
|
|
// Env var should be set from `vercel.json`
|
|
const res = await fetch(`http://localhost:${port}/api`);
|
|
const body = await res.json();
|
|
expect(body.FOO).toBe('bar');
|
|
}
|
|
|
|
{
|
|
// Env var should not be set after `vercel.json` is deleted
|
|
await fs.remove(configPath);
|
|
|
|
const res = await fetch(`http://localhost:${port}/api`);
|
|
const body = await res.json();
|
|
expect(body.FOO).toBe(undefined);
|
|
}
|
|
} finally {
|
|
dev.kill('SIGTERM');
|
|
await fs.writeJSON(configPath, originalConfig);
|
|
}
|
|
});
|
|
|
|
test('[vercel dev] reflects changes to config and env without restart', async () => {
|
|
const dir = fixture('node-helpers');
|
|
const configPath = join(dir, 'vercel.json');
|
|
const originalConfig = await fs.readJSON(configPath);
|
|
const { dev, port, readyResolver } = await testFixture(dir);
|
|
|
|
try {
|
|
await readyResolver;
|
|
|
|
{
|
|
// Node.js helpers should be available by default
|
|
const res = await fetch(`http://localhost:${port}/?foo=bar`);
|
|
const body = await res.json();
|
|
expect(body.hasHelpers).toBe(true);
|
|
expect(body.query.foo).toBe('bar');
|
|
}
|
|
|
|
{
|
|
// Disable the helpers via `config.helpers = false`
|
|
const config = {
|
|
...originalConfig,
|
|
builds: [
|
|
{
|
|
...originalConfig.builds[0],
|
|
config: {
|
|
helpers: false,
|
|
},
|
|
},
|
|
],
|
|
};
|
|
await fs.writeJSON(configPath, config);
|
|
|
|
const res = await fetch(`http://localhost:${port}/?foo=bar`);
|
|
const body = await res.json();
|
|
expect(body.hasHelpers).toBe(false);
|
|
expect(body.query).toBe(undefined);
|
|
}
|
|
|
|
{
|
|
// Enable the helpers via `config.helpers = true`
|
|
const config = {
|
|
...originalConfig,
|
|
builds: [
|
|
{
|
|
...originalConfig.builds[0],
|
|
config: {
|
|
helpers: true,
|
|
},
|
|
},
|
|
],
|
|
};
|
|
await fs.writeJSON(configPath, config);
|
|
|
|
const res = await fetch(`http://localhost:${port}/?foo=baz`);
|
|
const body = await res.json();
|
|
expect(body.hasHelpers).toBe(true);
|
|
expect(body.query.foo).toBe('baz');
|
|
}
|
|
|
|
{
|
|
// Disable the helpers via `NODEJS_HELPERS = '0'`
|
|
const config = {
|
|
...originalConfig,
|
|
build: {
|
|
env: {
|
|
NODEJS_HELPERS: '0',
|
|
},
|
|
},
|
|
};
|
|
await fs.writeJSON(configPath, config);
|
|
|
|
const res = await fetch(`http://localhost:${port}/?foo=baz`);
|
|
const body = await res.json();
|
|
expect(body.hasHelpers).toBe(false);
|
|
expect(body.query).toBe(undefined);
|
|
}
|
|
|
|
{
|
|
// Enable the helpers via `NODEJS_HELPERS = '1'`
|
|
const config = {
|
|
...originalConfig,
|
|
build: {
|
|
env: {
|
|
NODEJS_HELPERS: '1',
|
|
},
|
|
},
|
|
};
|
|
await fs.writeJSON(configPath, config);
|
|
|
|
const res = await fetch(`http://localhost:${port}/?foo=boo`);
|
|
const body = await res.json();
|
|
expect(body.hasHelpers).toBe(true);
|
|
expect(body.query.foo).toBe('boo');
|
|
}
|
|
} finally {
|
|
dev.kill('SIGTERM');
|
|
await fs.writeJSON(configPath, originalConfig);
|
|
}
|
|
});
|
|
|
|
test('[vercel dev] `@vercel/node` TypeScript should be resolved by default', async () => {
|
|
// The purpose of this test is to test that `@vercel/node` can properly
|
|
// resolve the default "typescript" module when the project doesn't include
|
|
// its own version. To properly test for this, a fixture needs to be created
|
|
// *outside* of the `vercel` repo, since otherwise the root-level
|
|
// "node_modules/typescript" is resolved as relative to the project, and
|
|
// not relative to `@vercel/node` which is what we are testing for here.
|
|
const dir = join(os.tmpdir(), 'vercel-node-typescript-resolve-test');
|
|
const apiDir = join(dir, 'api');
|
|
await fs.mkdirp(apiDir);
|
|
await fs.writeFile(
|
|
join(apiDir, 'hello.js'),
|
|
'export default (req, res) => { res.end("world"); }'
|
|
);
|
|
|
|
const { dev, port, readyResolver } = await testFixture(dir);
|
|
|
|
try {
|
|
await readyResolver;
|
|
|
|
const res = await fetch(`http://localhost:${port}/api/hello`);
|
|
const body = await res.text();
|
|
expect(body).toBe('world');
|
|
} finally {
|
|
dev.kill('SIGTERM');
|
|
await fs.remove(dir);
|
|
}
|
|
});
|
|
|
|
test(
|
|
'[vercel dev] validate routes that use `check: true`',
|
|
testFixtureStdio('routes-check-true', async (testPath: any) => {
|
|
await testPath(200, '/blog/post', 'Blog Home');
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] validate routes that use `check: true` and `status` code',
|
|
testFixtureStdio('routes-check-true-status', async (testPath: any) => {
|
|
await testPath(403, '/secret');
|
|
await testPath(200, '/post', 'This is a post.');
|
|
await testPath(200, '/post.html', 'This is a post.');
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] validate routes that use custom 404 page',
|
|
testFixtureStdio('routes-custom-404', async (testPath: any) => {
|
|
await testPath(200, '/', 'Home Page');
|
|
await testPath(404, '/nothing', 'Custom User 404');
|
|
await testPath(404, '/exact', 'Exact Custom 404');
|
|
await testPath(200, '/api/hello', 'Hello');
|
|
await testPath(404, '/api/nothing', 'Custom User 404');
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] handles miss after route',
|
|
testFixtureStdio('handle-miss-after-route', async (testPath: any) => {
|
|
await testPath(200, '/post', 'Blog Post Page', {
|
|
test: '1',
|
|
override: 'one',
|
|
});
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] handles miss after rewrite',
|
|
testFixtureStdio('handle-miss-after-rewrite', async (testPath: any) => {
|
|
await testPath(200, '/post', 'Blog Post Page', {
|
|
test: '1',
|
|
override: 'one',
|
|
});
|
|
await testPath(200, '/blog/post', 'Blog Post Page', {
|
|
test: '1',
|
|
override: 'two',
|
|
});
|
|
await testPath(404, '/blog/about.html', undefined, {
|
|
test: '1',
|
|
override: 'two',
|
|
});
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] does not display directory listing after 404',
|
|
testFixtureStdio('handle-miss-hide-dir-list', async (testPath: any) => {
|
|
await testPath(404, '/post');
|
|
await testPath(200, '/post/one.html', 'First Post');
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] should preserve query string even after miss phase',
|
|
testFixtureStdio('handle-miss-querystring', async (testPath: any) => {
|
|
await testPath(200, '/', 'Index Page');
|
|
if (process.env.CI && process.platform === 'darwin') {
|
|
console.log('Skipping since GH Actions hangs for some reason');
|
|
} else {
|
|
await testPath(200, '/echo/first/second', 'a=first,b=second');
|
|
await testPath(200, '/functions/echo.js?a=one&b=two', 'a=one,b=two');
|
|
}
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] handles hit after handle: filesystem',
|
|
testFixtureStdio('handle-hit-after-fs', async (testPath: any) => {
|
|
await testPath(200, '/blog.html', 'Blog Page', { test: '1' });
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] handles hit after dest',
|
|
testFixtureStdio('handle-hit-after-dest', async (testPath: any) => {
|
|
await testPath(200, '/post', 'Blog Post', { test: '1', override: 'one' });
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] handles hit after rewrite',
|
|
testFixtureStdio('handle-hit-after-rewrite', async (testPath: any) => {
|
|
await testPath(200, '/post', 'Blog Post', { test: '1', override: 'one' });
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] should serve the public directory and api functions',
|
|
testFixtureStdio('public-and-api', async (testPath: any) => {
|
|
await testPath(200, '/', 'This is the home page');
|
|
await testPath(200, '/about.html', 'This is the about page');
|
|
await testPath(200, '/.well-known/humans.txt', 'We come in peace');
|
|
await testPath(200, '/api/date', /current date/);
|
|
await testPath(200, '/api/rand', /random number/);
|
|
await testPath(200, '/api/rand.js', /random number/);
|
|
await testPath(404, '/api/api', /NOT_FOUND/m);
|
|
await testPath(404, '/nothing', /Custom 404 Page/);
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] should allow user rewrites for path segment files',
|
|
testFixtureStdio('test-zero-config-rewrite', async (testPath: any) => {
|
|
await testPath(404, '/');
|
|
await testPath(200, '/echo/1', '{"id":"1"}', {
|
|
'Access-Control-Allow-Origin': '*',
|
|
});
|
|
await testPath(200, '/echo/2', '{"id":"2"}', {
|
|
'Access-Control-Allow-Headers': '*',
|
|
});
|
|
})
|
|
);
|
|
|
|
test('[vercel dev] validate builds', async () => {
|
|
const directory = fixture('invalid-builds');
|
|
const output = await exec(directory);
|
|
|
|
expect(output.exitCode).toBe(1);
|
|
expect(output.stderr).toMatch(
|
|
/Invalid vercel\.json - `builds\[0\].src` should be string/m
|
|
);
|
|
});
|
|
|
|
test('[vercel dev] validate routes', async () => {
|
|
const directory = fixture('invalid-routes');
|
|
const output = await exec(directory);
|
|
|
|
expect(output.exitCode).toBe(1);
|
|
expect(output.stderr).toMatch(
|
|
/Invalid vercel\.json - `routes\[0\].src` should be string/m
|
|
);
|
|
});
|
|
|
|
test('[vercel dev] validate cleanUrls', async () => {
|
|
const directory = fixture('invalid-clean-urls');
|
|
const output = await exec(directory);
|
|
|
|
expect(output.exitCode).toBe(1);
|
|
expect(output.stderr).toMatch(
|
|
/Invalid vercel\.json - `cleanUrls` should be boolean/m
|
|
);
|
|
});
|
|
|
|
test('[vercel dev] validate trailingSlash', async () => {
|
|
const directory = fixture('invalid-trailing-slash');
|
|
const output = await exec(directory);
|
|
|
|
expect(output.exitCode).toBe(1);
|
|
expect(output.stderr).toMatch(
|
|
/Invalid vercel\.json - `trailingSlash` should be boolean/m
|
|
);
|
|
});
|
|
|
|
test('[vercel dev] validate rewrites', async () => {
|
|
const directory = fixture('invalid-rewrites');
|
|
const output = await exec(directory);
|
|
|
|
expect(output.exitCode).toBe(1);
|
|
expect(output.stderr).toMatch(
|
|
/Invalid vercel\.json - `rewrites\[0\].destination` should be string/m
|
|
);
|
|
});
|