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

Allow async predicate for cors AllowOrigin #478

Merged
merged 13 commits into from
Mar 15, 2024

Conversation

PoOnesNerfect
Copy link
Contributor

@PoOnesNerfect PoOnesNerfect commented Mar 8, 2024

This PR allows using async predicate for AllowOrigin.

Motivation

This is required when the allowed origins may change depending on the origin and the requested resource; this often requires accessing some external state such as a database or another service.

Pretty common use case is SaaS like CMS where a user makes API calls to your server from their client side. The user will provide the list of allowed origins that can access their data from the client side.

Solution

AllowOrigin now has a function async_predicate which takes a closure that returns a future that returns bool.

Internally, AllowOrigin::to_header is now AllowOrigin::to_future which returns a future AllowOriginFuture that returns Option<(HeaderName, HeaderValue)>.
For existing cases like exact, list, or predicate, the future returns immediately with the values on the first poll, so it should not incur much (if any) overhead.

One unfortunate thing about this feature is that, because the returned future needs to have a static lifetime, you cannot pass references to it like:

AllowOrigin::async_predicate(|origin, parts| async move { origin == "https://example.com" })

Instead, you must make sure all the values passed into the future have static lifetimes:

AllowOrigin::async_predicate(|origin, parts| {
    let origin = origin.to_owned();
    async move { origin == "https://example.com" }
})

@jplatte jplatte self-requested a review March 11, 2024 09:15
Copy link
Collaborator

@jplatte jplatte left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR, looks very good! I'm just wondering about one small detail, see inline comment.

/// [`CorsLayer::allow_origin`]: super::CorsLayer::allow_origin
pub fn async_predicate<F, Fut>(f: F) -> Self
where
F: Fn(&HeaderValue, &RequestParts) -> Fut + Send + Sync + 'static,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like users would virtually always have to clone the origin HeaderValue, so what do you think about changing this to

Suggested change
F: Fn(&HeaderValue, &RequestParts) -> Fut + Send + Sync + 'static,
F: Fn(HeaderValue, &RequestParts) -> Fut + Send + Sync + 'static,

?

Copy link
Contributor Author

@PoOnesNerfect PoOnesNerfect Mar 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's an interesting point; I've thought about it for a little, and, for now, I'm still leaning towards keeping it as is, for more consistent type signatures.

For most, or all, async predicate use-cases, you would be awaiting on some external state, which requires you to follow the pattern of:

let client = get_api_client();

AllowOrigin::async_predicate(move |origin, parts| {
    let origin = origin.clone();
    let client = client.clone();
    async move {
        client.call_api(origin).await?;
        ...
    }
})

I'm not sure if removing the line let origin = origin.clone(); is a big lift in dx, when the pattern itself is already pretty verbose.
I would rather have the type signatures be consistent with the other allow origin functions.

Also, having origin as owned value may make it feel like there are two ways of doing things.

For example, this would work fine:

AllowOrigin::async_predicate(|origin, parts| async move { some_simple_logic(origin) })

So, users may try to do something like this, which will not compile:

AllowOrigin::async_predicate(|origin, parts| async move { some_simple_logic(origin, parts.uri().path()) })

This may give users a hard time with lifetime errors, if they haven't worked much with async closures.

So, I would just like there to be a single way of doing things, although it might be a bit more annoying, which is:

// this compiles
AllowOrigin::async_predicate(|origin, parts| {
    let origin = origin.clone();
    async move { some_simple_logic(origin) }
})

// this compiles
AllowOrigin::async_predicate(|origin, parts| {
    let origin = origin.clone();
    let path = parts.uri().path().to_owned();
    async move { some_simple_logic(origin, path) }
})

However, I just might be overthinking it too much, when it's just not a big deal, and will just be a better dx.

I don't think my opinion on this is strong, so I will just go with your decision.
If you think this change makes sense, I will make the change and re-request review.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an interesting point about captured variables also needing clone. Could you try changing Fn to FnOnce + Clone? I'm pretty sure that then the extra cloning of captured variables can be internalized into the library as one clone of the closure.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that works! however, if you want to use parts, you still have to clone it before passing it to the future.
Should we also just provide parts as RequestParts instead of &RequestParts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it becomes too costly at that point, and it'd be better to just let the user decide what to pass into the future.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah for the origin I think it's okay to clone it always, it's extremely likely the user will need it and not that expensive to clone. The RequestParts are much more expensive to clone and virtually never needed in full, or even at all.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense.
I've made the updates, and fixed the docs. Please take a look!

@clowzed
Copy link

clowzed commented Mar 14, 2024

Hi @jplatte
I'm excited about this feature. My project currently relies on workarounds that this PR could replace, improving efficiency significantly. I'm eager to integrate it without resorting to forking. Can we expedite the merge process?

@PoOnesNerfect
Copy link
Contributor Author

Hi @jplatte I'm excited about this feature. My project currently relies on workarounds that this PR could replace, improving efficiency significantly. I'm eager to integrate it without resorting to forking. Can we expedite the merge process?

It should be merged within today or tomorrow, assuming there are no other issues. Thanks for waiting.

@jplatte jplatte merged commit 15d32dc into tower-rs:main Mar 15, 2024
11 checks passed
@clowzed
Copy link

clowzed commented Mar 15, 2024

Congratulations! Thanks for your work!

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

Successfully merging this pull request may close these issues.

3 participants