-
Notifications
You must be signed in to change notification settings - Fork 79
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
Middleware changes #693
base: main
Are you sure you want to change the base?
Middleware changes #693
Conversation
can you update the README please? |
ea523b4
to
fa5cbe4
Compare
@gr2m now? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work on this! I appreciate you taking the time to make webhooks.js
better ❤️
README.md
Outdated
@@ -586,6 +588,29 @@ createServer(middleware).listen(3000); | |||
Custom path to match requests against. Defaults to <code>/api/github/webhooks</code>. | |||
</td> | |||
</tr> | |||
<tr> | |||
<td> | |||
<code>pathRegex</code> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think of accepting a path
and allowing it to be either a string
or a RegExp
? That interface feels nicer to me - although I'm not an expert in idiomatic JavaScript. It avoids the "override" complexity you have here, which could lead to surprises.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not a js pro either, but something like a string | RegExp
and later on a type matching to them seems to be more dangerous to me. But I can try it, a typeof string should split them correctly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let’s wait and see what @gr2m or other maintainers think.
If we keep it this way, we should enforce that exactly one of the path options is provided.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not a js pro either, but something like a
string | RegExp
and later on a type matching to them seems to be more dangerous to me.
Can you explain why you think so?
I don't see the need to add another argument that has the exact same purpose
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I fixed this also.
src/middleware/node/middleware.ts
Outdated
@@ -1,8 +1,19 @@ | |||
// remove type imports from http for Deno compatibility | |||
// see https://github.com/octokit/octokit.js/issues/24#issuecomment-817361886 | |||
// import { IncomingMessage, ServerResponse } from "http"; | |||
type IncomingMessage = any; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Despite this any
, we did clearly have some implicit assumptions before about what properties the IncomingMessage
would have. Is the interface you define below common across many JavaScript libraries/frameworks?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know if it is common or not, I just made those "implicit assumptions" to be explicit, so others could write a bridge more easily.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They aren't really "implicit assumptions", the middleware was designed for NodeJS HTTP servers of the likes of express, and raw http.Server
implementations.
We had to comment out the imports due to Deno compatibility (The link in the comment is out of date)
See octokit/octokit.js#2075 (comment) for context.
That's why they are currently set to any
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@wolfy1339 Thanks for the context! Do you think we should stick with any
? Or can we define our own interface as @tg44 has?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure. I have no clue when it comes to Deno, or other environments.
It would probably be best to write new middleware designed with other platforms at mind
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had this in mind, but that would mean that we need to publish multiple small packages (like middleware-node, middleware-deno, middleware-sunder), and I didn't want to go this deep.
BTW I did nothing "strange" here, I just made an interface from the typedef, wrote the used params/functions into it, and cross-checked with the original node types (so the headers could be string | string[] | undefined
instead of just string
and the URL and method could be undefined too). In theory this should not broke anything.
@@ -72,7 +85,7 @@ export async function middleware( | |||
didTimeout = true; | |||
response.statusCode = 202; | |||
response.end("still processing\n"); | |||
}, 9000).unref(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is needed for Node, so it exits even with a timeout running
octokit/octokit.js#2079
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I read what unref is and why it is used, and I'm almost certain that we don't "need" that unref (we have a small timeout and also we clear in every paths). Also, v8 does not have unref, so cloudflare workers die BCS of it.
Probably we should use the below check instead of just adding/deleting it;
if (typeof timeout.unref === "function") {
timeout.unref();
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, v8 does not have unref, so cloudflare workers die BCS of it
are you sure? Node is built on v8
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I faced the "unref is not a function" error when I tried to run it on cloudflare workers so I'm sure. But a fixup commit is on the way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added this back with an if, and I left out the else from the branch coverage, to keep it 100%. I don't think it is worth the effort to mock the setInterval
out.
README.md
Outdated
@@ -316,7 +316,7 @@ eventHandler | |||
### webhooks.receive() | |||
|
|||
```js | |||
webhooks.receive({ id, name, payload }); | |||
webhooks.receive({ id, name, payload, originalRequest }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this will work. If I recall correctly, we made the APIs in a way to work independent of its environment. Node's standard request/response middleware is the most common, but we also want to support others such as Cloudflare Workers and Deno.
So I think this would need to be something abstract, like extraData
, or similar.
The node middleware could then pass the extraData.request
reference by default
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would work, but it’s sad to lose the request type that makes this pattern a bit friendlier to work with.
Could we keep the current pattern, and then say that the originalRequest
is not guaranteed to be provided? That way, you have the Node middleware case where the request is passed to the handler, whereas if you call receive
directly, it’s missing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The originalRequest
already an originalRequest?: IncomingMessage
so it is optional. You need to check if it is set or not, and you can possibly use the whole thing without it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think the webhooks.*
API is the right place for it. When using the node middleware then extraData.request
could be typed correctly as IncomingMessage
.
The Webhooks
constructor would need a generic to which you can pass types for the .extraData
, so even without using the node middleware, you'll be able to get all the types you want, without us needing to import the types for all users.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we are speaking about different things, so I will WOT down all of my gathered ideas.
- Problem 1; I need data from the request in the handlers.
- Problem 2; We want to handle multiple "http frameworks"
For problem 1;
- one option is to simply pass the whole request in "as-is", and we can read it in the hooks (current PR)
- we can wrap this data, or type it better (like I did in this PR or like I will describe later)
- we define an extractor function, which will allow as to type the "worthy data", call this function in the middleware, and pass the "additional data" down to the hooks (my original idea, but it still needs a lot of generic types so from the two options I would choose the first one)
With problem 2, I started out a solution (which is probably not the best in retro, see later), which does nothing special just type the request and response as we currently use them (making a minimal needed subset). This is better than the previous type IncomingMessage = any
but still not really good to call this IncomingMessage
(bcs it is not necessarily an IM from node, and it is a bit misleading, but I did not introduce this misleading behavior to the code, it was already there).
After I slept on it I would solve this whole middleware problem with TypeClasses. Instead of extending the original implementation. We could generic-type out a middleware<I, O>(helper: MiddlewareIOHelper<I, O>, request: I, response: O, ...)
and get a helper which looks something like this;
interface MiddlewareIOHelper<I, O> {
getHeader(i: I, name: string): string;
getBodyAsJson(i: I): Promise<unknown>;
getUrlPath(i: I): string;
getMethod(i: I): "GET" | "POST" | "DELETE" | "OPTION" | "PUT" | "UNKNOWN";
respondWith(o: O, body?: string, status?: number, headers?: Record<string, string>): void;
}
From the interface above, we can easily implement an instance for <IncomingMessage, ServerResponse>
or <Context, Context>
or we could even type the <I, O>
to a single type so less typeparams will be dragged in the code.
I think this PR is focusing on problem 1 solution 1. And bcs of the area, it touches problem 2 too.
I think the "solution" would be sth like;
- renaming the
IncomingMessage
bcs it is misleading - merging this PR
- agreeing on the typeclass solution (I can draft out one), and handling it in an another PR
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My gut feeling is that we should probably simplify this proposal and go back to something similar to what you proposed originally: just allow the handler to accept an extraData
object (which can be any object; no specific type!), and then have a convention that the built-in Node middleware passes a request
key inside that extraData
with the IncomingMessage
.
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, I reverted all changes with IncomingMessage
, and renamed the originalRequest
to extraData
.
@timrogers @gr2m ping, I think I fixed/reverted all the concerns. |
Hey @tg44 , im using fastify and was wondering if this pr could help me integrate with it?? |
@amitgilad3 this is the relevant WOT about what this PR does and what it doesn't. I think it would be nice to provide the described I think the current implementation of this lib uses the interfaces described here. If you can mock/wrap these functions to fastify specific, you can probably write a middleware yourself. |
fixes #682
There are three changes in this commit.
I typed the request and the response (as close as the code uses it)The third mod could be opinionated. It matches the
http
interface and can be implemented in other HTTP frameworks too. Quick and dirty example for sunder;