1

so currently I'm working on a project where we have product objects which in turn contain "Origin" objects (containing region: String and country: String).

What I'm trying to do is a RestController which takes in an optional Origin object and does something with it (e.g. logs it).

This is what I have right now:

@GetMapping("search")
    public Page<Wine> getProductByStuff(
            @RequestParam(required = false) Origin origin, 
            /* other attributes */) {
    log.info(origin); // it has a proper toString method.
}

There are two problem with this approach. First of all, when I send a request like:

http://[...]/search?origin={"region":"blah","country":"UK"}

or even the html converted string like:

http://[...]/search?origin={%22region%22:%22blah%22%44%22country%22:%22UK%22}

... it says

Invalid character found in the request target [/api/products/search?origin={%22region%22:%22blah%22%44%22country%22:%22DE%22}]. The valid characters are defined in RFC 7230 and RFC 3986.

Afaik the only valid characters Tomcat has that I need are {}. All others I've replaced with the html encoded chars and it still doesn't work.

What I did to prevent this:

@Component
public class TomcatWebServerCustomizer
        implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {

    @Override
    public void customize(TomcatServletWebServerFactory factory) {
        TomcatConnectorCustomizer a = null;
        factory.addConnectorCustomizers(connector -> {
            connector.setAttribute("relaxedPathChars", "<>[\\]^`{|},\"");
            connector.setAttribute("relaxedQueryChars", "<>[\\]^`{|},\"");
        });
    }
}

(See this, which is, by the way, deprecated (at least connector.setAttribute).)

This produced:

MethodArgumentConversionNotSupportedException: Failed to convert value of type 'java.lang.String' to required type '[censored].backend.model.Origin'.

My questions are:

  • (How) is it possible to configure Tomcat/Spring so that they can actually accept json in the url params?
  • How would I format it in e.g. Postman so that it would work? Currently I'm just converting special characters by hand in the params tab of Postman.
Panossa
  • 119
  • 11
  • 1
    Why are you trying to send JSON object with GET as URL parameters? To send in the URL it should be key/value pairs. Do you have any special purpose? You can just send it with a POST request in the body. – Taha Yavuz Bodur Jul 25 '20 at 19:35
  • Well, I'm using a GET request since it fits the situation the most (it's a search query after all) and I'm not simply writing the attributes of origin into the path cause it makes it harder in the front end since I can't just put product.origin in there but have to deconstruct it. If it's not possible, Imma do that though. – Panossa Jul 25 '20 at 19:38
  • 2
    @TahaYavuzBodur I highly disagree wrt. the POST part of your comment. A POST should change the server state, i.e. if data is only fetched, it should be a GET. But I agree wrt. the JSON-part. Request-parameters are meant as key-value pairs, not complex objects. – Turing85 Jul 25 '20 at 19:40
  • Discussion on using GET vs POST when using body: https://stackoverflow.com/questions/978061/http-get-with-request-body. There is not really any consensus, but the latest HTTP spec doesn't mention anything to the contrary, so I would recommend doing that. – gagarwa Jul 25 '20 at 20:03

2 Answers2

1

As mentioned, don't use JSON as a path parameter.

Directly use path parameters, and convert to Origin object.

@GetMapping("search")
public Page<Wine> getProductByStuff(
        @RequestParam(required = false) String region,
        @RequestParam(required = false) String country, /* other attributes */) {
    Origin origin = new Origin(region, country);
    log.info(origin); // it has a proper toString method.
}
gagarwa
  • 979
  • 11
  • 21
1
  • Here is what you need to do if you want to send it as json query param.
     @RestController
     public class OriginController {

      @GetMapping("/search")
      public void getOrigin(@RequestParam(value = "origin", required = false) 
                            Origin origin) {
        System.out.println(origin);
      }

     }
  • Register a converter
    @Component
   public class StringToOriginConverter implements 
                              Converter<String, Origin> {

      ObjectMapper objectMapper = new ObjectMapper();

      @Override
      public Origin convert(String source) {
         try {
            return objectMapper.readValue(source, Origin.class);
         } catch (JsonProcessingException e) {
            //You could throw some exception here instead for custom error
            return null;
         }

       }
    }
  • Sending from postman enter image description here

Note

My answer is not debating whether you should use POST or GET as it is not what you have asked. It is just providing one option if you want to send some payload as query param

  • OMG that actually works (after I figured out "Converter" is from Spring, not from Jackson)! Thank you so, so much. I had no idea something like this exist! Do you mind sharing where you know this from? – Panossa Jul 25 '20 at 21:17
  • 1
    I have using for long and recently as couple of days ago on this. https://stackoverflow.com/questions/62970847/spring-boot-converter-ignored-for-nested-property?answertab=active#tab-top – Kavithakaran Kanapathippillai Jul 25 '20 at 21:22
  • 1
    https://docs.spring.io/spring/docs/5.2.8.RELEASE/spring-framework-reference/core.html#core-convert – Kavithakaran Kanapathippillai Jul 25 '20 at 21:25
  • Hey, just a little question in a smiliar question. I have another method accepting Collection idList but if I send idList=1,2 it works and if I send idList=[1,2] (encoded via Postman like you showed me) it interprets that as [[1,2]] and fails number conversion. How do I fix that? – Panossa Jul 29 '20 at 22:14