What is the best way to validate client request using Pageable?
#GetMapping("/items")
fun getItems(
#RequestParam name: String? = null,
pageable: Pageable
): Page<UserItem> {
return userItemService.search(name, pageable)
}
In my case client may sent request with invalid sort direction, for example.
items?name=test&page=0&sort=name,asc1
Related
I haven an endpoint POST /api/marketplace/add that accepts a DTO object as request body. When I send the body below with platformName field set , server accepts request and processes it with no problem. But when I only try to change field platformName to null I get Http 404 error from server. I debugged the request and found out that it even can not reach controller method. I also got no trace from that error. What might be the cause that makes API respond differently to same request?
below
{
"platformName": "Trendyol",
"commissionAmounts": [
{
"amount": 23.45,
"categoryInfos": [
{
"categoryName": "Game"
}
],
"isCategoryBasedPricing": true
}
],
"shipmentAmounts": [
{
"amount": 23.45,
"scaleInfo": {
"order": 0,
"lowerBound": 0,
"upperBound": 0
},
"volumeInfo": {
"order": 0,
"lowerBound": 0,
"upperBound": 0
},
"isVolumeBasedPricing": true
}]
}
EDIT: dto model is
#Generated
public class MarketPlaceDTO {
#JsonProperty("platformName")
private String platformName;
#JsonProperty("commissionAmounts")
#Valid
private List<CommissionInfoDTO> commissionAmounts = new ArrayList<>();
#JsonProperty("shipmentAmounts")
#Valid
private List<ShipmentInfoDTO> shipmentAmounts = new ArrayList<>();
Controller is implementing swagger generated api interface. with postmapping and requestbody annotations.
#RequiredArgsConstructor
#RestController
public class MarketPlaceApiController implements MarketplaceApi {
private final MarketPlaceDAOService marketPlaceDAOService;
#Override
public ResponseEntity<BaseResponseDTO> addMarketPlace(MarketPlaceDTO
marketPlaceDTO) {
BaseResponseDTO dto =
marketPlaceDAOService.addMarketPlace(marketPlaceDTO);
return ResponseEntity.ok(dto);
}
}
Swagger generated api interface
#RequestMapping(
method = RequestMethod.POST,
value = "/marketplace/add",
produces = { "application/json", "application/xml" },
consumes = { "application/json" })
default ResponseEntity<BaseResponseDTO> _addMarketPlace(
#Parameter(name = "MarketPlaceDTO", description = "Add new
marketplace with given request body", required = true) #Valid
#RequestBody MarketPlaceDTO marketPlaceDTO) {
return addMarketPlace(marketPlaceDTO);
}
Response is
{
"timestamp": 1666866382906,
"status": 404,
"error": "Not Found",
"path": "/marketplace/add"
}
Obviously, that you use an endpoint with #RequestBody where body is a DTO.
And on trying to call this endpoint Spring Web first should match that a model in your request payload matches a require object in #RequestBody argument.
Ideally, using DTO as a request model is not a good idea. But I don't see your structure and cannot say if it's a problem or not.
The simple solution in your case is preparation (annotating) your DTO with specific JSON annotations:
#JsonInclude
#JsonIgnoreProperties(ignoreUnknown = true)
public class YourDTO {
private String platformName;
}
and for Controller add class annotation #Validated; for #RequestBody add #Valid annotation.
Recommendation: use request models for incoming objects, and later converters to DTO/entities with ability to response them with filtering (or in complex cases add also response model - usually it's overhead).
My problem was global exception handler component annotated with #ControllerAdvice. I tried to handle validation exceptions and forgot to add #ResponseBody to my handler methods which is in my case probabaly required. That somehow caused server to send http 404 message when any input validation exception was thrown. After I made changes , Exceptions was handled correctly by handler component.
#ControllerAdvice
#ResponseBody // this resolved my issue.
public class MVCExceptionHandler {
#ExceptionHandler(MethodArgumentNotValidException.class)
#ResponseStatus(HttpStatus.BAD_REQUEST)
public BaseErrorResponse
methodArgumentExceptions(MethodArgumentNotValidException e){
return BaseErrorResponse.builder()
.errorMessage(AppError.INVALID_OR_MISSING_USER_INPUT.getErrorMessage())
.errorCode(AppError.INVALID_OR_MISSING_USER_INPUT.getErrorCode())
.errorTime(Date.from(Instant.now())).build();
}
I have some Sprint controller Mapping like below.
#GetMapping("/hello/{name}/age")
private String hello(#PathVariable(value = "name", required = true) String name){
//...
}
#GetMapping("/hello/{name}")
private String hello(#PathVariable(value = "name", required = true) String name){
//...
}
#GetMapping("/name")
private ResponseEntity<?> queryPerson(#RequestParam(value = "query", required = false) String query) {
// ...
}
But there is a client expetection to handle the below case
when client sends /hello/john/age, I should return age related pojo but when client calls /hello//age I have to return 400 with invalid user name as error.
Since im my code I already have other mapping hello/{name} so it is calling this API and trying to find username='age' and returing 404.
Here I am suppossed to 400 when user calls /hello//age, so how to handle this in spring?
There will be no chance to create a 400.
Tomcat is collapsing the double slashes:
http://mail-archives.apache.org/mod_mbox/tomcat-users/201912.mbox/%3C5DEE18E4.4080902#ice-sa.com%3E
I'm trying to use an interface as the RequestBody in Feign, but Feign is creating an empty object as the request. Is this not possible or am I doing something wrong here? I could not find anything on that topic so far.
This is a simplified example of what I am trying to do (in reality there are 3 different kinds of requests)
interface BookingClient {
#RequestLine("POST /booking")
#Headers("Content-Type: application/json")
fun createBooking(request: BookingRequest): BookingResponse
}
interface BookingRequest
data class NormalBooking(
val product: String
): BookingRequest
data class DiscountedBooking(
val product: String,
val discountCode: String
): BookingRequest
// Client Configuration
val client = Feign.builder()
.client(feign.okhttp.OkHttpClient())
.errorDecoder(BadEntityErrorDecoder())
.encoder(JacksonEncoder())
.decoder(JacksonDecoder(listOf(KotlinModule(), JavaTimeModule())))
.logger(feign.Logger.JavaLogger())
.logLevel(feign.Logger.Level.FULL)
.target(BookingClient::class.java, mockServer.getUrl())
If I now call createBooking() with either implementation, Feign always serializes
{}
instead of
{
"product": "productA"
}
and
{
"product": "productA",
"discountCode": "discountCode"
}
The problem is parameter type, when we invoke Feign.target(...), Feign start to parse your BookingClient to a Http request template, and body type is BookingRequest, so Feign always serializes "{}"
interface BookingClient {
#RequestLine("POST /booking")
#Headers("Content-Type: application/json")
// BookingRequest should change to NormalBooking or DiscountedBooking
fun createBooking(request: BookingRequest): BookingResponse
}
I have an application with some controllers that require access control based on the requested resources ids, checking against the Spring Security user Authentication roles. At the moment I have created a function that checks this condition returning a Mono<True> if it is ok (so that I can flatmap it) or an empty Mono (and also setting a 403 status code) otherwise:
#RestController
#RequestMapping("/api/v1/clients/{clientId}/departments/{departmentId}/users")
class UserRestController(private val userService: UserService) {
#GetMapping
fun getAll(principal: Principal, response: ServerHttpResponse,
#PathVariable clientId: String, #PathVariable departmentId: String): Flux<Users> {
return checkDepartmentViewPermissions(principal, response, clientId, departmentId)
.flatMap {
userService.getAll(clientId, departmentId)
}
}
...
}
fun checkDepartmentViewPermissions(principal: Principal, response: ServerHttpResponse,
clientId: String, departmentId: String): Mono<Boolean> {
val authentication = principal as MyAuthentication
authentication.authorities.contains(SimpleGrantedAuthority("${clientId}:${departmentId}")).toMono()
.filter {
it == true
}.switchIfEmpty {
response.statusCode = HttpStatus.FORBIDDEN
Mono.empty()
}
}
As seen above, requests are in the format /api/v1/clients/{clientId}/departments/{departmentId}/users where clientId and departmentId are dynamic path variables.
The checkDepartmentViewPermission method accesses the Authentication roles where users will have a list such as (client1:department1, client1:department2, client2:department1). Thus, a URL /api/v1/clients/client1/departments/department1/users would work fine for those permissions.
Although what I have works, I would like to use a more declarative way to deal with this if it is possible, ideally something based on Spring Security annotations and having into account I need to access PathVariable parameters, something like (I'm making it up):
#RestController
#RequestMapping("/api/v1/clients/{clientId}/departments/{departmentId}/users")
class UserRestController(private val userService: UserService) {
#PreAuthorize("#{principal.roles.contains(clientId:departmentId)}")
#GetMapping
fun getAll(principal: Principal, response: ServerHttpResponse,
#PathVariable clientId: String, #PathVariable departmentId: String): Flux<Users> {
return userService.getAll(clientId, departmentId)
}
...
}
Does Spring Security support a way to do this?
If not, could you suggest any ideas to achieve it?
First off, thanks Thomas for pointing me in the right direction, I hadn't realized we can invoke a Spring bean from the security web expressions. This way, there's no need anymore to inject the principal since the Authentication object will be passed to the bean.
Controller:
#PreAuthorize("#permissionChecker.hasDepartmentViewPermissions(authentication, #clientId, #departmentId)")
#GetMapping
fun getAll(#PathVariable clientId: String, #PathVariable departmentId: String): Flux<Users> {
return userService.getAll(clientId, departmentId)
}
PermissionChecker bean:
class PermissionChecker {
fun hasDepartmentViewPermissions(authentication: Authentication, clientId: String, projectId: String): Boolean {
...
}
Am writing a REST endpoint which needs to support both application/x-www-form-urlencoded and application/json as request body simultaneously. I have made below configuration,
#RequestMapping(method = RequestMethod.POST, produces = { MediaType.APPLICATION_JSON_VALUE }, consumes = {
MediaType.APPLICATION_FORM_URLENCODED_VALUE, MediaType.APPLICATION_JSON_VALUE }, path = Constants.ACCESS_TOKEN_V1_ENDPOINT)
public OAuth2Authorization createAccessTokenPost(
#RequestBody(required = false) MultiValueMap<String, String> paramMap) { ..
While it supports application/x-www-form-urlencoded or application/json individually (when I comment out one content type from consumes = {}), but it does not support both simultaneously. Any ideas ?
So RestControllers by default can handle application/json fairly easily and can create a request pojo from a #RequestBody annotated parameter, while application/x-www-form-urlencoded takes a little more work. A solution could be creating an extra RestController method that has the same mapping endpoint to handle the different kinds of requests that come in (application/json, application/x-www-form-urlencoded, etc). This is because application/x-www-form-urlencoded endpoints need to use the #RequestParam instead of the #RequestBody annotation (for application/json).
For instance if I wanted to host a POST endpoint for /emp that takes either application/json or application/x-www-form-urlencoded as Content-Types and uses a service to do something, I could create Overload methods like so
#Autowired
private EmpService empService;
#PostMapping(path = "/emp", consumes = {MediaType.APPLICATION_FORM_URLENCODED_VALUE})
public ResponseEntity createEmp(final #RequestHeader(value = "Authorization", required = false) String authorizationHeader,
final #RequestParam Map<String, String> map) {
//After receiving a FORM URLENCODED request, change it to your desired request pojo with ObjectMapper
final ObjectMapper mapper = new ObjectMapper();
final TokenRequest tokenRequest = mapper.convertValue(map, CreateEmpRequest.class);
return empService.create(authorizationHeader, createEmpRequest);
}
#PostMapping(path = "/emp", consumes = {MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity createEmp(final #RequestHeader(value = "Authorization", required = false) String authorizationHeader,
final #RequestBody CreateEmpRequest createEmpRequest) {
//Receieved a JSON request, the #RequestBody Annotation can handle turning the body of the request into a request pojo without extra lines of code
return empService.create(authorizationHeader, createEmpRequest);
}
As per my findings, spring does not support content types "application/x-www-form-urlencoded", "application/json" and "application/xml" together.
Reason I figured: Spring processes JSON and XML types by parsing and injecting them into the java pojo marked with #RequestBody spring annotation. However, x-www-form-urlencoded must be injected into a MultiValueMap<> object marked with #RequestBody. Two different java types marked with #RequestBody will not be supported simultaneously, as spring may not know where to inject the payload.
A working solution:
"application/x-www-form-urlencoded" can be supported as it is in the API. That is, it can be injected into spring's MultiValueMap<> using an #RequestBody annotation.
To support JSON and XML on the same method, we can leverage servlet specification and spring's class built on top of them to extract the payload as stream.
Sample code:
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.util.MultiValueMap;
// usual REST service class
#Autowired
private MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter;
#Autowired
private Jaxb2RootElementHttpMessageConverter jaxb2RootElementHttpMessageConverter;
public ResponseEntity<Object> authorizationRequestPost(HttpServletResponse response, HttpServletRequest request,#RequestBody(required = false) MultiValueMap<String, String> parameters) {
// this MultiValueMap<String,String> will contain key value pairs of "application/x-www-form-urlencoded" parameters.
// payload object to be populated
Authorization authorization = null;
HttpInputMessage inputMessage = new ServletServerHttpRequest(request) {
#Override
public InputStream getBody() throws IOException {
return request.getInputStream();
}
};
if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {
authorization = (Authorization) mappingJackson2HttpMessageConverter.read(Authorization.class, inputMessage);
}
else if (request.getContentType().equals(MediaType.APPLICATION_XML_VALUE)) {
authorization = (Authorization)jaxb2RootElementHttpMessageConverter.read(Authorization.class, inputMessage);
}
else{
// extract values from MultiValueMap<String,String> and populate Authorization
}
// remaining method instructions
}
Point to note that any custom data type/markup/format can be supported using this approach. Spring's org.springframework.http.converter.HttpMessageConverter<> can be extended to write the parsing logic.
Another possible approach could be an AOP style solution which would execute the same logic: parse payload by extracting it from HttpServlet input stream and inject into the payload object.
A third approach will be to write a filter for executing the logic.
It's not possible to handle application/json and application/x-www-form-urlencoded requests simultaneously with a single Spring controller method.
Spring get application/x-www-form-urlencoded data by ServletRequest.getParameter(java.lang.String), the document said:
For HTTP servlets, parameters are contained in the query string or posted form data.
If the parameter data was sent in the request body, such as occurs with an HTTP POST request, then reading the body directly via getInputStream() or getReader() can interfere with the execution of this method.
So, if your method parameter is annotated with #RequestBody, Spring will read request body and parse it to the method parameter object. But application/x-www-form-urlencoded leads Spring to populate the parameter object by invoking ServletRequest.getParameter(java.lang.String).
Just to make it, the above answer doesn't work as even if you do not annotate MultiValueMap with #RequestBody it would always check for contentType==MediaType.APPLICATION_FORM_URLENCODED_VALUE which again in rest of the cases resolves to 415 Unsupported Media Type.