'Springboot @DeleteMapping respond 404, but response body is empty

I have problem with @DeleteMapping.

Situation is like below.

  1. If I request to /v1/cache/{cacheEntry} with method DELETE,
    It respond with 404, but body was empty. no message, no spring default json 404 response message.

  2. If i request to /v1/cache/{cacheEntry} with method POST,
    It respond with 405 and body was below. (This action is correct, not a bug.)

  3. If I change @DeleteMapping to @PostMapping, and request /v1/cache/{cacheEntry} with method POST, It respond success with code 200.

{
    "timestamp": 1643348039913,
    "status": 405,
    "error": "Method Not Allowed",
    "message": "",
    "path": "/v1/cache/{cacheEntry}"
}

// Controller


@Slf4j
@RestController
@RequestMapping("/v1/cache")
@RequiredArgsConstructor
public class CacheController {

    private final CacheService cacheService;

    @PostMapping("/{cacheEntry}")
    public CacheClearResponse clearCacheEntry(@PathVariable("cacheEntry") CacheChannels cacheEntry) {
        try {
            log.info("Cache entry :: " + cacheEntry);
            cacheService.evictCacheEntry(cacheEntry);
            return CacheClearResponse.builder()
                    .result(
                            RequestResult.builder()
                                    .code(9200)
                                    .message("SUCCESS")
                                    .build()
                    )
                    .common(
                            Common.builder().build()
                    )
                    .date(LocalDateTime.now())
                    .build();
        } catch (Exception e) {
            e.printStackTrace();
            StringWriter sw = new StringWriter();
            e.printStackTrace(new PrintWriter(sw));
            return CacheClearResponse.builder()
                    .result(
                            RequestResult.builder()
                                    .code(9999)
                                    .message(sw.toString())
                                    .build()
                    )
                    .common(
                            Common.builder().build()
                    )
                    .date(LocalDateTime.now())
                    .build();
        }

    }
}
}

// CacheService

@Service
@RequiredArgsConstructor
public class CacheService {

    private final CacheManager cacheManager;

    public void evictCacheEntry(CacheChannels cacheEntry) {
        Cache cache = cacheManager.getCache(cacheEntry.getCacheName());
        if (cache != null) {
            cache.clear();
        }
    }

    public void evictCache(CacheChannels cacheEntry, String cacheKey) {
        Cache cache = cacheManager.getCache(cacheEntry.getCacheName());
        if (cache != null) {
            cache.evict(cacheKey);
        }
    }
}

// Enum

@Getter
@AllArgsConstructor
public enum CacheChannels {
    CACHE_TEN_MIN(Names.CACHE_TEN_MIN, Duration.ofMinutes(10)),
    CACHE_HALF_HR(Names.CACHE_HALF_HR, Duration.ofMinutes(30)),
    CACHE_ONE_HR(Names.CACHE_ONE_HR, Duration.ofHours(1)),
    CACHE_THREE_HR(Names.CACHE_THREE_HR, Duration.ofHours(3)),
    CACHE_SIX_HR(Names.CACHE_SIX_HR, Duration.ofHours(6)),
    CACHE_ONE_DAY(Names.CACHE_ONE_DAY, Duration.ofDays(1));
    private final String cacheName;
    private final Duration cacheTTL;

    public static CacheChannels from(String value) {
        return Arrays.stream(values())
                .filter(cacheChannel -> cacheChannel.cacheName.equalsIgnoreCase(value))
                .findAny()
                .orElse(null);
    }

    public static class Names {

        public static final String CACHE_TEN_MIN = "cache10Minutes";
        public static final String CACHE_HALF_HR = "cache30Minutes";
        public static final String CACHE_ONE_HR = "cache1Hour";
        public static final String CACHE_THREE_HR = "cache3Hours";
        public static final String CACHE_SIX_HR = "cache6Hours";
        public static final String CACHE_ONE_DAY = "cache1Day";
    }
}

// Converter

@Slf4j
public class StringToCacheChannelConverter implements Converter<String, CacheChannels> {
    @Override
    public CacheChannels convert(String source) {
        log.info("Convert Target: " + source);
        return CacheChannels.from(source);
    }
}

// Security Config

@Configuration
@EnableWebSecurity
@Order(1)
public class APISecurityConfig extends WebSecurityConfigurerAdapter {

    @Value("${spring.security.auth-token-header-name:Authorization}")
    private String apiKeyHeader;

    @Value("${spring.security.secret}")
    private String privateApiKey;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        APIKeyAuthFilter filter = new APIKeyAuthFilter(apiKeyHeader);
        filter.setAuthenticationManager(new AuthenticationManager() {
            @Override
            public Authentication authenticate(Authentication authentication)
                    throws AuthenticationException {
                String requestedApiKey = (String) authentication.getPrincipal();
                if (!privateApiKey.equals(requestedApiKey)) {
                    throw new BadCredentialsException("The API Key was not found or not the expected value");
                }

                authentication.setAuthenticated(true);
                return authentication;
            }
        });

        http
                .csrf().disable()
                .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .addFilter(filter)
                .authorizeRequests()
                    .antMatchers("/v1/cache/**")
                        .authenticated();
    }
}

// Filter

@Slf4j
public class APIKeyAuthFilter extends AbstractPreAuthenticatedProcessingFilter {

    private String apiKeyHeader;

    public APIKeyAuthFilter(String apiKeyHeader) {
        this.apiKeyHeader = apiKeyHeader;
    }

    @Override
    protected Object getPreAuthenticatedPrincipal(HttpServletRequest httpServletRequest) {
        log.info("Check authenticated.");
        return httpServletRequest.getHeader(apiKeyHeader);
    }

    @Override
    protected Object getPreAuthenticatedCredentials(HttpServletRequest httpServletRequest) {
        return "N/A";
    }

}

// Web Config

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new StringToCacheChannelConverter());
    }

    @Bean
    public HiddenHttpMethodFilter hiddenHttpMethodFilter() {
        return new HiddenHttpMethodFilter();
    }
}

This can be expected the controller was loaded, endpoint was mapped.
I tried change @DeleteMapping to @PostMapping and it was successfully respond against to POST request.

What am I missing?



Solution 1:[1]

I found reason why received 404 without any messages. My tomcat is on remote server. It configured with security-constraint and disable DELETE method for all enpoints.

I just comment out it and It work properly with delete method.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Junghoon Kong