0

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");

Vivere
  • 1,919
  • 4
  • 16
  • 35
  • See if [Add ShallowEtagHeaderFilter in Spring Boot MVC](https://stackoverflow.com/questions/26742207/add-shallowetagheaderfilter-in-spring-boot-mvc) helps – samabcde Dec 02 '20 at 14:03
  • @samabcde sorry for the late response, I only now got to work on this again. I looked into it and it seems that it doesn't work for me. – Vivere Dec 13 '20 at 13:40
  • I can see the Etag in http response header using similar configuration, but I can't follow your code in `WebSecurityConfig` for `userDetailsService`(can't inject), `configure(HttpSecurity http)` (`new AuthenticationFilter(authenticationManager())`, `new AuthorizationFilter(authenticationManager())`) can't compile – samabcde Dec 14 '20 at 14:46
  • @samabcde Those are some security filters used for authorization & authentication. I don't think they are relevant to the issue. Did you test it with postman? If so, I also did that and it is true that I can see it there, but my web client cannot see the ETag if I don't specify it exactly in the endpoint. – Vivere Dec 14 '20 at 21:11
  • As Etag actually appear in the response header, I think we need to check 1. What is the difference in response header when changing the endpoint, 2. Problem may be due to web client, please provide details of your web client. – samabcde Dec 15 '20 at 01:29
  • @samabcde 1) as for Postmant requests, there is no difference. 2) I have an Ionic React application where I do rest requests using Axios. There isn't anything special there. I will update the post providing an example as for how I make a request. – Vivere Dec 15 '20 at 17:45
  • 1
    After some investigation, I think the different is whether the response header: "Access-Control-Expose-Headers: ETag" exist, when you add `response.setHeader("Access-Control-Expose-Headers", "ETag");` this header will always exist in the response, otherwise it will only exist when the request is CORS (request header: "origin" exist and different to server), hope this help. – samabcde Dec 16 '20 at 09:58
  • I didn't fully comprehend it. You are saying that possibly my request isn't considered CORS? If so, it's understandable since I'm running both the server & client locally while debugging. – Vivere Dec 30 '20 at 00:07

1 Answers1

1

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.

samabcde
  • 6,988
  • 2
  • 25
  • 41