I want to return an ETag header, but my Client cannot read it because it is not exposed. I have the following code:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(@NonNull CorsRegistry registry) { registry.addMapping("/**") .allowedHeaders("*") .allowedMethods("*") .allowedOrigins("*") .exposedHeaders("ETag"); } @Bean public ShallowEtagHeaderFilter shallowEtagHeaderFilter() { return new ShallowEtagHeaderFilter(); } }
But the client still cannot read the ETag. The only thing that works is the following:
@ApiResponses({ @ApiResponse(code = 200, message = "OK"), @ApiResponse(code = 500, message = "System Error") }) @ApiOperation(value = "returns the meals", response = MealDTO.class, responseContainer = "List", produces = MediaType.APPLICATION_JSON_VALUE) @GetMapping("/meal") public List<MealDTO> getMeals( @ApiParam(name = "page", type = "Integer", value = "Number of the page", example = "2") @RequestParam(required = false) Integer page, @ApiParam(name = "size", type = "Integer", value = "The size of one page", example = "5") @RequestParam(required = false) Integer size, @ApiParam(name = "sortBy", type = "String", value = "sort criteria", example = "name.asc,price.desc") @RequestParam(required = false) String sortBy, @ApiParam(name = "userId", type = "long", value = "ID of the User", example = "-1") @PathVariable Long userId, HttpServletResponse response ) { log.debug("Entered class = MealController & method = getMeals"); response.setHeader("Access-Control-Expose-Headers", "ETag"); return this.mealService.getMealsByUserId(page, size, sortBy, userId); }
Manually setting the exposed header for each endpoint. Isn’t this wat Cors Mapping was supposed to do? ExposedHeaders simply don’t work for me at all.
UPDATE:
From the comment below I saw that it may have something to do with the WebSecurityConfigurerAdapter, I’m also adding that:
@EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { private final UserDetailsService userDetailsService; public WebSecurityConfig(@Qualifier("userServiceImpl") UserDetailsService userDetailsService) { this.userDetailsService = userDetailsService; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService); } @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .cors() .and() .addFilter(new AuthenticationFilter(authenticationManager())) .addFilter(new AuthorizationFilter(authenticationManager())) .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // http.headers().cacheControl().disable(); } //region Beans @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(Collections.singletonList("*")); configuration.setAllowedMethods(Collections.singletonList("*")); configuration.setAllowedHeaders(Collections.singletonList("*")); configuration.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; } //endregion }
UPDATE 2:
Showing how I’m making calls from my client:
export const getMealByIdApi: (mealId: number, etag: string | null) => Promise<Meal | void> = (mealId, etag) => { let _config = config; if (etag) { _config.headers['If-None-Match'] = etag; } return axiosInstance .get<Meal>(`/meal/${mealId}`, _config) .then(response => { log(`[getMealByIdApi] [${response.status}] Successful API call for meal with id ${mealId}.`); const result: Meal = response.data; result.etag = response.headers['etag']; return result; }) .catch(err => { if (err.response.status === 304) { log(`[getMealByIdApi] [304] Successful API call for meal with id ${mealId}.`); return; } throw err; }); }
I’m getting no etag in the headers if I don’t specify explicitly in the endpoint: response.setHeader(“Access-Control-Expose-Headers”, “ETag”);
Advertisement
Answer
According to Axios get access to response header fields. we need to add “ETag” to “Access-Control-Expose-Headers”, such that we can access response.headers['etag']
.
Since you have already added ShallowEtagHeaderFilter
and also exposed “etag” in addCorsMappings
. “ETag” should be added to response header “Access-Control-Expose-Headers” for CORS request.
To make sure you request is CORS, you can debug DefaultCorsProcessor#processRequest
method, and check if CorsUtils.isCorsRequest(request)
returning true.
public boolean processRequest(@Nullable CorsConfiguration config, HttpServletRequest request, HttpServletResponse response) throws IOException { response.addHeader(HttpHeaders.VARY, HttpHeaders.ORIGIN); response.addHeader(HttpHeaders.VARY, HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD); response.addHeader(HttpHeaders.VARY, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS); if (!CorsUtils.isCorsRequest(request)) { return true; // will not add response header if not CORS } ...add response header
If it is returning false, you can check the requirement of CORS request from CorsUtils#isCorsRequest
description:
Returns true if the request is a valid CORS one by checking Origin header presence and ensuring that origins are different.