Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(handler): ability to override default on request event #2939

Merged
merged 6 commits into from Jan 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
132 changes: 132 additions & 0 deletions packages/handler/__tests__/onRequest.test.ts
@@ -0,0 +1,132 @@
import { createHandler } from "~/fastify";
import { createRoute } from "~/plugins/RoutePlugin";
import { createHandlerOnRequest } from "~/plugins/HandlerOnRequestPlugin";

const createOptionsRoute = () => {
return createRoute(({ onOptions }) => {
onOptions("/webiny-test", async (request, reply) => {
return reply.send({
weGotToOptionsReply: true
});
});
});
};

describe("fastify onRequest event", () => {
it("should return our built-in headers when sending options request", async () => {
const app = createHandler({
plugins: [createOptionsRoute()]
});

const result = await app.inject({
path: "/webiny-test",
method: "OPTIONS",
query: {},
payload: JSON.stringify({})
});

expect(result).toMatchObject({
statusCode: 204,
cookies: [],
headers: {
"cache-control": "public, max-age=86400",
"content-type": "application/json; charset=utf-8",
"access-control-allow-origin": "*",
"access-control-allow-headers": "*",
"access-control-allow-methods": "OPTIONS",
"access-control-max-age": "86400",
connection: "keep-alive"
},
body: "",
payload: ""
});
});

it("should return users headers set via the plugin", async () => {
const app = createHandler({
plugins: [
createOptionsRoute(),
createHandlerOnRequest(async (request, reply) => {
const raw = reply.code(205).hijack().raw;

raw.setHeader("user-set-header", "true");
raw.end(JSON.stringify({ usersPlugin: true }));

return false;
})
]
});

const result = await app.inject({
path: "/webiny-test",
method: "OPTIONS",
query: {},
payload: JSON.stringify({})
});

expect(result).toMatchObject({
statusCode: 205,
cookies: [],
headers: {
"user-set-header": "true",
connection: "keep-alive",
"transfer-encoding": "chunked"
},
body: JSON.stringify({ usersPlugin: true }),
payload: JSON.stringify({ usersPlugin: true })
});
});

it("should throw a log if user did not end onRequest plugin correctly", async () => {
const app = createHandler({
plugins: [
createOptionsRoute(),
createHandlerOnRequest(async (request, reply) => {
const raw = reply.code(205).hijack().raw;

raw.setHeader("user-set-header", "true");
raw.end(JSON.stringify({ usersPlugin: true }));
})
]
});

const log = console.log;
/**
* This way we can check if the log, which should not be sent, was sent.
*/
let logged = false;

console.log = values => {
if (typeof values === "string") {
try {
const obj = JSON.parse(values);
if (obj?.message && obj?.explanation) {
logged = true;
}
} catch {}
}
log(values);
};

const result = await app.inject({
path: "/webiny-test",
method: "OPTIONS",
query: {},
payload: JSON.stringify({})
});

expect(logged).toEqual(true);

expect(result).toMatchObject({
statusCode: 205,
cookies: [],
headers: {
"user-set-header": "true",
connection: "keep-alive",
"transfer-encoding": "chunked"
},
body: JSON.stringify({ usersPlugin: true }),
payload: JSON.stringify({ usersPlugin: true })
});
});
});
35 changes: 35 additions & 0 deletions packages/handler/src/fastify.ts
Expand Up @@ -17,6 +17,7 @@ import { BeforeHandlerPlugin } from "./plugins/BeforeHandlerPlugin";
import { HandlerResultPlugin } from "./plugins/HandlerResultPlugin";
import { HandlerErrorPlugin } from "./plugins/HandlerErrorPlugin";
import { ModifyFastifyPlugin } from "~/plugins/ModifyFastifyPlugin";
import { HandlerOnRequestPlugin } from "~/plugins/HandlerOnRequestPlugin";

const DEFAULT_HEADERS: Record<string, string> = {
"Cache-Control": "no-store",
Expand Down Expand Up @@ -257,11 +258,45 @@ export const createHandler = (params: CreateHandlerParams) => {
* Also, if it is an options request, we skip everything after this hook and output options headers.
*/
app.addHook("onRequest", async (request, reply) => {
/**
* Our default headers are always set. Users can override them.
*/
const defaultHeaders = getDefaultHeaders(definedRoutes);
reply.headers(defaultHeaders);
/**
* Users can define their own custom handlers for the onRequest event - so let's run them first.
*/
const plugins = app.webiny.plugins.byType<HandlerOnRequestPlugin>(
HandlerOnRequestPlugin.type
);
for (const plugin of plugins) {
const result = await plugin.exec(request, reply);
if (result === false) {
return;
}
}
/**
* When we receive the OPTIONS request, we end it before it goes any further as there is no need for anything to run after this - at least for our use cases.
*
* Users can prevent this by creating their own HandlerOnRequestPlugin and returning false as the result of the callable.
*/
if (request.method !== "OPTIONS") {
return;
}

if (reply.sent) {
/**
* At this point throwing an exception will not do anything with the response. So just log it.
*/
console.log(
JSON.stringify({
message: `Output was already sent. Please check custom plugins of type "HandlerOnRequestPlugin".`,
explanation:
"This error can happen if the user plugin ended the reply, but did not return false as response."
})
);
return;
}
const raw = reply.code(204).hijack().raw;
const headers = { ...defaultHeaders, ...OPTIONS_HEADERS };
for (const key in headers) {
Expand Down
1 change: 1 addition & 0 deletions packages/handler/src/index.ts
Expand Up @@ -6,4 +6,5 @@ export * from "~/plugins/RoutePlugin";
export * from "~/plugins/BeforeHandlerPlugin";
export * from "~/plugins/HandlerErrorPlugin";
export * from "~/plugins/HandlerResultPlugin";
export * from "~/plugins/HandlerOnRequestPlugin";
export * from "~/plugins/ModifyFastifyPlugin";
35 changes: 35 additions & 0 deletions packages/handler/src/plugins/HandlerOnRequestPlugin.ts
@@ -0,0 +1,35 @@
import { Plugin } from "@webiny/plugins";
import { FastifyReply, FastifyRequest } from "fastify";

/**
* If the execution of the callable returns false, no more plugins will be executed after the given one.
* Nor it will execute our default OPTIONS code.
*
* This way users can prevent stopping of the request on our built-in OPTIONS request.
*/
export type HandlerOnRequestPluginCallableResponse = false | undefined | null | void;
interface HandlerOnRequestPluginCallable {
(request: FastifyRequest, reply: FastifyReply): Promise<HandlerOnRequestPluginCallableResponse>;
}

export class HandlerOnRequestPlugin extends Plugin {
public static override type = "handler.event.onRequest";

private readonly cb: HandlerOnRequestPluginCallable;

public constructor(cb: HandlerOnRequestPluginCallable) {
super();
this.cb = cb;
}

public async exec(
request: FastifyRequest,
reply: FastifyReply
): Promise<HandlerOnRequestPluginCallableResponse> {
return this.cb(request, reply);
}
}

export const createHandlerOnRequest = (cb: HandlerOnRequestPluginCallable) => {
return new HandlerOnRequestPlugin(cb);
};