We have built an API in which in our model We have a field
#Min(1) #Max(16)
private Long demoField;
When we provide 17 to demoField
it will throw us an error on the client-side
"must be less than or equal to 16"
But when we see the violation message it includes the field name and the message looks like
"demoField: must be less than or equal to 16"
So the question of why we are not getting field name in the client-side error message.
Am I missing something?
API built on spring boot
It's not passed by default. You could implement your own error handler to customize the message passed back, by using #ControllerAdvice for example.
One way is to just specify the message:
#Min(value = 5, message="Age must be at least 5")
In which case in the #ControllerAdvice, you would just need to read getDefaultMessage()
If you don't want to manually add default messages, the approach would be to implement something along the lines of (with appropriate null checks etc):
package com.example.demo;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
#ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
#Override
protected ResponseEntity<Object> handleBindException(
BindException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
return new ResponseEntity<>(
ex.getFieldError().getField() + ": " + ex.getFieldError().getDefaultMessage(),
headers,
status);
}
}
Related
when using spring boot upload file with parameter, I used #RequestPart for all parameter.
Here the code:
spring boot: 2.7.8
when one of them is String and the other is Integer and both of them annotated by #RequestPart will be cause Exception
package com.interfaces.anti.mage.api;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.interfaces.anti.mage.model.Address;
import com.interfaces.anti.mage.model.Order;
import com.interfaces.anti.mage.service.OrderService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
/**
* #author dengbojing
*/
#RestController
#RequestMapping("/order")
public class OrderApi {
#PostMapping("/upload")
public String upload(#RequestPart("file")MultipartFile file, #RequestPart("id") String id, #RequestPart("number") Integer number) {
System.out.println(id);
System.out.println(number);
return "success";
}
}
Exception info:
2023-02-13 22:51:40,319 WARN [http-nio-8099-exec-2] org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver: Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'application/octet-stream' not supported]
when all the other parameter type is String and annotated by #RequestPart, the method will be worked and print the info.
#PostMapping("/upload")
public String upload(#RequestPart("file")MultipartFile file, #RequestPart("id") String id, #RequestPart("number") String number) {
System.out.println(id);
System.out.println(number);
return "success";
}
So why this? the exception means the program treat the String and Integer paramter as Stream? but why all String will be fine?
ps: even change to this #PostMapping(value = "/upload", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE, MediaType.APPLICATION_JSON_VALUE}) also got the same problem
ps: this is the request info:
I take the chance of this post because it pointed me to the right direction anyway so, for sake of anyone using Postman, receiving the error above here is my hint, learnt with pain and solved thanks to the hints above.
In Postman, when you fill a form-data body, the table misses, by default I believe, the CONTENT TYPE column.
You can add the column the using the button on the right side, but then it will show Auto consequently, depending from your parameters, you should change it to the proper type.
I hope it may help others :)
Regards
I have the following GET REST method:
import java.time.OffsetDateTime;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import com.product.rest.api.TransactionsApi;
import com.product.rest.model.Transaction;
#Path("/transactions")
#Api(description = "the transactions API")
#Consumes({ "application/json" })
#Produces({ "application/json" })
public class TransactionsApiImpl extends TransactionsApi {
#GET
#Consumes({ "application/json" })
#Produces({ "application/json" })
#ApiOperation(value = "", notes = "Get all transactions", response = Transaction.class, responseContainer = "List", tags = {})
#ApiResponses(
value = { #ApiResponse(code = 200, message = "OK", response = Transaction.class, responseContainer = "List"),
#ApiResponse(code = 400, message = "Bad Request", response = Transaction.class, responseContainer = "List"),
#ApiResponse(code = 404, message = "Not Found", response = Transaction.class, responseContainer = "List"),
#ApiResponse(code = 500, message = "Internal Server Error", response = Transaction.class, responseContainer = "List") })
#Override
public Response transactionsGet(
#HeaderParam("tok") String tok,
#QueryParam("param1") Integer param1,
#QueryParam("param2") String param2,
#QueryParam("param3") OffsetDateTime param3,
#QueryParam("param4") OffsetDateTime param4,
#QueryParam("param5") Integer param5,
#QueryParam("param6") Integer param6,
#QueryParam("param7") String param7) {
return Response.ok().entity("Success!").build();
}
The TransactionsApi is a generated implementation using Swagger Codegen, as is the Transaction model class. I have several other functions in this class, but whenever I leave the GET /transactions function uncommented, I receive the following error:
WARN [Thread-1] (ContextHandler.java:2175) - unavailable
org.glassfish.jersey.server.model.ModelValidationException: Validation of the application resource model has failed during application initialization.
[[FATAL] No injection source found for a parameter of type public javax.ws.rs.core.Response
com.product.rest.impl.v1.TransactionsApiImpl.transactionsGet(java.lang.String,java.lang.Integer,java.lang.String,java.time.OffsetDateTime,java.time.OffsetDateTime,java.lang.Integer,java.lang.Integer,java.lang.String) at index 3.; source='ResourceMethod{httpMethod=GET, consumedTypes=[application/json], producedTypes=[application/json], suspended=false, suspendTimeout=0, suspendTimeoutUnit=MILLISECONDS, invocable=Invocable{handler=ClassBasedMethodHandler{handlerClass=class com.product.rest.impl.v1.TransactionsApiImpl, handlerConstructors=[org.glassfish.jersey.server.model.HandlerConstructor#7df78e88]}, definitionMethod=public javax.ws.rs.core.Response
All other similar questions I have found had to do with MultiPart Data and file uploading, whereas I am making a simple GET request. Other functions that also use the javax.ws.rs.code.Response class do not have this issue and the server starts normally.
I have noticed that the problem happens whenever the OffsetDateTime class is in the parameters (i.e. param3 and param4), but I have been unable to find out why. Moreover, OffsetDateTime was chosen by Swagger Codegen and I am reluctant to change it seeing how I will have to change every derived file afterwards whenever I regenerate my sources.
Has anyone had this issue before with REST services and OffsetDateTime?
All other similar questions I have found had to do with MultiPart Data and file uploading
It's related. The error is a general error you get when Jersey can't validate the resource model. Part of the resource model is the method parameters. Jersey has a system for knowing which parameters it will be able to process and which ones it won't. In your case, it doesn't know how to process the OffsetDateTime.
There are a set of rules that you need to follow in order to able to use non basic types as #QueryParams (and all other #XxxParams such as #PathParam and #FormParam, etc.):
Be a primitive type
Have a constructor that accepts a single String argument
Have a static method named valueOf or fromString that accepts a single String argument (see, for example, Integer.valueOf(String))
Have a registered implementation of ParamConverterProvider JAX-RS extension SPI that returns a ParamConverter instance capable of a "from string" conversion for the type.
Be List<T>, Set<T> or SortedSet<T>, where T satisfies 2, 3 or 4 above. The resulting collection is read-only.
So in this case of OffsetDateTime, going down the list; it's not a primitive; it doesn't have a String constructor; it doesn't have a static valueOf or fromString
So basically, the only option left is to implement a ParamConverter/ParamConverterProvider for it. The basic set up looks like
#Provider
public class OffsetDateTimeProvider implements ParamConverterProvider {
#Override
public <T> ParamConverter<T> getConverter(Class<T> clazz, Type type, Annotation[] annotations) {
if (clazz.getName().equals(OffsetDateTime.class.getName())) {
return new ParamConverter<T>() {
#SuppressWarnings("unchecked")
#Override
public T fromString(String value) {
OffsetDateTime time = ...
return (T) time;
}
#Override
public String toString(T time) {
return ...;
}
};
}
return null;
}
}
Jersey will pass you the String value of the query parameter, and it's your job to to create it and return it.
Then just register the OffsetDateTimeProvider with the application. If you're using package scanning, it should be picked up and registered automatically from the #Provider annotation.
I don't use Swagger, so I don't know if they already provide something like this already implemented, but it seems odd that they would generate this for you, and not have a way to make it work. I know Jersey 3 will have Java 8 support out the box, but who know when the heck that's gonna be released.
I have a class which checks the health status of a REST-Interface. It works nice but my log files are full of these warnings:
2017-08-23 03:59:58.707 WARN 1849 --- [io-13811-exec-5] o.a.h.c.protocol.ResponseProcessCookies : Cookie rejected [JSESSIONID="14747303A2F23D4BE6DBAE0F282DEA94", version:0, domain:dealersearch.....com, path:/DCRMBroker/, expiry:null] Illegal 'path' attribute "/DCRMBroker/". Path of origin: "/system/healthcheck.jsp"
It seems the requests do not accept any cookies. I do not understand what is going on and how to accept them to get around the masses of log messages.
My health check class:
package com......commons.health;
import com.....commons.health.HealthResult.Health;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.web.client.RestOperations;
public abstract class RestHealthCheck extends HealthCheck {
private static final Logger LOGGER = LoggerFactory.getLogger(RestHealthCheck.class);
public RestHealthCheck() {}
public abstract String getUrl();
public abstract RestOperations getRestOperations();
public HealthResult getHealthResult() {
HealthResult result = new HealthResult();
result.setName(this.getName());
result.setHealth(Health.HEALTHY);
result.setMessage((String)null);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.TEXT_PLAIN);
HttpEntity entity = new HttpEntity(headers);
try {
this.getRestOperations().exchange(this.getUrl(), HttpMethod.HEAD, entity, String.class, new Object[0]);
} catch (Exception var5) {
LOGGER.trace("Health check failed", var5);
result.setHealth(Health.UNHEALTHY);
result.setMessage(var5.getMessage());
}
return result;
}
}
An URL http://server.example.com/system/healthcheck.jsp is requested and the response contained a cookie that is defined for the URL http://server.example.com/DCRMBroker/.
The URLs are different and a client worth its name should reject that which happens here leading to the logs. The information you gave is not enough to say for sure who the culprit is. It can be the JSP-page setting a wrong cookie, it might be a forwarding rule on the server, so the JSP-page is internally forwarded to a different resource without changing the path within the response. It might also be something on your client where some kind of redirection takes place leading to this effect.
Maybe my explanations help you to track the reason down. If it's something on your side, you can fix that. If it's something on the other side, you might get in contact with them in order to let that fix that. Alternatively you can check your client if you can switch off the warning for this particular case so your log becomes calm again.
I have the following GET REST method:
import java.time.OffsetDateTime;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import com.product.rest.api.TransactionsApi;
import com.product.rest.model.Transaction;
#Path("/transactions")
#Api(description = "the transactions API")
#Consumes({ "application/json" })
#Produces({ "application/json" })
public class TransactionsApiImpl extends TransactionsApi {
#GET
#Consumes({ "application/json" })
#Produces({ "application/json" })
#ApiOperation(value = "", notes = "Get all transactions", response = Transaction.class, responseContainer = "List", tags = {})
#ApiResponses(
value = { #ApiResponse(code = 200, message = "OK", response = Transaction.class, responseContainer = "List"),
#ApiResponse(code = 400, message = "Bad Request", response = Transaction.class, responseContainer = "List"),
#ApiResponse(code = 404, message = "Not Found", response = Transaction.class, responseContainer = "List"),
#ApiResponse(code = 500, message = "Internal Server Error", response = Transaction.class, responseContainer = "List") })
#Override
public Response transactionsGet(
#HeaderParam("tok") String tok,
#QueryParam("param1") Integer param1,
#QueryParam("param2") String param2,
#QueryParam("param3") OffsetDateTime param3,
#QueryParam("param4") OffsetDateTime param4,
#QueryParam("param5") Integer param5,
#QueryParam("param6") Integer param6,
#QueryParam("param7") String param7) {
return Response.ok().entity("Success!").build();
}
The TransactionsApi is a generated implementation using Swagger Codegen, as is the Transaction model class. I have several other functions in this class, but whenever I leave the GET /transactions function uncommented, I receive the following error:
WARN [Thread-1] (ContextHandler.java:2175) - unavailable
org.glassfish.jersey.server.model.ModelValidationException: Validation of the application resource model has failed during application initialization.
[[FATAL] No injection source found for a parameter of type public javax.ws.rs.core.Response
com.product.rest.impl.v1.TransactionsApiImpl.transactionsGet(java.lang.String,java.lang.Integer,java.lang.String,java.time.OffsetDateTime,java.time.OffsetDateTime,java.lang.Integer,java.lang.Integer,java.lang.String) at index 3.; source='ResourceMethod{httpMethod=GET, consumedTypes=[application/json], producedTypes=[application/json], suspended=false, suspendTimeout=0, suspendTimeoutUnit=MILLISECONDS, invocable=Invocable{handler=ClassBasedMethodHandler{handlerClass=class com.product.rest.impl.v1.TransactionsApiImpl, handlerConstructors=[org.glassfish.jersey.server.model.HandlerConstructor#7df78e88]}, definitionMethod=public javax.ws.rs.core.Response
All other similar questions I have found had to do with MultiPart Data and file uploading, whereas I am making a simple GET request. Other functions that also use the javax.ws.rs.code.Response class do not have this issue and the server starts normally.
I have noticed that the problem happens whenever the OffsetDateTime class is in the parameters (i.e. param3 and param4), but I have been unable to find out why. Moreover, OffsetDateTime was chosen by Swagger Codegen and I am reluctant to change it seeing how I will have to change every derived file afterwards whenever I regenerate my sources.
Has anyone had this issue before with REST services and OffsetDateTime?
All other similar questions I have found had to do with MultiPart Data and file uploading
It's related. The error is a general error you get when Jersey can't validate the resource model. Part of the resource model is the method parameters. Jersey has a system for knowing which parameters it will be able to process and which ones it won't. In your case, it doesn't know how to process the OffsetDateTime.
There are a set of rules that you need to follow in order to able to use non basic types as #QueryParams (and all other #XxxParams such as #PathParam and #FormParam, etc.):
Be a primitive type
Have a constructor that accepts a single String argument
Have a static method named valueOf or fromString that accepts a single String argument (see, for example, Integer.valueOf(String))
Have a registered implementation of ParamConverterProvider JAX-RS extension SPI that returns a ParamConverter instance capable of a "from string" conversion for the type.
Be List<T>, Set<T> or SortedSet<T>, where T satisfies 2, 3 or 4 above. The resulting collection is read-only.
So in this case of OffsetDateTime, going down the list; it's not a primitive; it doesn't have a String constructor; it doesn't have a static valueOf or fromString
So basically, the only option left is to implement a ParamConverter/ParamConverterProvider for it. The basic set up looks like
#Provider
public class OffsetDateTimeProvider implements ParamConverterProvider {
#Override
public <T> ParamConverter<T> getConverter(Class<T> clazz, Type type, Annotation[] annotations) {
if (clazz.getName().equals(OffsetDateTime.class.getName())) {
return new ParamConverter<T>() {
#SuppressWarnings("unchecked")
#Override
public T fromString(String value) {
OffsetDateTime time = ...
return (T) time;
}
#Override
public String toString(T time) {
return ...;
}
};
}
return null;
}
}
Jersey will pass you the String value of the query parameter, and it's your job to to create it and return it.
Then just register the OffsetDateTimeProvider with the application. If you're using package scanning, it should be picked up and registered automatically from the #Provider annotation.
I don't use Swagger, so I don't know if they already provide something like this already implemented, but it seems odd that they would generate this for you, and not have a way to make it work. I know Jersey 3 will have Java 8 support out the box, but who know when the heck that's gonna be released.
We have an issue where embedded Tomcat is throwing IllegalArgumentException from the LegacyCookieProcessor. It throws a 500 HTTP response code.
We need to handle the exception and do something with it (specifically, send it as a 400 instead).
The typical #ExceptionHandler(IllegalArgumentException.class) doesn't seem to get triggered and Google only seems to give results for dealing with Spring Boot specific exceptions.
Example:
Here is an example to reproduce the behavior. You can execute the example by downloading the initial project including spring-web (https://start.spring.io/) in version 2.1.5.RELEASE. Then add the following two classes to your project.
DemoControllerAdvice.java
package com.example.demo;
import java.util.HashMap;
import java.util.Map;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
#RestControllerAdvice
public class DemoControllerAdvice {
#ExceptionHandler(IllegalArgumentException.class)
#ResponseStatus(HttpStatus.FORBIDDEN)
public Map<String, String> forbiddenHandler() {
Map<String, String> map = new HashMap<>();
map.put("error", "An error occurred.");
map.put("status", HttpStatus.FORBIDDEN.value() + " " + HttpStatus.FORBIDDEN.name());
return map;
}
}
DemoRestController.java
package com.example.demo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
#RestController
public class DemoRestController {
#GetMapping(value = "/working")
public void working() {
throw new java.lang.IllegalArgumentException();
}
#GetMapping(value = "/not-working")
public String notWorking(#RequestParam String demo) {
return "You need to pass e.g. the character ^ as a request param to test this.";
}
}
Then, start the server and request the following URLs in the browser:
http://localhost:8080/working An IllegalArgumentException is thrown manually in the controller. It is then caught by the ControllerAdvice and will therefore produce a JSON string containing the information defined in the DemoControllerAdvice
http://localhost:8080/not-working?demo=test^123 An IllegalArgumentException is thrown by the Tomcat, because the request param cannot be parsed (because of the invalid character ^). The exception however is not caught by the ControllerAdvice. It shows the default HTML page provided by Tomcat. It also provides a different error code than defined in the DemoControllerAdvice.
In the logs the following message is shown:
o.apache.coyote.http11.Http11Processor : Error parsing HTTP request header
Note: further occurrences of HTTP request parsing errors will be logged at DEBUG level.
java.lang.IllegalArgumentException: Invalid character found in the request target. The valid characters are defined in RFC 7230 and RFC 3986
at org.apache.coyote.http11.Http11InputBuffer.parseRequestLine(Http11InputBuffer.java:467) ~[tomcat-embed-core-9.0.19.jar:9.0.19]
This is a feature of Tomcat itself as mentioned in this answer.
However, you can do something like this by allowing the special characters that you are expecting as part of your request and handle them yourself.
First, you need to allow the special characters that you would need to handle by setting up the relaxedQueryChars like this.
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.stereotype.Component;
#Component
public class TomcatCustomizer implements
WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
#Override
public void customize(TomcatServletWebServerFactory factory) {
factory.addConnectorCustomizers((connector) -> {
connector.setAttribute("relaxedQueryChars", "^");
});
}
}
and later handle the special characters in each of your requests or create an interceptor and handle it in a common place.
To handle it in the request individually you can do something like this.
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
#RestController
public class DemoRestController {
#GetMapping(value = "/working")
public void working() {
throw new java.lang.IllegalArgumentException();
}
#GetMapping(value = "/not-working")
public String notWorking(#RequestParam String demo) {
if (demo.contains("^")) {
throw new java.lang.IllegalArgumentException("^");
}
return "You need to pass e.g. the character ^ as a request param to test this.";
}
}
You might also want to refer this answer to decide if you really need this fix.
Try to catch the IllegalArgumentException in your filter, then call HttpServletResponse.sendError(int sc, String msg);. This may catch the IllegalArgumentExceptions that do not come from Tomcat though. But I suppose you already handle them properly.