tower-rs / tower-http

HTTP specific Tower utilities.
701 stars 164 forks source link

ensure_usable_cors_rules() isn't client friendly #272

Open garypen opened 2 years ago

garypen commented 2 years ago

Feature Request

Motivation

We are using tower-http in our project and we allow users to configure CORS. If there is a mistake which we can't detect in the composition of those rules, then this can result in a runtime panic in the ensure_usable_cors_rules() fn. This is not a good experience and we are working around this at the moment by validating our CORS rules before we pass them into tower-http.

Proposal

Could we modify ensure_usable_cors_rules() so that rather than asserting rules are usable, returns a result and expose this function so that we can validate before attempting to create a CorsLayer?:

    pub fn ensure_usable_cors_rules(&self) -> Result<(), &'static str>;

Alternatives

Perhaps have a try_layer() on Cors (or CorsLayer?) which is a fallible CorsLayer creator? If we had this, we could keep the original assert based ensure_usable_cors_rules() and add a new private fallible ensurer for consumption in try_layer. I feel like this would be the cleanest solution.

jplatte commented 2 years ago

I don't really understand what you mean with try_layer. I see two solutions here:

davidpdrsn commented 2 years ago

Returning Result makes most sense when you expect users to handle the errors somehow. ensure_usable_cors_rules checks for invalid configurations which I'm not sure how you'd handle. Maybe you can elaborate on your use case and how you expect to handle the errors?

Something like try_layer wont work since tower::Layer trait doesn't have such a method. It also wouldn't be compatible with tower::ServiceBuilder unless we make fallible as well, which is a big change. If we do decide to support this I say we provide a separate builder.

garypen commented 2 years ago

I'll try to clarify why this is potentially useful:

@jplatte What I mean bytry_layer() is to avoid changing the existing layer() interface, to help maintain backwards compatibility for existing API consumers. There may be better ways to achieve this, but that's the motivation. @davidpdrsn The nice thing about returning a Result here is that I like the existing error messages and so, I want to maintain those (hence, bool not as attractive) and in my use-case I can handle the errors. In our product, we maintain a configuration state machine. Before we transition to the new configuration, ensure that the incoming configuration is correct. If not, we reject the proposed configuration (with helpful error messages) and continue with the current configuration. Here's a link to a PR on our product which may be helpful if you want more detail: https://github.com/apollographql/router/pull/1197

I could have a go at trying to implement something for a draft PR if this is something that you think would be useful in tower-http. If you think it's not generally applicable, then I can live with the solution I've put together in the PR above.

jplatte commented 2 years ago

Alright, I guess Result makes sense if you want to pass on the error message. I could totally see having .validate() on the layer and service, which returns Result<(), CorsConfigurationError>, with the existing ensure_usable_cors_rules calls being replaced by something like

if let Err(e) = self.validate() {
    panic!("{}", e);
}
davidpdrsn commented 2 years ago

I think I'm gonna experiment with some sort of builder that doesn't panic.