I have an abstract crud controller which has a GetMapping and DeleteMapping on the same path which uses pathvariables. Both HttpMethods are defined on the same path and are without body.
The GET works perfectly fine, but for the DELETE i am getting a consistent 405 Method not supported. The original calls are made via JS using the below method
@Transactional
public abstract class BasicController<T extends Dto, D extends PagingAndSortingRepository<J, Long> & JpaSpecificationExecutor<J>, J> {
@GetMapping("/content/{id}")
public ResponseEntity<DtoResponse<T>> handleGetById(@PathVariable(value = "id") Long id) {
try {
checkAuthorization(getPath(), "get");
Optional<J> jpaObject = getJpaObject(id);
if (jpaObject.isPresent()) {
T dto = getJpaToDtoMapper().apply(jpaObject.get());
DtoResponse<T> response = new DtoResponse<>(List.of(dto));
return new ResponseEntity<>(response, HttpStatus.OK);
} else {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
} catch (AuthorizationException e) {
log.error("User not authorized to get record by Id config for page {}", getPath(), e);
return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
}
}
@DeleteMapping("/content/{id}")
public ResponseEntity<DtoResponse<String>> handleDelete(@PathVariable(value = "id") Long id) {
try {
checkAuthorization(getPath(), "delete");
performValidationsBeforeDelete(id);
if (dao.existsById(id)) {
dao.deleteById(id);
return new ResponseEntity<>(HttpStatus.OK);
} else {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
} catch (BasicValidationException e) {
log.error("User not authorized to delete {} because of {}", id, e.getErrors(), e);
return new ResponseEntity<>(new DtoResponse<>(e.getErrors(), false), HttpStatus.BAD_REQUEST);
} catch (AuthorizationException e) {
log.error("User not authorized to delete {}", id, e);
return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
}
}
and the implementation:
@Controller
@RequestMapping(UserRoleController.PATH)
public class UserRoleController extends BasicController<UserRegistrationDto, UserDao, User> {
protected static final String PATH = "maintenance/userrole";
protected UserRoleController(UserDao dao) {
super(dao);
}
@Override
public Function<User, UserRegistrationDto> getJpaToDtoMapper() {
return user -> {
UserRegistrationDto dto = new UserRegistrationDto();
dto.setFirstName(user.getFirstName());
dto.setLastName(user.getLastName());
dto.setUserName(user.getUserName());
dto.setEmail(user.getEmail());
dto.setPassword("f-off");
dto.setConfirmedPassword("you wished");
dto.setUserRolesDtoList(new ArrayList<>());
for (Role role : user.getRole()) {
UserRolesDto userRolesDto = new UserRolesDto();
userRolesDto.setUserId(role.getId());
userRolesDto.setRoleId(role.getId());
dto.getUserRolesDtoList().add(userRolesDto);
}
return dto;
};
}
@Override
public Function<UserRegistrationDto, User> getDtoToJpaMapper() {
return dto -> new User(dto.getFirstName(), dto.getLastName(), dto.getUserName(), dto.getEmail(), dto.getPassword(), "enc", 0, new Date());
}
@Override
public Class<User> getJpaClass() {
return User.class;
}
@Override
public Class<UserRegistrationDto> getDtoClass() {
return UserRegistrationDto.class;
}
@Override
protected void performValidationsBeforeCreate(User newModel, HttpServletRequest request) throws BasicValidationException {
//do nothing
}
@Override
protected void performValidationsBeforeUpdate(User existingModel, User newModel, HttpServletRequest request) throws BasicValidationException {
//do nothing
}
@Override
protected void performValidationsBeforeDelete(Long id) throws BasicValidationException {
//do nothing
}
@Override
public void handleAction() {
//do nothing
}
@Override
public String getPath() {
return PATH;
}
}
And the JavaScript doing the calling. The _this.pageUrl contains the "userrole" string while being on the "http://localhost:8080/maintenance/userrole" page. This results in a call to URL as also visible in Network tab of Chrome Developer.
DELETE http://localhost:8080/maintenance/userrole/content/1
The url is fine, when using GET to this URL it works and the path is defined exactly the same way...
this.pageConfig.sourceConfig.deleterow = function (rowid, commit) {
let xmlHttp = new XMLHttpRequest();
xmlHttp.onreadystatechange = () => {
if (xmlHttp.readyState === 4 && xmlHttp.status === 200) {
commit(true);
} else if (xmlHttp.readyState === 4 && xmlHttp.status !== 200) {
//todo: handle exceptions properly
commit(false);
}
}
xmlHttp.open("DELETE", _this.pageUrl+"/content/"+rowid, false); // true for asynchronous
xmlHttp.send();
};
When doing a GET I am getting the expected object. When using POSTMAN and doing a GET, it works as well, when doing a OPTIONS on the URL, i am getting the following ALLOW header values: DELETE,GET,HEAD,OPTIONS.
If I remove the DeleteMapping, then the DELETE is not availabe on the ALLOW header values when using OPTIONS. In other words, Spring is exposing that DELETE method, but it's just now able to resolve it somehow
In Postman, using DELETE, i have tried all the body types that could be defined, all with the same 405 result. (note that the body was always empty)
I have also tried settings the spring.mvc.hiddenmethod.filter.enabled=true but this did not have any effect either.
Furthermore i tried to create a simple controller with only a DeleteMapping method without any fuzz (no abstraction and so), i was getting a 405 on this as well.
What am I missing, is there any specific configuration required to enable DELETE? Why is Spring not able to resolve it...
I remember with POST i was having a similar issue and setting the Consumes and Produces attribute worked, but in this scenario I am not consuming any specific data as the parametesr are coming from the path variable.