Skip to content

HttpServiceProxyFactory should omit optional @RequestParam if converted from null to empty string #33794

@ftreede

Description

@ftreede

Spring version: 6.1.14

When using HttpServiceProxyFactory, we can define request parameters as optional, so that a value of null leads to the parameter being omitted completely:

@GetExchange("/test")
void testString(@RequestParam(value = "param", required = false) String param);

Here, calling testString(null) will result in a request with url /test. The query parameter is omitted completely.

However, If the value is not a string and run through conversion, this does not work.
In this example:

@GetExchange("/test")
void testDate(@RequestParam(value = "param", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) OffsetDateTime param);

Calling testDate(null) makes a request with /test?param=, setting the query parameter to an empty string.

This behaviour is inconsistent and counterintuitive. Also, a server expecting a date value in the parameter is likely to not accept an empty string.

You can work around this by customizing the conversion service or the argument resolvers in the HttpServiceProxyFactory, but there really should be a consistent solution in spring itself.

A full test case for reference:

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.HttpMethod;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.support.RestClientAdapter;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;

import java.time.OffsetDateTime;

import static org.springframework.test.web.client.match.MockRestRequestMatchers.*;
import static org.springframework.test.web.client.response.MockRestResponseCreators.*;

class HttpInterfaceConvertedNullTest {

    private MockRestServiceServer mockServer;
    private HttpInterface httpInterface;

    @BeforeEach
    void setUp() {
        // setup client and mock server
        RestClient.Builder rcb = RestClient.builder();
        mockServer = MockRestServiceServer.bindTo(rcb).build();
        rcb.baseUrl("http://example.com");

        // make http interface
        httpInterface = HttpServiceProxyFactory.builderFor(RestClientAdapter.create(rcb.build()))
                .build()
                .createClient(HttpInterface.class);
    }

    @Test
    void testDateNull() {
        // setup expectations - the requestTo expectation also requires there to be no query parameters
        mockServer.expect(requestTo("http://example.com/test"))
                .andExpect(method(HttpMethod.GET))
                .andRespond(withSuccess());

        // make call
        httpInterface.testDate(null);

        // verify
        mockServer.verify();
    }

    @Test
    void testStringNull() {
        // setup expectations - the requestTo expectation also requires there to be no query parameters
        mockServer.expect(requestTo("http://example.com/test"))
                .andExpect(method(HttpMethod.GET))
                .andRespond(withSuccess());

        // make call
        httpInterface.testString(null);

        // verify
        mockServer.verify();
    }


    interface HttpInterface {
        @GetExchange("/test")
        void testDate(@RequestParam(value = "param", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) OffsetDateTime param);

        @GetExchange("/test")
        void testString(@RequestParam(value = "param", required = false) String param);
    }

}

Metadata

Metadata

Assignees

Labels

in: webIssues in web modules (web, webmvc, webflux, websocket)type: enhancementA general enhancement

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions