Spring-Boot WebFlux getFormData fails to parse real x-www-form-urlencoded data?

Lost Carrier :

I did a little test project with latest-greatest Spring-Boot V2.2.5 and WebFlux starter.

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <!-- ... -->

    <properties>
        <java.version>11</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>

    <!-- ... -->

In that project I created a really simple @RestController parsing x-www-form-urlencoded like this:

@RestController
public class MyController {

    /*
     This worked in Tomcat stack, but it's unfortunately too old-school for WebFlux...

     java.lang.IllegalStateException: In a WebFlux application, form data is accessed via ServerWebExchange.getFormData().
         at org.springframework.web.reactive.result.method.annotation.AbstractMessageReaderArgumentResolver.readBody(AbstractMessageReaderArgumentResolver.java:158) ~[spring-webflux-5.2.4.RELEASE.jar:5.2.4.RELEASE]
         Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
     Error has been observed at the following site(s):
         |_ checkpoint ⇢ HTTP POST "/" [ExceptionHandlingWebHandler]
     Stack trace:
         ...

    @PostMapping
    public Map<String, String> test(@RequestBody MultiValueMap<String, String> formData) {
        String formDataTest = formData.getFirst("test");
        String result = Objects.requireNonNullElse(formDataTest, "you failed!");
        return Map.of("result", result);
    }
    */

    @PostMapping
    public Map<String, String> test(ServerWebExchange serverWebExchange) {
        MultiValueMap<String, String> formData = getFormData(serverWebExchange);
        String formDataTest = formData.getFirst("test");
        String result = Objects.requireNonNullElse(formDataTest, "you failed!");
        return Map.of("result", result);
    }

    private static MultiValueMap<String, String> getFormData(ServerWebExchange serverWebExchange) {
        MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
        serverWebExchange.getFormData().subscribe(formData::addAll);
        return formData;
    }
}

To test this, I wrote the following...

@WebFluxTest(controllers = MyController.class)
class MyControllerTest {

    @Autowired
    private WebTestClient webClient;

    @Test
    void test() {
        webClient.post()
                .uri("/")
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                .body(BodyInserters.fromFormData("test", "testing-test"))
                .exchange()
                .expectStatus().is2xxSuccessful()
                .expectBody()
                .jsonPath("$.result").isEqualTo("testing-test");
    }
}

...and that tests goes green. Great stuff!

Unfortunately this appears to fail for real-world data. Tested in Postman and curl - this query should work fine to my understanding, but...

➜  ~ curl --location --request POST 'localhost:8080' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'test=testing-test'
{"result":"you failed!"}%

Did I do something wrong here? Why is there no from-data parsed when queried with curl?

Thomas Andolf :

in webflux you have something called a subscriber (consumer) and a publisher.

One publishes, one consumes. Your application publishes data to a consumer. So you have a client (curl) that wants to consume data. To consume it, curl subscribes to a webflux stream and webflux delivers either one or many objects. If its one object it delivers a Mono<T> if it's many it delivers a Flux<T>.

Why am i explaining this?

Well here:

serverWebExchange.getFormData().subscribe(formData::addAll)

you are subscribing internally in your application. As soon as you subscribe, you leave the reactive world, and you loose every benefit with having a webflux application and you consume the data.

So the bottom line is, you should (almost) never subscribe in your own application. If your application is the one initiating the call and consuming then sure it can subscribe. But here it is curl that subscribes and wants to consume, so you shouldn't.

You should instead return a Mono<Map<String, String>> from your @PostMapping annotated function.

public Mono<Map<String, String>> test(ServerWebExchange serverWebExchange)

and then rewrite to return a Mono:

private static Mono<MultiValueMap<String, String> >getFormData(ServerWebExchange serverWebExchange) {
    return serverWebExchange.getFormData()
        .flatMap(formData -> {
            MultiValueMap<String, String> formDataResponse = new LinkedMultiValueMap<>();
            return Mono.just(formDataResponse.addAll(formData));
        });
}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=18684&siteId=1