How Do I Customize Spring Error Timestamps Without Causing Exceptions? - spring-boot

When there are errors in my backend, SpringBoot returns a json result with the errors.
I wanted to change the timestamp in the json to be readable so I customised the attributes:
#Component
public class CustomErrorAttributes extends DefaultErrorAttributes {
private static final DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
private static final String TIMESTAMP = "timestamp";
#Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
//Let Spring handle the error first
Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, includeStackTrace);
//Format & update timestamp
Object timestamp = errorAttributes.get(TIMESTAMP);
if(timestamp == null) {
errorAttributes.put(TIMESTAMP, dateFormat.format(new Date()));
} else {
errorAttributes.put(TIMESTAMP, dateFormat.format((Date)timestamp));
}
return errorAttributes;
}
}
However, now when there is a 404, an Exception is thrown because of this customisation:
E 23:21:36 88 [dispatcherServlet].log - Servlet.service() for servlet [dispatcherServlet] threw exception
java.lang.ClassCastException: java.lang.String cannot be cast to java.util.Date
at org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration$StaticView.render(ErrorMvcAutoConfiguration.java:224) ~[spring-boot-autoconfigure-2.1.4.RELEASE.jar:2.1.4.RELEASE]
The ErrorMvcAutoConfiguration is trying to parse the timestamp attribute:
#Override
public void render(Map<String, ?> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
if (response.isCommitted()) {
String message = getMessage(model);
logger.error(message);
return;
}
StringBuilder builder = new StringBuilder();
Date timestamp = (Date) model.get("timestamp");
How can I customize the timestamp in the error response without affecting other standard error processing?

It looks like you're working with an outdated version of spring mvc.
And we're talking about ErrorMvcAutoConfiguration.java
There was a bug:
See here
It was fixed in a commit d1d953819ac9f0c0ece5160b96899030cabda46c on Sep 12, 2020 into spring boot repository by Phil Webb into version 2.2.x and it's also been forward merged to 2.3.x and 2.4.x.
So now it looks like:
private static class StaticView implements View {
private static final MediaType TEXT_HTML_UTF8 = new MediaType("text", "html", StandardCharsets.UTF_8);
private static final Log logger = LogFactory.getLog(StaticView.class);
#Override
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
throws Exception {
if (response.isCommitted()) {
String message = getMessage(model);
logger.error(message);
return;
}
response.setContentType(TEXT_HTML_UTF8.toString());
StringBuilder builder = new StringBuilder();
// !!!note this line!!!!
Object timestamp = model.get("timestamp");
Object message = model.get("message");
Object trace = model.get("trace");
if (response.getContentType() == null) {
response.setContentType(getContentType());
}
builder.append("<html><body><h1>Whitelabel Error Page</h1>").append(
"<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>")
// !!! and this line !!!
.append("<div id='created'>").append(timestamp).append("</div>")
.append("<div>There was an unexpected error (type=").append(htmlEscape(model.get("error")))
.append(", status=").append(htmlEscape(model.get("status"))).append(").</div>");
if (message != null) {
builder.append("<div>").append(htmlEscape(message)).append("</div>");
}
if (trace != null) {
builder.append("<div style='white-space:pre-wrap;'>").append(htmlEscape(trace)).append("</div>");
}
builder.append("</body></html>");
response.getWriter().append(builder.toString());
Having said that, usually people customize the error page (from my experience at least) and do not even use this default view.
There are plenty of tutorials about that, here is one of them

Related

Spring Boot app counts two error requests

I got Spring boot app and used spring aop to count my last 100 rerequestslso I used the Spring boot error page and just added the error template to the code and its works. the problem is that the error page is counted twice. I guess it counted /error and some wrong url like /somewrongUrl. how can I solve this?
#Aspect
#Component
#Slf4j
public class RequestLoggingAspect {
private List<HttpModel> requests = new ArrayList<>();
#Before("within(#org.springframework.stereotype.Controller *)")
public void logRequest(JoinPoint joinPoint) {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
HttpServletResponse response = requestAttributes.getResponse();
HttpModel httpModel = new HttpModel();
httpModel.setMethod(request.getMethod());
httpModel.setUri(request.getRequestURI());
httpModel.setStatusCode(response.getStatus());
httpModel.setTimeStamp(getTime(request));
if(!httpModel.getUri().equals("/getLogg")) {
requests.add(httpModel);
}
}
public List<HttpModel> getRequests() {
int size = requests.size();
if (size > 100) {
return requests.subList(size - 100, size);
}
return requests;
}
public String getTime(HttpServletRequest request) {
long time = request.getDateHeader("Date");
if (time == -1) {
time = Instant.now().toEpochMilli();
}
Date date = new Date(time);
SimpleDateFormat dateFormat = new SimpleDateFormat("kk:mm dd.MM.yyyy");
return dateFormat.format(date);
}

Springboot show error message for invalid date (YearMonth) formats: eg 2020-15

I have a project with Spring Boot and I want to show an error response if the given date format is incorrect.
The correct format is yyyy-MM (java.time.YearMonth) but I want to want to show a message if someone sends 2020-13, 2020-111 or 2020-1.
When I've added a custom validator the debugger goes in there with a valid request but not with an incorrect request. I also tried to use the message.properties with the typeMismatch.project.startdate=Please enter a valid date. but I also don't see that message in my response body.
It seems like the application does not understand my incorrect request and then always throws a BAD REQUEST with empty body, which is not strange because it is not a valid date.
Can someone explain me how I can show an errormessage in the response for these incorrect values?
Or is there no other way then use a String and convert that to the YearMonth object so I can show catch and show an error message?
Request object:
#Getter
#Setter
public class Project {
#NotNull(message = "mandatory")
#DateTimeFormat(pattern = "yyyy-MM")
private YearMonth startdate;
}
Controller:
#RestController
#RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE)
public class ProjectController {
#PostMapping(value = "/project", consumes = MediaType.APPLICATION_JSON_VALUE)
public Project newProject(#Valid #RequestBody Project newProject) {
return projectService.newProject(newProject);
}
}
ExceptionHandler:
#RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
#SneakyThrows
#Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
headers.add("Content-Type", "application/json");
ObjectMapper mapper = new ObjectMapper();
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach(error -> {
String name;
if (error instanceof FieldError)
name = ((FieldError) error).getField();
else
name = error.getObjectName();
String errorMessage = error.getDefaultMessage();
errors.put(name, errorMessage);
});
return new ResponseEntity<>(mapper.writeValueAsString(errors), headers, status);
}
}
Okay, I made a solution which is workable for me.
I've added the solution below for people who find this thread in the future and has the same problem I had.
Create a custom validator with a simple regex pattern:
#Target({ FIELD })
#Retention(RUNTIME)
#Constraint(validatedBy = YearMonthValidator.class)
#Documented
public #interface YearMonthPattern {
String message() default "{YearMonth.invalid}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
public class YearMonthValidator implements ConstraintValidator<YearMonthPattern, String> {
#Override
public boolean isValid(String value, ConstraintValidatorContext context) {
Pattern pattern = Pattern.compile("^([0-9]{4})-([0-9]{2})$");
Matcher matcher = pattern.matcher(value);
try {
return matcher.matches();
} catch (Exception e) {
return false;
}
}
}
Update the request object:
#Getter
#Setter
public class Project {
#NotNull(message = "mandatory")
#YearMonthPattern
private String startdate;
public YearMonth toYearMonth(){
return YearMonth.parse(startdate);
}
}
The DateTimeFormat annotation is replaced with our new custom validator and instead of a YearMonth, make it a String. Now the validator annotation can be executed because the mapping to the YearMonth won't fail anymore.
We also add a new method to convert the String startdate to a YearMonth after Spring has validated the request body, so we can use it in the service as a YearMonth instead of having to translate it each time.
Now when we send a requestbody with:
{
"startdate": "2020-1"
}
we get a nice 400 bad request with the following response:
{
"endDate": "{YearMonth.invalid}"
}

Could not write JSON: JsonObject; nested exception is com.fasterxml.jackson.databind.JsonMappingException: JsonObject

Spring boot 2.5
#PostMapping("/cart/product")
public Response addProduct(#RequestBody Map<String, Object> payloadMap) {
logger.info("addProduct: payloadMap: " + payloadMap);
String userName = payloadMap.get("user_name").toString();
final Product product = new ObjectMapper().convertValue(payloadMap.get("product"), Product.class);
int quantity = (int) payloadMap.get("quantity");
Cart findCart = cartRepository.findByUsername(userName);
if (findCart == null) {
Cart cart = new Cart();
cart.setCreated(new Date());
cart.setUsername(userName);
cart.addProduct(product, quantity);
cartRepository.save(cart);
logger.info("addProduct: success_add_product_to_new_cart: " + cart);
return ResponseService.getSuccessResponse(GsonUtil.gson.toJsonTree(cart));
} else {
findCart.addProduct(product, quantity);
logger.info("addProduct: before_save_exist_cart: " + findCart);
cartRepository.save(findCart);
logger.info("addProduct: success_add_product_to_exist_cart: " + findCart);
return ResponseService.getSuccessResponse(GsonUtil.gson.toJsonTree(findCart));
}
}
public class ResponseService {
private static final int SUCCESS_CODE = 0;
private static final String SUCCESS_MESSAGE = "Success";
private static final int ERROR_CODE = -1;
private static Logger logger = LogManager.getLogger(ResponseService.class);
public static Response getSuccessResponse(JsonElement body) {
Response response = new Response(SUCCESS_CODE, SUCCESS_MESSAGE, body);
logger.info("getSuccessResponse: response = " + response);
return response;
}
import com.google.gson.JsonElement;
public class Response {
private int code;
private String message;
private JsonElement body;
public Response(int code, String message) {
this.code = code;
this.message = message;
}
public Response(int code, String message, JsonElement body) {
this.code = code;
this.message = message;
this.body = body;
}
But I get error when try to return response:
2020-04-12 12:02:18.825 INFO 9584 --- [nio-8092-exec-1] r.o.s.e.controllers.CartController : addProduct: success_add_product_to_new_cart: Cart{id=130, username='admin#admin.com', created=Sun Apr 12 12:02:18 EEST 2020, updated=null, productEntities=[
ProductEntity{id=131, created=Sun Apr 12 12:02:18 EEST 2020, updated=null, quantity=1, orders=null, product=
Product{id=132, name='product name', description='product description', created=Tue Mar 10 22:34:15 EET 2020, updated=null, price=11.15, currency='EUR', images=[http://www.gravatar.com/avatar/44444?s=200x200&d=identicon, http://www.gravatar.com/avatar/33333?s=200x200&d=identicon]}}], totalAmount=11.15, currency='EUR'}
2020-04-12 12:02:18.836 INFO 9584 --- [nio-8092-exec-1] r.o.s.e.service.ResponseService : getSuccessResponse: response = Response{code = 0, message = 'Success', body = '{"id":130,"username":"admin#admin.com","created":"Apr 12, 2020, 12:02:18 PM","productEntities":[{"id":131,"created":"Apr 12, 2020, 12:02:18 PM","quantity":1,"product":{"id":132,"name":"product name","description":"product description","created":"Mar 10, 2020, 10:34:15 PM","price":11.15,"currency":"EUR","images":["http://www.gravatar.com/avatar/44444?s=200x200&d=identicon","http://www.gravatar.com/avatar/33333?s=200x200&d=identicon"]}}],"totalAmount":11.15,"currency":"EUR"}'}
2020-04-12 12:02:18.861 WARN 9584 --- [nio-8092-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: JsonObject; nested exception is com.fasterxml.jackson.databind.JsonMappingException: JsonObject (through reference chain: ru.otus.software_architect.eshop_orders.api.Response["body"]->com.google.gson.JsonObject["asBoolean"])]
Spring Boot uses jackson by default to serialize json. In Response object you have field JsonElement body. It is object from gson package and jackson don't know how to serialize that.
Solution: add property (in the application.properties file) to use gson instead of jackson. Note that Spring Boot version is important.
Spring Boot >= 2.3.0.RELEASE:
spring.mvc.converters.preferred-json-mapper=gson
Spring Boot < 2.3.0.RELEASE:
spring.http.converters.preferred-json-mapper=gson
More informations:
Configuring Spring Boot to use Gson instead of Jackson
Spring Boot 2.3 Release Notes
Application.properties
spring.mvc.converters.preferred-json-mapper=gson
I've found a workaround by keeping Jackson but implementing my own Serializer for the classes that cause Jackson serialisation issue.
It's hackish solution but it's working now.
public class GSONObjectSerializer extends SimpleSerializers {
private static final long serialVersionUID = -8745250727467996655L;
private class EmptySerializer extends StdSerializer<Object> {
/**
*
*/
private static final long serialVersionUID = 5435498165882848947L;
protected EmptySerializer(Class t) {
super(t);
}
#Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {
// --> Will write an empty JSON string
gen.writeStartObject();
gen.writeEndObject();
}
}
#Override
public JsonSerializer<?> findSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) {
// --> Here is to setup which class will be serialised by the custom serializer.
if (type.isTypeOrSubTypeOf(JsonObject.class) || type.isTypeOrSubTypeOf(StripeResponse.class)) {
return new EmptySerializer(type.getRawClass());
}
return super.findSerializer(config, type, beanDesc);
}
}
And you can register your serializer like this
#Configuration
public class SerialisationConfig {
#Bean
public ObjectMapper createMapper() {
SimpleModule simpleModule = new SimpleModule();
simpleModule.setSerializers(new GSONObjectSerializer());
return Jackson2ObjectMapperBuilder.json().modules(Arrays.asList(simpleModule)).build();
}
}

Spring Cloud OpenFeign Failed to Create Dynamic Query Parameters

Spring cloud openFeign can't create dynamic query parameters. It throws below exception because SpringMvcContract tries to find the RequestParam value attribute which doesn't exist.
java.lang.IllegalStateException: RequestParam.value() was empty on parameter 0
#RequestMapping(method = RequestMethod.GET, value = "/orders")
Pageable<Order> searchOrder2(#RequestParam CustomObject customObject);
I tried using #QueryMap instead of #RequestParam but #QueryMap does not generate query parameters.
Btw #RequestParam Map<String, Object> params method parameter works fine to generate a dynamic query parameter.
But I want to use a custom object in which the feign client can generate dynamic query parameters from the object's attributes.
From Spring Cloud OpenFeign Docs:
Spring Cloud OpenFeign provides an equivalent #SpringQueryMap annotation, which is used to annotate a POJO or Map parameter as a query parameter map
So your code should be:
#RequestMapping(method = RequestMethod.GET, value = "/orders")
Pageable<Order> searchOrder2(#SpringQueryMap #ModelAttribute CustomObject customObject);
spring-cloud-starter-feign has a open issue for supporting pojo object as request parameter. Therefore I used a request interceptor that take object from feign method and create query part of url from its fields. Thanks to #charlesvhe
public class DynamicQueryRequestInterceptor implements RequestInterceptor {
private static final Logger LOGGER = LoggerFactory.getLogger(DynamicQueryRequestInterceptor.class);
private static final String EMPTY = "";
#Autowired
private ObjectMapper objectMapper;
#Override
public void apply(RequestTemplate template) {
if ("GET".equals(template.method()) && Objects.nonNull(template.body())) {
try {
JsonNode jsonNode = objectMapper.readTree(template.body());
template.body(null);
Map<String, Collection<String>> queries = new HashMap<>();
buildQuery(jsonNode, EMPTY, queries);
template.queries(queries);
} catch (IOException e) {
LOGGER.error("IOException occurred while try to create http query");
}
}
}
private void buildQuery(JsonNode jsonNode, String path, Map<String, Collection<String>> queries) {
if (!jsonNode.isContainerNode()) {
if (jsonNode.isNull()) {
return;
}
Collection<String> values = queries.computeIfAbsent(path, k -> new ArrayList<>());
values.add(jsonNode.asText());
return;
}
if (jsonNode.isArray()) {
Iterator<JsonNode> it = jsonNode.elements();
while (it.hasNext()) {
buildQuery(it.next(), path, queries);
}
} else {
Iterator<Map.Entry<String, JsonNode>> it = jsonNode.fields();
while (it.hasNext()) {
Map.Entry<String, JsonNode> entry = it.next();
if (StringUtils.hasText(path)) {
buildQuery(entry.getValue(), path + "." + entry.getKey(), queries);
} else {
buildQuery(entry.getValue(), entry.getKey(), queries);
}
}
}
}
}

concurrency problems in interceptor tracking MDC

I have the following interceptor that tracks request/response based on saving and restoring some vars stored in MDC context for every request.
public class LoggingInterceptor implements DeferredResultProcessingInterceptor {
private final HelloSeeYouLogger helloSeeYouLogger;
private static final String X_UOW = "X-UOW";
private static final String X_REQUEST_ID = "X-RequestId";
private Map<String, String> context;
public LoggingInterceptor(HelloSeeYouLogger helloSeeYouLogger) {
this.helloSeeYouLogger = helloSeeYouLogger;
}
#Override
public <T> void beforeConcurrentHandling(NativeWebRequest request, DeferredResult<T> deferredResult) {
addUowAndRequestIdToMDC(request.getHeader(X_UOW), request.getHeader(X_REQUEST_ID));
final String uri = getUri((HttpServletRequest) request.getNativeRequest());
helloSeeYouLogger.logHelloThere(uri);
context = MDC.getCopyOfContextMap();
}
#Override
public <T> void afterCompletion(NativeWebRequest request, DeferredResult<T> deferredResult) {
if (context != null) {
MDC.setContextMap(context);
}
final String uri = getUri((HttpServletRequest) request.getNativeRequest());
String body = getBody((HttpServletRequest) request.getNativeRequest());
if (!StringUtils.isEmpty(body)) {
body = replaceMoreThanOneSpacesWithOneSpace(hideCreditCardNumber(body));
}
helloSeeYouLogger.logSeeYou(uri, body);
clearUowAndRequestIdFromMDC();
}
public static void addUowAndRequestIdToMDC(final String uow, final String requestId) {
//This NewRelic stuff shouldn't be here as it is used for distributed tracing and not logging.
// However it helps to levarage requests tracking from a metric service such as new relic to a log aggregation service
//such as ELK.
NewRelic.addCustomParameter(UOW, uow);
NewRelic.addCustomParameter(REQUEST_ID, requestId);
MDC.put(UOW, uow);
MDC.put(REQUEST_ID, requestId);
}
public static void clearUowAndRequestIdFromMDC() {
if (MDC.get(UOW) != null) {
MDC.remove(UOW);
}
if (MDC.get(REQUEST_ID) != null) {
MDC.remove(REQUEST_ID);
}
}
I think i mn going to have concurrency problems as context is an instance variable and when running multiple concurrent thread saving and restoring MDC context will result in wrong results. Also using synchronize keyword will add performance problems.
I was wondering if there is a better approach to track MDC context when a spring controller returns a DeferredResult.
Thanks
You can use try using HandlerInterceptorAdapter instead
Check: https://www.logicbig.com/how-to/code-snippets/jcode-spring-mvc-deferredresultprocessinginterceptor.html

Resources