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.
CodePudding user response:
did you try to add @ResponseBody
as @Controller
need this annotation for each method while @RestController
eliminates this annotation
CodePudding user response:
Ok, after doing some debugging and trial and error i finally found the problem. I have CSRF enabled and I was not passing the CSRF token as part of the header. This could have easily been identified by enabling DEBUG logs from Spring. The interesting bit is why was I getting the 405 then instead of the 403?
Well, this is because my initial request got rejected with a 403, then Spring tried to redirect me to the /Error controller using the same HTTP Method (DELETE) which obviously does not exist for the /Error page.... As a result i was seeing only the 405 in the response and the info logs.
When setting loglevel of Spring to debug I found this, which clearly shows what was going on... Lessons learned: enable the debug logs.
2022-03-08 18:01:48,540 DEBUG [http-nio-8080-exec-7] [] o.s.s.w.FilterChainProxy: Securing DELETE /maintenance/userrole/content/1
2022-03-08 18:01:48,540 DEBUG [http-nio-8080-exec-7] [] o.s.s.w.c.HttpSessionSecurityContextRepository: Retrieved SecurityContextImpl [Authentication=UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=admin, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_ADMIN]], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=6189DF303212ABD9109CF706ACAFD75E], Granted Authorities=[ROLE_ADMIN]]]
2022-03-08 18:01:48,540 DEBUG [http-nio-8080-exec-7] [] o.s.s.w.c.SecurityContextPersistenceFilter: Set SecurityContextHolder to SecurityContextImpl [Authentication=UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=admin, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_ADMIN]], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=6189DF303212ABD9109CF706ACAFD75E], Granted Authorities=[ROLE_ADMIN]]]
2022-03-08 18:01:48,540 DEBUG [http-nio-8080-exec-7] [] o.s.s.w.c.CsrfFilter: Invalid CSRF token found for http://localhost:8080/maintenance/userrole/content/1
2022-03-08 18:01:48,540 DEBUG [http-nio-8080-exec-7] [] o.s.s.w.a.AccessDeniedHandlerImpl: Responding with 403 status code
2022-03-08 18:01:48,540 DEBUG [http-nio-8080-exec-7] [] o.s.s.w.c.SecurityContextPersistenceFilter: Cleared SecurityContextHolder to complete request
2022-03-08 18:01:48,540 DEBUG [http-nio-8080-exec-7] [] o.s.s.w.FilterChainProxy: Securing DELETE /error
2022-03-08 18:01:48,541 DEBUG [http-nio-8080-exec-7] [] o.s.s.w.c.HttpSessionSecurityContextRepository: Retrieved SecurityContextImpl [Authentication=UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=admin, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_ADMIN]], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=6189DF303212ABD9109CF706ACAFD75E], Granted Authorities=[ROLE_ADMIN]]]
2022-03-08 18:01:48,541 DEBUG [http-nio-8080-exec-7] [] o.s.s.w.c.SecurityContextPersistenceFilter: Set SecurityContextHolder to SecurityContextImpl [Authentication=UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=admin, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_ADMIN]], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=6189DF303212ABD9109CF706ACAFD75E], Granted Authorities=[ROLE_ADMIN]]]
2022-03-08 18:01:48,541 DEBUG [http-nio-8080-exec-7] [] o.s.s.w.a.r.RememberMeAuthenticationFilter: SecurityContextHolder not populated with remember-me token, as it already contained: 'UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=admin, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_ADMIN]], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=6189DF303212ABD9109CF706ACAFD75E], Granted Authorities=[ROLE_ADMIN]]'
2022-03-08 18:01:48,541 DEBUG [http-nio-8080-exec-7] [] o.s.s.a.i.AbstractSecurityInterceptor: Authorized filter invocation [DELETE /error] with attributes [permitAll]
2022-03-08 18:01:48,541 DEBUG [http-nio-8080-exec-7] [] o.s.s.w.FilterChainProxy$VirtualFilterChain: Secured DELETE /error
2022-03-08 18:01:48,542 DEBUG [http-nio-8080-exec-7] [] o.s.c.l.LogFormatUtils: "ERROR" dispatch for DELETE "/error", parameters={}
2022-03-08 18:01:48,542 WARN [http-nio-8080-exec-7] [] o.s.w.s.h.AbstractHandlerExceptionResolver: Resolved [org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'DELETE' not supported]
2022-03-08 18:01:48,543 DEBUG [http-nio-8080-exec-7] [] o.s.w.s.FrameworkServlet: Exiting from "ERROR" dispatch, status 405
2022-03-08 18:01:48,543 DEBUG [http-nio-8080-exec-7] [] o.s.s.w.c.SecurityContextPersistenceFilter: Cleared SecurityContextHolder to complete request