Skip to content

Relaxed Vary header defaults#674

Open
seun-ja wants to merge 8 commits into
tower-rs:mainfrom
seun-ja:539-CORS-module-excessively-sets-the-Vary-response-header-
Open

Relaxed Vary header defaults#674
seun-ja wants to merge 8 commits into
tower-rs:mainfrom
seun-ja:539-CORS-module-excessively-sets-the-Vary-response-header-

Conversation

@seun-ja
Copy link
Copy Markdown
Contributor

@seun-ja seun-ja commented May 9, 2026

Motivation

Fixes #539

Relaxes the default Vary header to an empty list.

Solution

Instead of the custom Default implementation adding some header values, it now simply returns the out-of-the-box Default, which is an empty Vector.

Also, I altered the test to reflect the new change and an additional test.

seun-ja added 2 commits May 9, 2026 11:57
Signed-off-by: Aminu Oluwaseun Joshua <seun.aminujoshua@gmail.com>
Signed-off-by: Aminu Oluwaseun Joshua <seun.aminujoshua@gmail.com>
@jplatte
Copy link
Copy Markdown
Member

jplatte commented May 9, 2026

This is worse than the current state of things IMO. Has the current default caused some specific issue for you?

@seun-ja
Copy link
Copy Markdown
Contributor Author

seun-ja commented May 9, 2026

No @jplatte.

I haven't had any issue using it in this context.

I'm just fixing because of the issue raised in #539 and seems it was ok to fix as you were ok with PR on it

@jlizen
Copy link
Copy Markdown
Member

jlizen commented May 12, 2026

++ to @jplatte 's feedback that this will break a lot of users since we are shifting from "conservative" defaults (include vary headers unnecessarily), to empty as a default, meaning we force anybody with dynamic CORS to manually set the header or risk caching correctness problems. Definitely a breaking change, probably not a justifiable one.

Would it work to instead have permissive() implicitly set .vary(Vary::list([])) rather than shifting defaults globally?

Or are there other overly conservative cases you are concerned with @seun-ja ?

@jplatte
Copy link
Copy Markdown
Member

jplatte commented May 12, 2026

So, what I was thinking of when writing the referenced comment was having the various methods like allow_origin subtract from the vary list (only if not customized), if and only if the input is one that doesn't result in different header values for different requests. For AllowOrigin this would be any and exact (AllowOriginInner::Const). For AllowHeaders it would be any and list (AllowHeadersInner::Const).

seun-ja added 2 commits May 13, 2026 22:23
Signed-off-by: Aminu Oluwaseun Joshua <seun.aminujoshua@gmail.com>
Signed-off-by: Aminu Oluwaseun Joshua <seun.aminujoshua@gmail.com>
Copy link
Copy Markdown
Member

@jlizen jlizen left a comment

Choose a reason for hiding this comment

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

The approach of eagerly stripping the vary headers based on permissive() creates a problem:

CorsLayer::permissive().allow_origin(AllowOrigin::predicate(...))

This would produce no vary: origin header, when in fact we might have dynamic CORS that has multiple allowed origins. That could result in a stale cache improperly rejecting origins that DO match the predicate.

The proper way to handle this would be, like jplatte@ suggested, doing the stripping on the explicit allow_* methods.

And then, we also would need an extra Is_custom_vary parameter in the builder (or something), to make sure that we don't overwrite the user's explicit configuration.

Signed-off-by: Aminu Oluwaseun Joshua <seun.aminujoshua@gmail.com>
Comment thread tower-http/src/cors/mod.rs Outdated
Comment on lines +209 to +222
} else {
// Ensure header is present
let mut vary = self.vary.clone();
let name = header::ACCESS_CONTROL_REQUEST_HEADERS;
if !vary.to_header().map_or(false, |(_, v)| {
v.to_str()
.unwrap_or("")
.split(',')
.any(|s| s.trim().eq_ignore_ascii_case(name.as_str()))
}) {
vary = Vary::list([name]);
}
self.vary = vary;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Instead of this complicated logic to add back the vary header if it has been removed from the default list, I think we could just do the checks to omit some vary headers in the Layer impl for CorsLayer (and additionally the Service impl of Cors, since that one also has the builder-style methods, though they are probably much less frequently used). What do you think?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah this makes more sense.

It's a lot cleaner and maintainable

Signed-off-by: Aminu Oluwaseun Joshua <seun.aminujoshua@gmail.com>
@seun-ja seun-ja requested review from jlizen and jplatte May 16, 2026 08:32
Signed-off-by: Aminu Oluwaseun Joshua <seun.aminujoshua@gmail.com>
Copy link
Copy Markdown
Member

@jlizen jlizen left a comment

Choose a reason for hiding this comment

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

Coming together! Down to just small tweaks.

Comment thread tower-http/src/cors/mod.rs Outdated
let mut layer = self.clone();

// Only set Vary if not custom
if !layer.is_vary_custom {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we extract this logic into a shared helper that can be used across both Layer::layer() and Cors::map_layer()

Comment thread tower-http/src/cors/mod.rs Outdated
// Only set Vary if not custom
if !layer.is_vary_custom {
// If all origins, methods, and headers are allowed, omit Vary
let all_origins = layer.allow_origin.is_wildcard();
Copy link
Copy Markdown
Member

@jlizen jlizen May 19, 2026

Choose a reason for hiding this comment

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

This logic looks fine in that it won't ever remove Vary headers from potentially dynamic CORS configurations.

But, it could be broadened to also include eg: AllowOrigin::exact("http://example.com").

Also the list method for headers/methods are constant (return all values), only the origin will vary what is returned per request.

The current approach is fine as a first step, optionally you could leave a TODO to sweep up the rest, or just add the others in now.

Comment thread tower-http/src/cors/tests.rs Outdated
];

#[tokio::test]
#[allow(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't think we need these clippy annotations on this test. Can we flip them over to expect() for all usages in this file to avoid future buildup?

@@ -1,37 +1,106 @@
use std::convert::Infallible;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we also have tests for:

  • "mixed" case where eg origin is a wildcard, but headers/method include vary headers
  • very_permissive emitting vary headers
  • at least a couple smoke tests on the Cors::map_layer test with and without vary headers

Comment thread tower-http/src/cors/mod.rs Outdated
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should be updated to explain that the default is derived from which other configuration is set, and that setting it explicitly will pin it regardless of other configuration.

Signed-off-by: Aminu Oluwaseun Joshua <seun.aminujoshua@gmail.com>
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.

CORS module excessively sets the Vary response header

3 participants