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

Support backend middleware #635

Open
bitspittle opened this issue Dec 23, 2024 · 6 comments
Open

Support backend middleware #635

bitspittle opened this issue Dec 23, 2024 · 6 comments
Labels
enhancement New feature or request

Comments

@bitspittle
Copy link
Collaborator

bitspittle commented Dec 23, 2024

In backend parlance, middleware is a method that intercepts all requests.

Note

The design for this concept is inspired by NextJS. See also: https://nextjs.org/docs/app/building-your-application/routing/middleware

(For a first pass at least, it should be an error to define more than one middleware method)

So imagine something like this:

// api/Middleware.kt

@Middleware
fun intercept(ctx: MiddlewareContext): Response? {
   // Check ctx.req and craft an alternate response if relevant, like a redirect if not logged in
   return null // If null, continue to the target API route. Otherwise, return the response directly
}

MiddlewareContext here is similar to ApiContext except it won't contain any response object.

If the method returns a response, then no followup API route will be triggered.

@bitspittle
Copy link
Collaborator Author

bitspittle commented Dec 23, 2024

Another thought is maybe we have two intercepting methods, one for handling all incoming requests before API routes handle them, and one for handling all outgoing responses right before sending them back to the client.

As one example, maybe users might want to add headers to all outgoing responses after a feature change.

@InterceptRequests
fun interceptRequest(ctx: InterceptRequestContext): Response?

@InterceptResponses
fun interceptResponse(ctx: ApiContext)

So in a chain, targeting a route called /api/user/query:

client sends request --> interceptRequest --> API route --> interceptResponse --> client receives response
                                 \                                 /   
                                  \-------------------------------/

(In the chart above, responses created by interceptRequest still go to interceptResponse, but maybe it could be argued that it could go straight to the client)

I will try to think of an actual use-case for this before deciding. However, it could be worth figuring this out because it would probably affect the naming of the feature (e.g. middleware --> intercept, or some other name). It's worth noting that NextJS does not do this so maybe it's not the right solution.

@kastork
Copy link

kastork commented Dec 23, 2024

Interesting idea...

This reminds me of the way AWS API gateway and elastic load balancing works. In those services, you can insert an authorization handler between the proxy and your backend. Your backend can then query the request for that auth information.

@kastork
Copy link

kastork commented Dec 23, 2024

Oh, and Ktor already has the interceptor concept. Not sure if it would be useful in this context though.

@mrlem
Copy link

mrlem commented Dec 23, 2024

@bitspittle seems to do the job. This doesn't seem to handle several interceptors (interceptors are often modeled as an ordered chain). But I wouldn't see it as too big an issue, given that you can insert several steps within the declared interceptor, so it doesn't seem like there are cases that can't be handled.

@bitspittle
Copy link
Collaborator Author

@mrlem

But I wouldn't see it as too big an issue, given that you can insert several steps within the declared interceptor, so it doesn't seem like there are cases that can't be handled.

Thanks! Yeah, that's how NextJS works -- one middlelayer declaration but you can of course break the logic up however you want.

@bitspittle
Copy link
Collaborator Author

bitspittle commented Dec 23, 2024

So waking up this morning and having slept on it, here's an interesting wrinkle...

A Kobweb project is allowed to be split up -- you can definitely define your project's API endpoints across separate modules in your codebase.

Forcing a single global interceptor rubs a bit up against this philosophy. So I'm staring down a few choices that I can think of:

  1. Only allow the final app backend to declare the interceptor (there is precedence for this! With the @App annotation)
  2. Change the proposed interceptor API in a way that allows multiple interceptors all to act on an intermediate response, and then always run them all.
  3. Allow library modules to define interceptors but don't apply them automatically; instead, if any are discovered, issue warnings when building the app until the developer explicitly adds them themselves into a top-level interceptor.

At the moment, I'm leaning to 1, but 3 would be pretty cool (where 3 is really just 1 + extra steps now that I am thinking through it).

If there would be a way to make 2 work, I would probably love that for the flexibility. This is how @InitSilk works -- it's very nice to initialize bits of Silk code here and there. But there's never initialization code that says "I want to run but no one else should."

For now, I can't see a way to do 2 fairly -- it is too easy to imagine a case where two interceptors want to create mutually exclusive responses.

I think the biggest danger is I push for 1 now and then realize I could have done 2 in a smart way later.


To break down approach 3 further, here are various cases a user can end up in:

  • No interceptors are defined anywhere: 👍
  • Multiple interceptors are defined in the app: compile error, delete all but one
  • Multiple interceptors are defined in a library: compile error, delete all but one
  • A single interceptor is defined in app, no interceptors defined in any library: 👍
  • A single interceptor is defined in exactly one library module, no interceptor defined in app: automatically generate an interceptor at the app level (is this a good idea?) 👍?
  • A single interceptor is defined in multiple library modules, app has none: compile warning, app developer should create an app interceptor and manually call all library interceptors.
  • A single interceptor is defined in multiple library modules, app has one: compile warning, app developer should manually call all library interceptors

Note that detecting which interceptors were already called in a codebase might be really hard to detect, actually, as I don't think KSP parses deep enough to give me that information. 3 may be dead in the water because of KSP limitations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants