I have a problem with my test Spring Boot app. It works just fine, but when I enable Spring validation by adding dependency etc and adding a #Configuration:
#Configuration
public class TestConfiguration {
#Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
return new MethodValidationPostProcessor();
}
}
I get 404 for my test endpoint.
{
"timestamp": 1601507037178,
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/test"
}
I've already applied some solutions/proposals from similar problems (eg here or here) but without a success.
Here is my code:
https://github.com/zolv/error-handling-test
API interface:
#Validated
public interface TestApi {
#PostMapping(
value = "/test",
produces = {"application/json"},
consumes = {"application/json"})
#ResponseBody
ResponseEntity<TestEntity> getTest(#Valid #RequestBody(required = false) TestEntity request);
}
TestEntity just to send something:
#Data
public class TestEntity {
#JsonProperty("test")
#NotNull
private String test;
}
Controller implementation:
#RestController
#RequiredArgsConstructor
#Validated
public class TestController implements TestApi {
#Override
#ResponseBody
public ResponseEntity<TestEntity> getTest(#Valid #RequestBody TestEntity request) {
return ResponseEntity.ok(request);
}
}
My controller advice:
#ControllerAdvice
public class DefaultErrorHandlerAdvice extends ResponseEntityExceptionHandler {
#ExceptionHandler(value = {ConstraintViolationException.class})
#ResponseStatus(value = HttpStatus.BAD_REQUEST)
#ResponseBody
public ResponseEntity<String> handleValidationFailure(ConstraintViolationException ex) {
StringBuilder messages = new StringBuilder();
for (ConstraintViolation<?> violation : ex.getConstraintViolations()) {
messages.append(violation.getMessage());
}
return ResponseEntity.badRequest().body(messages.toString());
}
#Override
#ResponseBody
#ResponseStatus(HttpStatus.BAD_REQUEST)
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatus status,
WebRequest request) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.contentType(MediaType.APPLICATION_PROBLEM_JSON)
.body("problem");
}
}
Application:
#SpringBootApplication
#EnableWebMvc
#ComponentScan(basePackages = "com.test")
public class TestApplication {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
}
A test I use, but it fails also using Postman:
#SpringJUnitConfig
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class TestControllerTest {
#Autowired protected TestRestTemplate restTemplate;
#Test
void registrationHappyPath() throws Exception {
/*
* Given
*/
final TestEntity request = new TestEntity();
/*
* When/
*/
final ResponseEntity<String> response =
restTemplate.postForEntity("/test", request, String.class);
/*
* Then
*/
Assertions.assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
final String body = response.getBody();
Assertions.assertNotNull(body);
}
}
If I comment out a TestConfiguration then everything works fine.
Thank You in advance for any help.
You should set MethodValidationPostProcessor#setProxyTargetClass(true) because by default MethodValidationPostProcessor uses JDK proxy which leads to loss of your controller in the Spring context.
When AbstractHandlerMethodMapping#processCandidateBean is called isHandler(Class<?> beanType) will return false because JDK proxy doesn't contain #RestController annotation.
public MethodValidationPostProcessor methodValidationPostProcessor() {
MethodValidationPostProcessor mvProcessor = new MethodValidationPostProcessor();
mvProcessor.setProxyTargetClass(true);
return mvProcessor;
}
Related
I have problem with #DeleteMapping.
Situation is like below.
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.
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.)
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?
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.
I am consuming API which has to type of response success response 200 and Bad response 400 both of them has parameters inside their response body but the problem is am not able to get the bad response parameters it throws this exception
public ResponseEntity<String> balanceInquiry(BalanceInquiryRequestDto balanceInquiryRequestDto) {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
httpHeaders.set("API-KEY", "5d6f54d4");
HttpEntity<BalanceInquiryRequestDto> request = new HttpEntity<BalanceInquiryRequestDto>(balanceInquiryRequestDto , httpHeaders);
ResponseEntity<String> postForEntity =
restTemplate.postForEntity(uri , request, String.class);
return postForEntity;
}
it is working good when the response is ok 200
I created a small spring boot project to showcase what you can do.
First a simple service that will give us an error when called:
#RestController
public class Endpoint {
#GetMapping("/error")
public ResponseEntity createError() {
ErrorDetails errorDetails = new ErrorDetails("some error message");
return ResponseEntity.status(400).body(errorDetails);;
}
}
The error details which you want to extract are similar to this in this example:
#AllArgsConstructor
#Getter
#Setter
#ToString
#EqualsAndHashCode
public class ErrorDetails {
private String errorMessage;
}
And then another endpoint with a client that calls the failing service. It returns the error details received:
#RestController
public class ClientDemo {
#Autowired
private RestTemplate restTemplate;
#GetMapping("/show-error")
public String createError() {
try{
return restTemplate.getForEntity("http://localhost:8080/error", String.class).getBody();
} catch(HttpClientErrorException | HttpServerErrorException ex) {
return ex.getResponseBodyAsString();
}
}
}
For sake of completion:
#SpringBootApplication
public class StackoverflowApplication {
public static void main(String[] args) {
SpringApplication.run(StackoverflowApplication.class, args);
}
#Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
When navigating to http://localhost:8080/show-error you see this:
{
"errorMessage": "some error message"
}
I have created a Spring Restful Service and Spring MVC application.
Restful Service ::
Restful service returns an entity if its existing in DB. If it doesn't exist It returns a custom Exception information in ResponseEntity object.
It is working as expected tested using Postman.
#GetMapping(value = "/validate/{itemId}", produces = { MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE })
public ResponseEntity<MyItem> validateItem(#PathVariable Long itemId, #RequestHeader HttpHeaders httpHeaders) {
MyItem myItem = myitemService.validateMyItem(itemId);
ResponseEntity<MyItem> responseEntity = null;
if (myItem == null) {
throw new ItemNotFoundException("Item Not Found!!!!");
}
responseEntity = new ResponseEntity<MyItem>(myItem, headers, HttpStatus.OK);
return responseEntity;
}
If the requested Entity does not exist Restful Service returns below.
#ExceptionHandler(ItemNotFoundException.class)
public ResponseEntity<ExceptionResponse> itemNotFEx(WebRequest webRequest, Exception exception) {
System.out.println("In CREEH::ItemNFE");
ExceptionResponse exceptionResponse = new ExceptionResponse("Item Not Found Ex!!!", new Date(), webRequest.getDescription(false));
ResponseEntity<ExceptionResponse> responseEntity = new ResponseEntity<ExceptionResponse>(exceptionResponse, HttpStatus.NOT_FOUND);
return responseEntity;
}
But when I am calling the above service from a spring MVC application using RestTemplate, It is returning a valid object if it exists.
If the requested object does not exist Restful service is returning the exception information but its not reaching the calling(spring MVC) application.
Spring MVC application calls Restful Web Service using Rest template
String url = "http://localhost:8080/ItemServices/items/validate/{itemId}";
ResponseEntity<Object> responseEntity = restTemplate.exchange(url, HttpMethod.GET, httpEntity, Object.class, uriParms);
int restCallStateCode = responseEntity.getStatusCodeValue();
This is expected behavior. Rest template throws exception when the http status is client error or server error and returns the response when http status is not error status.
You have to provide implementation to use your error handler, map the response to response entity and throw the exception.
Create new error exception class with ResponseEntity field.
public class ResponseEntityErrorException extends RuntimeException {
private ResponseEntity<ErrorResponse> errorResponse;
public ResponseEntityErrorException(ResponseEntity<ErrorResponse> errorResponse) {
this.errorResponse = errorResponse;
}
public ResponseEntity<ErrorResponse> getErrorResponse() {
return errorResponse;
}
}
Custom error handler which maps the error response back to ResponseEntity.
public class ResponseEntityErrorHandler implements ResponseErrorHandler {
private List<HttpMessageConverter<?>> messageConverters;
#Override
public boolean hasError(ClientHttpResponse response) throws IOException {
return hasError(response.getStatusCode());
}
protected boolean hasError(HttpStatus statusCode) {
return (statusCode.is4xxClientError() || statusCode.is5xxServerError());
}
#Override
public void handleError(ClientHttpResponse response) throws IOException {
HttpMessageConverterExtractor<ExceptionResponse> errorMessageExtractor =
new HttpMessageConverterExtractor(ExceptionResponse.class, messageConverters);
ExceptionResponse errorObject = errorMessageExtractor.extractData(response);
throw new ResponseEntityErrorException(ResponseEntity.status(response.getRawStatusCode()).headers(response.getHeaders()).body(errorObject));
}
public void setMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
this.messageConverters = messageConverters;
}
}
RestTemplate Configuration - You have to set RestTemplate's errorHandler to ResponseEntityErrorHandler.
#Configuration
public class RestTemplateConfiguration {
#Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
ResponseEntityErrorHandler errorHandler = new ResponseEntityErrorHandler();
errorHandler.setMessageConverters(restTemplate.getMessageConverters());
restTemplate.setErrorHandler(errorHandler);
return restTemplate;
}
}
Calling Method
#Autowired restTemplate
String url = "http://localhost:8080/ItemServices/items/validate/{itemId}";
try {
ResponseEntity<Object> responseEntity = restTemplate.exchange(url, HttpMethod.GET, httpEntity, Object.class, uriParms);
int restCallStateCode = responseEntity.getStatusCodeValue();
} catch (ResponseEntityErrorException re) {
ResponseEntity<ErrorResponse> errorResponse = re.getErrorResponse();
}
Try using the #ResponseBody annotation on your Exceptionhandler. e.g:
public #ResponseBody ResponseEntity<ExceptionResponse> itemNotFEx(WebRequest webRequest, Exception exception) {... }
You should use Custom Exception Handler to fix your case. It looks like this
#ControllerAdvice
public class CustomResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
public CustomResponseEntityExceptionHandler() {
super();
}
// 404
#ExceptionHandler(value = { EntityNotFoundException.class, ResourceNotFoundException.class })
protected ResponseEntity<Object> handleNotFound(final RuntimeException ex, final WebRequest request) {
BaseResponse responseError = new BaseResponse(HttpStatus.NOT_FOUND.value(),HttpStatus.NOT_FOUND.name(),
Constants.HttpStatusMsg.ERROR_NOT_FOUND);
logger.error(ex.getMessage());
return handleExceptionInternal(ex, responseError, new HttpHeaders(), HttpStatus.NOT_FOUND, request);
}
}
And your code should throw some exception, eg:
if (your_entity == null) {
throw new EntityNotFoundException("said something");
}
If you get this case in somewhere else again, you just throw exception like above. Your handler will take care the rest stuffs.
Hope this help.
I've started your application and works just fine.
Maven :
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
The controller class is :
#Controller
public class ValidationController {
#GetMapping(value = "/validate/{itemId}")
public #ResponseBody ResponseEntity<MyItem> validateItem(#PathVariable Long itemId) {
if (itemId.equals(Long.valueOf(1))) {
throw new ItemNotFoundException();
}
return new ResponseEntity<>(new MyItem(), HttpStatus.OK);
}
#ExceptionHandler(ItemNotFoundException.class)
public ResponseEntity<ExceptionResponse> itemNotFEx(WebRequest webRequest, Exception exception) {
System.out.println("In CREEH::ItemNFE");
ExceptionResponse exceptionResponse = new ExceptionResponse("Item Not Found Ex!!!", new Date(), webRequest.getDescription(false));
ResponseEntity<ExceptionResponse> responseEntity = new ResponseEntity<>(exceptionResponse, HttpStatus.NOT_FOUND);
return responseEntity;
}
}
and the test:
#RunWith(SpringRunner.class)
#WebMvcTest(value = ValidationController.class, secure = false)
public class TestValidationController {
#Autowired
private MockMvc mockMvc;
#Test
public void testExpectNotFound() throws Exception {
mockMvc.perform(get("/validate/1"))
.andExpect(status().isNotFound());
}
#Test
public void testExpectFound() throws Exception {
mockMvc.perform(get("/validate/2"))
.andExpect(status().isOk());
}
}
Are you sure the url you are trying to use with RestTemplate is correct?
String url = "http://localhost:8080/ItemServices/items/validate/{itemId}";
Your get method is #GetMapping(value = "/validate/{itemId}"
If you don't have request mapping at the level of the controller the url should be:
http://localhost:8080/validate/1
Another difference is the missing #ResponseBody on your controller method.
based on this example https://github.com/royclarkson/spring-rest-service-oauth I have modified the source code adding repositories and other rest controllers. The resource and authorization servers are running/configured in the same application.
I have added an endpoint "/api/login" (POST) with request body:
{"usernamme":"foo", "password":"bar"}
I want to get an access token for this user in the service called by the controller. This implementation looks as follows:
Controller
public ResponseEntity<OAuth2AccessToken> login(#RequestBody #Valid final LoginCommand credentials)
throws LoginFailedException, UnexpectedErrorException {
try {
final OAuth2AccessToken token = securityService.authenticate(credentials.getUsername(), credentials.getPassword());
return ResponseEntity.ok(token);
} catch (final InvalidGrantException badCredentialExeption) {
throw new LoginFailedException(badCredentialExeption.getMessage());
} catch (final Exception e) {
throw new UnexpectedErrorException(e.getMessage());
}
}
Service
#Autowired
#Qualifier("OAuth2RestOperations")
private OAuth2RestOperations client;
#Override
public OAuth2AccessToken authenticate(final String username, final String password) {
final ResourceOwnerPasswordResourceDetails resourceDetails = (ResourceOwnerPasswordResourceDetails) client.getResource();
resourceDetails.setUsername(username);
resourceDetails.setPassword(password);
return client.getAccessToken();
}
Rest client config
#Configuration
#Import({ OauthProperties2.class })
#EnableOAuth2Client
public class RestClientConfig {
#Autowired
private OauthProperties2 oauth;
#Bean(name = "OAuth2RestOperations")
public OAuth2RestOperations restTemplate(final OAuth2ClientContext oauth2ClientContext) {
return new OAuth2RestTemplate(resource(), oauth2ClientContext);
}
#Bean
public OAuth2ProtectedResourceDetails resource() {
final ResourceOwnerPasswordResourceDetails resource = new ResourceOwnerPasswordResourceDetails();
resource.setAccessTokenUri(oauth.getClient().getAccessTokenUri());
resource.setClientId(oauth.getClient().getClientId());
resource.setClientSecret(oauth.getClient().getClientSecret());
resource.setGrantType(oauth.getClient().getGrantType());
resource.setScope(oauth.getClient().getScope());
return resource;
}
}
The Test
public class SecurityApiControllerTest extends TestBase {
#InjectMocks
private SecurityApiController controller;
#Test
public void loginOK() throws Exception {
final String credentials = FileUtils.readContent("requests/loginOK.json");
// #formatter:off
mvc.perform(post("/api/login")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(credentials))
.andExpect(status().isOk());
// #formatter:on
}
}
TestBase
#RunWith(SpringJUnit4ClassRunner.class)
#WebAppConfiguration
#SpringBootTest(classes = Application.class)
public class TestBase {
#Autowired
protected WebApplicationContext context;
#Autowired
protected FilterChainProxy springSecurityFilterChain;
protected MockMvc mvc;
#Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mvc = MockMvcBuilders.webAppContextSetup(context).addFilter(springSecurityFilterChain).build();
}
}
When running the application, I can call the endpoint for example with POSTMAN. During the test I get a connection refused as described in question header. I have tried to figure out the reason why the test is not working. Can anybody give me a hint to solve this issue?
It is so weird. It works since I have changed the TestBase class to:
#RunWith(SpringJUnit4ClassRunner.class)
// #WebAppConfiguration
#ContextConfiguration(classes = Application.class)
#SpringBootTest(webEnvironment = WebEnvironment.DEFINED_PORT)
I am using my spring boot REST API with the controller as follows:-
`
#Autowired
HazelcastInstance hazelcastinstance;
String username="VAKSDNDDODM#DLDM#DMOD#DI##*#EK";
#RestController
#RequestMapping(value = "/Acode/availabile/multiple/{Code}/")
public class MultiController {
#Resource(name = "AService")
protected AImpl AService;
#RequestMapping(method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Response> readMulti(
#RequestHeader(value="Auth-Token") String token,
#RequestBody XRequest Request,
#PathVariable String Code) throws Exception {
if(token.equalsIgnoreCase(username))
{
Response response = AService.readMultiX(Request, Code);
}
else
{
response.setMessage("Unauthorized access!");
response.setStatus(340);
}
return new ResponseEntity<Response>(response, HttpStatus.OK);
}
}
`
I wish to do the header comparision before API control enters the controller such that declaring #RequestHeader is not required in controller.Is it possible to do this or do we have an implementation for that?