Skip to content

Commit

Permalink
[tests] add tests to edge handler template (#9528)
Browse files Browse the repository at this point in the history
The Edge Handler wrapping logic we add during `vc dev` was not testable. This PR refactors it to be testable and adds some basic tests.
  • Loading branch information
EndangeredMassa committed Mar 8, 2023
1 parent 765ec05 commit 66fe3db
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 56 deletions.
4 changes: 2 additions & 2 deletions packages/cli/test/dev/integration-1.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,12 +304,12 @@ test('[vercel dev] should handle missing handler errors thrown in edge functions
);
validateResponseHeaders(res);

const { stdout } = await dev.kill();
const { stderr } = await dev.kill();

expect(await res.text()).toMatch(
/<strong>500<\/strong>: INTERNAL_SERVER_ERROR/g
);
expect(stdout).toMatch(
expect(stderr).toMatch(
/No default export was found. Add a default export to handle requests./g
);
} finally {
Expand Down
130 changes: 80 additions & 50 deletions packages/node/src/edge-functions/edge-handler-template.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
// provided by the edge runtime:
/* global addEventListener Request Response */

// provided by our edge handler logic:
/* global IS_MIDDLEWARE ENTRYPOINT_LABEL */
/* global addEventListener */

function buildUrl(requestDetails) {
let proto = requestDetails.headers['x-forwarded-proto'];
Expand All @@ -11,63 +8,96 @@ function buildUrl(requestDetails) {
return `${proto}://${host}${path}`;
}

addEventListener('fetch', async event => {
try {
let serializedRequest = await event.request.text();
let requestDetails = JSON.parse(serializedRequest);
async function respond(
userEdgeHandler,
requestDetails,
event,
options,
dependencies
) {
const { Request, Response } = dependencies;
const { isMiddleware, entrypointLabel } = options;

let body;
let body;

if (requestDetails.method !== 'GET' && requestDetails.method !== 'HEAD') {
if (requestDetails.method !== 'GET' && requestDetails.method !== 'HEAD') {
if (requestDetails.body) {
body = Uint8Array.from(atob(requestDetails.body), c => c.charCodeAt(0));
}
}

let request = new Request(buildUrl(requestDetails), {
headers: requestDetails.headers,
method: requestDetails.method,
body: body,
});
let request = new Request(buildUrl(requestDetails), {
headers: requestDetails.headers,
method: requestDetails.method,
body: body,
});

event.request = request;
event.request = request;

let edgeHandler = module.exports.default;
if (!edgeHandler) {
let response = await userEdgeHandler(event.request, event);

if (!response) {
if (isMiddleware) {
// allow empty responses to pass through
response = new Response(null, {
headers: {
'x-middleware-next': '1',
},
});
} else {
throw new Error(
'No default export was found. Add a default export to handle requests. Learn more: https://vercel.link/creating-edge-middleware'
`Edge Function "${entrypointLabel}" did not return a response.`
);
}
}
return response;
}

let response = await edgeHandler(event.request, event);
function toResponseError(error, Response) {
// we can't easily show a meaningful stack trace
// so, stick to just the error message for now
const msg = error.cause
? error.message + ': ' + (error.cause.message || error.cause)
: error.message;
return new Response(msg, {
status: 500,
headers: {
'x-vercel-failed': 'edge-wrapper',
},
});
}

if (!response) {
if (IS_MIDDLEWARE) {
// allow empty responses to pass through
response = new Response(null, {
headers: {
'x-middleware-next': '1',
},
});
} else {
throw new Error(
`Edge Function "${ENTRYPOINT_LABEL}" did not return a response.`
);
}
async function parseRequestEvent(event) {
let serializedRequest = await event.request.text();
let requestDetails = JSON.parse(serializedRequest);
return requestDetails;
}

// This will be invoked by logic using this template
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function registerFetchListener(userEdgeHandler, options, dependencies) {
addEventListener('fetch', async event => {
try {
let requestDetails = await parseRequestEvent(event);
let response = await respond(
userEdgeHandler,
requestDetails,
event,
options,
dependencies
);
return event.respondWith(response);
} catch (error) {
event.respondWith(toResponseError(error, dependencies.Response));
}
});
}

return event.respondWith(response);
} catch (error) {
// we can't easily show a meaningful stack trace
// so, stick to just the error message for now
const msg = error.cause
? error.message + ': ' + (error.cause.message || error.cause)
: error.message;
event.respondWith(
new Response(msg, {
status: 500,
headers: {
'x-vercel-failed': 'edge-wrapper',
},
})
);
}
});
// for testing:
module.exports = {
buildUrl,
respond,
toResponseError,
parseRequestEvent,
registerFetchListener,
};
21 changes: 18 additions & 3 deletions packages/node/src/edge-functions/edge-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,28 @@ async function compileUserCode(
// user code
${compiledFile.text};
const userEdgeHandler = module.exports.default;
if (!userEdgeHandler) {
throw new Error(
'No default export was found. Add a default export to handle requests. Learn more: https://vercel.link/creating-edge-middleware'
);
}
// request metadata
const IS_MIDDLEWARE = ${isMiddleware};
const ENTRYPOINT_LABEL = '${entrypointRelativePath}';
const isMiddleware = ${isMiddleware};
const entrypointLabel = '${entrypointRelativePath}';
// edge handler
${edgeHandlerTemplate}
${edgeHandlerTemplate};
const dependencies = {
Request,
Response
};
const options = {
isMiddleware,
entrypointLabel
};
registerFetchListener(userEdgeHandler, options, dependencies);
`;

return { userCode, wasmAssets };
Expand Down
2 changes: 1 addition & 1 deletion packages/node/test/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"extends": "../tsconfig.json",
"include": ["*.test.ts"]
"include": ["**/*.test.ts"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Response, Request } from 'node-fetch';
import {
buildUrl,
respond,
// @ts-ignore - this is a special patch file to allow importing from the template
} from '../../../src/edge-functions/edge-handler-template.js';

describe('edge-handler-template', () => {
describe('buildUrl()', () => {
test('works with basic proto', async () => {
const url = buildUrl({
url: '/api/add',
headers: {
'x-forwarded-proto': 'https',
'x-forwarded-host': 'somewhere.com',
},
});
expect(url).toBe('https://somewhere.com/api/add');
});
});

describe('respond()', () => {
test('works', async () => {
const request = {
url: '/api/add',
headers: {
'x-forwarded-proto': 'https',
'x-forwarded-host': 'somewhere.com',
},
};

function userEdgeHandler(req: Request) {
return new Response(`hello from: ${req.url}`);
}

const event = {};
const isMiddleware = false;
const entrypointLabel = 'api/add.js';
const response = await respond(
userEdgeHandler,
request,
event,
{
isMiddleware,
entrypointLabel,
},
{
Request,
Response,
}
);
expect(await response.text()).toBe(
'hello from: https://somewhere.com/api/add'
);
});
});
});

0 comments on commit 66fe3db

Please sign in to comment.