public static CompletionStage<List<MultipartFile>> read(HttpServerRequest r) {
        var contentType = r.headers().getFirst("content-type");
        if (contentType == null) {
            throw HttpServerResponseException.of(400, "content-type header is required");
        }
        var m = boundaryPattern.matcher(contentType);
        if (!m.matches()) {
            throw HttpServerResponseException.of(400, "content-type header is invalid");
        }
        var boundary = m.group("boundary");
        var future = new CompletableFuture<List<MultipartFile>>();
        var decoder = new MultipartDecoder(boundary, future);
        r.body().subscribe(decoder);
        return future;
    }
@Test
    void filenetMultipart() {
        var body = """
            --A-B--MIME-BOUNDARY--b96857b1a70d2dc4-17e9b561ad7--Y-Z
            Content-Disposition: form-data; name="field1"
            Content-Type: text/plain
                        
            value1
            --A-B--MIME-BOUNDARY--b96857b1a70d2dc4-17e9b561ad7--Y-Z
            Content-Disposition: form-data; name="field2"; filename="example.txt"
            Content-Type: text/plain
                        
            """;
        var length = 12;
        var f = Flux.fromStream(body.lines())
            .map(l -> StandardCharsets.UTF_8.encode(l + "\r\n"))
            .concatWith(Flux.fromStream(IntStream.range(0, length).mapToObj(i -> {
                var b = new byte[1024 * 1024];
                ThreadLocalRandom.current().nextBytes(b);
                return ByteBuffer.wrap(b);
            })))
            .concatWithValues(StandardCharsets.UTF_8.encode("\r\n--A-B--MIME-BOUNDARY--b96857b1a70d2dc4-17e9b561ad7--Y-Z--\r\n\r\n"));

        var request = new SimpleHttpServerRequest("POST", "/", f, new Map.Entry[]{
            Map.entry("content-type", " multipart/related; boundary=A-B--MIME-BOUNDARY--b96857b1a70d2dc4-17e9b561ad7--Y-Z; type=\"application/xop+xml\"; start-info=\"application/soap+xml\"")
        }, Map.of());
        var result = MultipartReader.read(request)
            .toCompletableFuture().join();

        assertThat(result)
            .satisfies(part -> {
                assertThat(part.name()).isEqualTo("field1");
                assertThat(part.fileName()).isNull();
                assertThat(part.contentType()).isEqualTo("text/plain");
                assertThat(part.content()).asString(StandardCharsets.UTF_8).isEqualTo("value1");
            }, Index.atIndex(0))
            .satisfies(part -> {
                assertThat(part.name()).isEqualTo("field2");
                assertThat(part.fileName()).isEqualTo("example.txt");
                assertThat(part.contentType()).isEqualTo("text/plain");
                assertThat(part.content()).withRepresentation(new Representation() {
                    @Override
                    public String toStringOf(Object object) {
                        return "<array>";
                    }

                    @Override
                    public String unambiguousToStringOf(Object object) {
                        return "<array>";
                    }
                }).hasSize(length * 1024 * 1024);
            }, Index.atIndex(1));
    }