Micronaut-core: Query param String lists passed as comma separated binds to list of size 1 instead of n

Created on 1 May 2019  路  3Comments  路  Source: micronaut-projects/micronaut-core

Passing comma separated lists of strings as query params results in a list of length one (with the commas in the string) when binding and the high-level HttpClient seems to do this by default with List<String> parameters.

Steps to Reproduce

Make a controller that should echo a list of strings back:

@CompileStatic
@Controller("/api")
class EchoController {

    @Get("/echo-optional{?list}")
    HttpResponse echo(Optional<List<String>> list) {
        return HttpResponse.ok(list.get())
    }

     @Get("/echo-nullable{?list}")
    HttpResponse echo(@Nullable List<String> list) {
        return HttpResponse.ok(list)
    }
}

Make a client for testing that controller:

@CompileStatic
@Client("/api")
interface EchoClient {

        @Get("/echo-optional{?list}")
        HttpResponse echo(Optional<List<String>> list)

        @Get("/echo-nullable{?list}")
        HttpResponse echo(@Nullable List<String> list)
}

Make tests that test the controllers using a low level client and high-level client with comma separated string list query params:

@MicronautTest
class EchoControllerSpec extends Specification {

    @Inject
    @Client('/')
    RxHttpClient client

    @Inject
    EchoClient echoClient

    void "should pass lists correctly comma separated but doesn't with optional"() {
        given:
        List result = client.toBlocking().retrieve(HttpRequest.GET('/api/echo-optional?list=string1,string2'), List)

        expect:
        result.size() == 2
    }

    void "should pass lists correctly comma separated but doesn't with nullable"() {
        given:
        List result = client.toBlocking().retrieve(HttpRequest.GET('/api/echo-nullable?list=string1,string2'), List)

        expect:
        result.size() == 2
    }

    void "should pass lists correctly comma separated but doesn't with client optional"() {
        given:
        List result = echoClient.echo(Optional.of(["string1","string2"])).body() as List

        expect:
        result.size() == 2
    }

    void "should pass lists correctly comma separated but doesn't with client nullable"() {
        given:
        List result = echoClient.echo(["string1","string2"]).body() as List

        expect:
        result.size() == 2
    }
}

Expected Behaviour

Lists passed as comma separated should parse into lists of length 2 and be echoed back that way (and show up in debug that way)

Actual Behaviour

Lists are size 1 and contain the commas both in debug and on return from the response both for low-level and high-level http clients

BTW query param list handling DOES work for List<Long> which are also passed comma separated by the high-level client I believe

09:37:17.618 [nioEventLoopGroup-1-12] DEBUG i.m.http.client.DefaultHttpClient - Sending HTTP Request: GET /api/echo-optional?list=string1,string2
09:37:17.618 [nioEventLoopGroup-1-12] DEBUG i.m.http.client.DefaultHttpClient - Chosen Server: localhost(46774)
09:37:17.618 [nioEventLoopGroup-1-12] TRACE i.m.http.client.DefaultHttpClient - Accept: application/json
09:37:17.618 [nioEventLoopGroup-1-12] TRACE i.m.http.client.DefaultHttpClient - host: localhost:46774
09:37:17.618 [nioEventLoopGroup-1-12] TRACE i.m.http.client.DefaultHttpClient - connection: close
09:37:17.619 [nioEventLoopGroup-1-13] DEBUG i.m.h.server.netty.NettyHttpServer - Server localhost:46774 Received Request: GET /api/echo-optional?list=string1,string2
09:37:17.619 [nioEventLoopGroup-1-13] DEBUG i.m.h.s.netty.RoutingInBoundHandler - Matching route GET - /api/echo-optional
09:37:17.619 [nioEventLoopGroup-1-13] DEBUG i.m.h.s.netty.RoutingInBoundHandler - Matched route GET - /api/echo-optional to controller class com.ch.smartsearch.controllers.EchoController
09:37:17.620 [pool-1-thread-3] DEBUG i.m.h.s.netty.RoutingInBoundHandler - Encoding emitted response object [[string1,string2]] using codec: io.micronaut.jackson.codec.JsonMediaTypeCodec@3ac8cf9b
09:37:17.622 [nioEventLoopGroup-1-12] TRACE i.m.http.client.DefaultHttpClient - HTTP Client Response Received for Request: GET http://localhost:46774/api/echo-optional?list=string1,string2
09:37:17.622 [nioEventLoopGroup-1-12] TRACE i.m.http.client.DefaultHttpClient - Status Code: 200 OK
09:37:17.622 [nioEventLoopGroup-1-12] TRACE i.m.http.client.DefaultHttpClient - Date: Wed, 1 May 2019 13:37:17 GMT
09:37:17.622 [nioEventLoopGroup-1-12] TRACE i.m.http.client.DefaultHttpClient - content-type: application/json
09:37:17.622 [nioEventLoopGroup-1-12] TRACE i.m.http.client.DefaultHttpClient - content-length: 19
09:37:17.622 [nioEventLoopGroup-1-12] TRACE i.m.http.client.DefaultHttpClient - connection: close
09:37:17.622 [nioEventLoopGroup-1-12] TRACE i.m.http.client.DefaultHttpClient - Response Body
09:37:17.622 [nioEventLoopGroup-1-12] TRACE i.m.http.client.DefaultHttpClient - ----
09:37:17.622 [nioEventLoopGroup-1-12] TRACE i.m.http.client.DefaultHttpClient - ["string1,string2"]
09:37:17.622 [nioEventLoopGroup-1-12] TRACE i.m.http.client.DefaultHttpClient - ----

Condition not satisfied:

result.size() == 2
|      |      |
|      1      false
[string1,string2]

Environment Information

  • MacOs Mojave 10.14.4
  • Micronaut Version:1.1.0
  • JDK Version: 1.8.0_181
notabug

Most helpful comment

@jameskleeh
There is still an issue present: even if * is added both on client and controller side, it won't work, since the client ignores exploded operator and merges incoming list into coma separated string, which is then not exploded on the server side, as you described in #2157

Effectively this means, that there is no possibility to pass list of values as GET query value using micronaut on both sides. It is especially critical when common interface for both controller and client is used.

All 3 comments

This is working as designed as far as I can tell.

Server side:

@Get("/echo-optional{?list}")
    HttpResponse echo(Optional<List<String>> list) {

Should never bind to the list with more than 1 item if there is only a single query param. We can't assume comma separated means to split it. What if it was | separated, or tab separated?

Client side:

@Get("/echo-optional{?list}")
        HttpResponse echo(Optional<List<String>> list)

You have informed micronaut here to create a single query parameter from your list, which by default results in a comma separated string for a list argument. If you want the server to read it as a list, you need to change the URI template to @Get("/echo-optional{?list*}"). The * means to explode the list into multiple parameters.

If you included a sample application I would have submitted a PR to fix it

Closing this for now. If it turns out I was mistaken and there is an issue here I will reopen.

@jameskleeh
There is still an issue present: even if * is added both on client and controller side, it won't work, since the client ignores exploded operator and merges incoming list into coma separated string, which is then not exploded on the server side, as you described in #2157

Effectively this means, that there is no possibility to pass list of values as GET query value using micronaut on both sides. It is especially critical when common interface for both controller and client is used.

Was this page helpful?
0 / 5 - 0 ratings