Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Default Cache-Control/Expires/Pragma headers are being added in async response with custom Cache-Control header value #12865

Open
cmark opened this issue Mar 13, 2023 · 4 comments
Assignees
Labels
in: web An issue in web modules (web, webmvc) type: bug A general bug

Comments

@cmark
Copy link

cmark commented Mar 13, 2023

Describe the bug
Setting up a basic async HTTP GET endpoint where the returned response is allowed to be cached by downstream clients (via the Cache-Control header) produces duplicate Cache-Control header values. Expires and Pragma headers are also being added.

To Reproduce

  1. Spring Boot 2.7.x project
  2. Basic application with EnableWebSecurity annotation
  3. Async HTTP GET endpoint where the DeferredResult is a ResponseEntity with a Cache-Control header value

Headers being returned in case of the async HTTP response:

Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Cache-Control: max-age=0
Connection: keep-alive
Content-Length: 2
Content-Type: text/html;charset=UTF-8
Date: Mon, 13 Mar 2023 18:04:41 GMT
Expires: 0
Keep-Alive: timeout=60
Pragma: no-cache
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block

Headers being returned in case of the sync HTTP response:

Cache-Control: max-age=0
Connection: keep-alive
Content-Length: 2
Content-Type: text/html;charset=UTF-8
Date: Mon, 13 Mar 2023 18:06:24 GMT
Keep-Alive: timeout=60
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block

Expected behavior
Setting Cache-Control/Expires/Pragma headers in async request processing context should be honored by the security header writer and it should not populate the HTTP response with the default headers in this case.

See the sample's sync endpoint for desired behavior.

Sample
https://github.com/cmark/spring-security-async-cache-control

@cmark cmark added status: waiting-for-triage An issue we've not yet triaged type: bug A general bug labels Mar 13, 2023
@cmark
Copy link
Author

cmark commented Nov 7, 2023

Any news on this? Are there any easy workarounds other than having to manually set the header for each async endpoint.

@mrohlof-protofy
Copy link

I am experiencing a similar problem on Spring Boot 3.2.2
I have a suspended function adding CacheControl headers via ResponseEntity (ImageService is returning a Mono<ByteArray>):

override suspend fun downloadImage(id: UUID): ResponseEntity<ByteArray> {
        return ResponseEntity.ok()
            .cacheControl(CacheControl.maxAge(30, java.util.concurrent.TimeUnit.DAYS))
            .body(imageService.loadImage(id).awaitSingle())
    }

Those headers are not in the response when CacheControlHeadersWriter adds its own headers, resulting in duplicate headers and frontend applications being confused.

My current workaround is to inject the HttpServletResponse, which is later passed to CacheControlHeadersWriter, and set headers manually:

    override suspend fun downloadImage(id: UUID): ByteArray {
        response.setHeader("Cache-Control", "max-age=2592000")
        return imageService.loadImage(id).awaitSingle()
    }

Please find a way so I can use idiomatic ResponseEntity objects with typesafe CacheControl.

@sjohnr
Copy link
Member

sjohnr commented Jun 14, 2024

I apologize for the delay in responding, @cmark. Thanks for reaching out and for providing a reproducer!

Expected behavior
Setting Cache-Control/Expires/Pragma headers in async request processing context should be honored by the security header writer and it should not populate the HTTP response with the default headers in this case.

The reference docs mention in Spring MVC Async Integration:

There is no automatic integration with a DeferredResult that is returned by controllers. This is because DeferredResult is processed by the users and, thus, there is no way of automatically integrating with it.

I believe this is the reason for this issue. The headers writer fires after the initial controller method returns, but is not able to wait for the DeferredResult#setResult method to be called.

One workaround you might consider is to create a separate filter chain for your async endpoint(s) that does not write the Cache-Control headers. For example:

@Configuration
@EnableWebSecurity
public class SecurityConfiguration {

	@Bean
	@Order(1)
	public SecurityFilterChain asyncSecurityFilterChain(HttpSecurity http) throws Exception {
		http
			.securityMatcher("/async")
			.authorizeHttpRequests((authorize) -> authorize
				.anyRequest().authenticated()
			)
			.headers((headers) -> headers
				.cacheControl((cacheControl) -> cacheControl.disable())
			)
			.httpBasic(Customizer.withDefaults());

		return http.build();
	}

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			.authorizeHttpRequests((authorize) -> authorize
				.anyRequest().authenticated()
			)
			.httpBasic(Customizer.withDefaults());

		return http.build();
	}

	// ...

}

Given that Spring Security does not integrate with DeferredResult, I'm not sure there is much we can do here since the headers are written as part of the filter chain, and Spring MVC is handling the result of your controller asynchronously.

Unfortunately, it appears this issue also exists when returning a Callable, and it may be for a similar reason (which is obviously not ideal). I will leave this issue open to see if anything can be done but I wanted to at least provide a workaround for you to try. If you're still around, please let me know if this helps you (or anyone else).

@sjohnr sjohnr added in: web An issue in web modules (web, webmvc) and removed status: waiting-for-triage An issue we've not yet triaged labels Jun 14, 2024
@sjohnr sjohnr self-assigned this Jun 14, 2024
@cmark
Copy link
Author

cmark commented Jun 17, 2024

Hi @sjohnr,

Thank you for the detailed explanation and the workaround. In the meantime, we have solved the problem by handling the HTTP header writing ourselves (directly in the HTTPResponse), overriding any previously written HTTP header.

I will leave this issue open to see if anything can be done
In the long run, it would be nice to have a built-in solution for this in Spring Security.

Thanks again,
Mark

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
in: web An issue in web modules (web, webmvc) type: bug A general bug
Projects
None yet
Development

No branches or pull requests

3 participants