0

I find that with Spring Boot 2.0.3, the JSON error message passed to the exception handler and returned to frontend will be formatted as a String instead of a JSON and escaped with \ before every double quotation mark(i.e., {"foo":"bar"} will be "{\"foo\":\"bar\"}".

To be concrete, I have a method converting a java.util.Map into a JSON string. The method is:

import java.util.Map;
import java.util.Set;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;

public class Utilities {
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public static String jsonBuilder(Map<String, Object> resultMap) throws JsonProcessingException {
        ObjectMapper mapper = new ObjectMapper();
        ObjectNode node = mapper.createObjectNode();
        for (String key: resultMap.keySet()) {
            Object value = resultMap.get(key);
            if (value instanceof String) {
                node.put(key, (String)value);
            } else if (value instanceof Set) {
                ArrayNode arrayNode = mapper.createArrayNode();
                ((Set) value).forEach(e -> arrayNode.add(e.toString()));
                node.set(key, arrayNode); //put() is deprecated
            }
        }
        return mapper.writeValueAsString(node);
    }
}

And this test:

import static org.assertj.core.api.Assertions.assertThat;

import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import org.junit.Test;

import com.fasterxml.jackson.core.JsonProcessingException;

public class UtilitiesTests {
    @Test
    public void testJsonBuilder() throws JsonProcessingException {
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("key1", "value1");
        String result = Utilities.jsonBuilder(map);
        assertThat(result).isEqualTo("{\"key1\":\"value1\"}");

        Map<String, Object> map1 = new HashMap<String, Object>();
        Set<String> set = new LinkedHashSet<String>(); //we must retain order.
        set.add("arrVal1");
        set.add("arrVal2");
        map1.put("key2", "value2");
        map1.put("key3", set);

        String result1 = Utilities.jsonBuilder(map1);
        assertThat(result1).isEqualTo("{\"key2\":\"value2\",\"key3\":[\"arrVal1\",\"arrVal2\"]}");
    }
}

The test always passes, i.e., the method works well and can translate a map into a correct JSON.

Now, if I return directly from some @RequestMapping method to the RESTful endpoint(testing from Postman), the returned value is a JSON, without any quotation.

Something like:

    List<BinInfo> founds = repository.findAllByBin(bin);
    BodyBuilder builder = ResponseEntity.status(HttpStatus.OK);
    builder.contentType(MediaType.APPLICATION_JSON_UTF8);
    if (founds != null && !founds.isEmpty() && founds.size() == 1) {
        return builder.body(founds.get(0));
    } else {
        errors.put("error", "BIN not found");
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .body(Utilities.jsonBuilder(errors));
    }

Will return:

{
    "error": "BIN not found"
}

But, if I pass a wrongly formatted argument to the method, an exception org.springframework.web.bind.MethodArgumentNotValidException will happen and will be caught by this exception handler:

@ExceptionHandler(MethodArgumentNotValidException.class)
protected ResponseEntity<Object> handleMethodArgumentNotValid(
        MethodArgumentNotValidException ex, WebRequest request) throws JsonProcessingException {
    BindingResult result = ex.getBindingResult();
    final List<FieldError> fieldErrors = result.getFieldErrors();
    final Set<String> errors = new HashSet<>();
    for (FieldError fe: fieldErrors) {
        errors.add(fe.getField());
    }
    Map<String, Object> resultMap = new HashMap<String, Object>();
    resultMap.put("error", "validation");
    resultMap.put("fields", errors);
    log.error("Validation error. ", ex);
    return ResponseEntity.badRequest()
            .contentType(MediaType.APPLICATION_JSON_UTF8)
            .body(Utilities.jsonBuilder(resultMap));
}

And, the returned will not be a JSON but a quoted and escaped string, like:

"{\"error\":\"validation\"}"

I know the exception is caught because I see these in the log:

[NODE=xxxxxx] [ENV=dev] [SRC=UNDEFINED] [TRACE=] [SPAN=] [2018-08-21T12:43:50.722Z] [ERROR] [MSG=[XNIO-2 task-2] c.p.b.controller.BinInfoController - Validation error.  org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument at index 0 in method: public org.springframework.http.ResponseEntity<java.lang.Object> com.xxxxx.binlookup.controller.BinInfoController.insertBIN(com.xxxxxx.binlookup.model.BinInfo) throws com.fasterxml.jackson.core.JsonProcessingException, with 1 error(s): [Field error in object 'binInfo' on field 'bin': rejected value [11]; codes [Size.binInfo.bin,Size.bin,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [binInfo.bin,bin]; arguments []; default message [bin],8,6]; default message [?????6?8??]]
        at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:138)
        at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:124)
        at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:161)
        at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:131)
        at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:102)
        at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:877)
        ...

How can I prevent this?

Is there some interceptor between the handler and the endpoint?

WesternGun
  • 11,303
  • 6
  • 88
  • 157

0 Answers0