[JAVA] Get Flux result of Spring Web Flux from JS with Fetch API

Recently, I started making various things using Spring WebFlux. With Spring WebFlux,

	@GetMapping
	public Flux<Hoge> all() {
		return Flux.create(sink -> {
			// ...
		})
	}

I write a description in the Controller like this, but how do I receive this in the browser? I did a little research and tried it, so I will write it.

First, the return value of the request returned by Flux

This article will be very helpful. BLOG.IK.AM --First Spring WebFlux (Part 1 --Try Spring WebFlux)

It seems that it will be returned in the format of Content-Type: text / event-stream (Server-Sent Event) or Content-Type: application / stream + json.

Each seems to return the body in the following format.

When you google Server-Sent Events, you can see [Event Source] in the explanation of Using Server-Sent Events | MDN. I quickly realized that I would use it, but what about ʻapplication / stream + json`?

Access from Fetch API with ʻapplication / stream + json`

Using the Fetch API, it seems that you can access it as follows.

  const url = "..."; //URL to request
  const callback = json => { /* ...Logic to process each row JSON*/ };

  const decoder = new TextDecoder();

  const abortController = new AbortController();
  const { signal } = abortController;

  //start fetch
  fetch(url, {
    signal,
    headers: {
      Accept: "application/stream+json"
    }
  }).then(response => {
    let buffer = "";
    /**
     *Process the substring read from the stream
     * @param {string}chunk Read string
     * @returns {void}
     */
    function consumeChunk(chunk) {
      buffer += chunk;

      //Split by line feed code and fetch each JSON line
      // https://en.wikipedia.org/wiki/JSON_streaming#Line-delimited_JSON
      // http://jsonlines.org/
      const re = /(.*?)(\r\n|\r|\n)/g;
      let result;
      let lastIndex = 0;
      while ((result = re.exec(buffer))) {
        const data = result[1];
        if (data.trim()) {
          callback(JSON.parse(data)); //Pass JSON to the callback to process.
        }
        ({ lastIndex } = re);
      }
      //If there is no newline code, the rest of the string will be combined with the next read for processing.
      buffer = buffer.slice(lastIndex);
    }
    //Create a stream reader
    const reader = response.body.getReader();
    //Reading process body
    function readNext() {
      return reader.read().then(({ done, value }) => {
        if (!done) {
          // --Read process--
          consumeChunk(decoder.decode(value));
          return readNext();
        }

        // --End processing--
        if (buffer.trim()) {
          //If there is a character string left at the end, it will be processed.
          //This is reached if the line feed code is not included after the last line data.
          //It doesn't seem to happen in Spring WebFlux, but http://jsonlines.org/Looking at, I got the impression that the last line feed code may not be available, so I will implement it just in case.
          // `consumeChunk`Does not recognize the line unless you pass a line feed code, so pass a line feed code.
          consumeChunk("\n");
        }

        return response;
      });
    }
    return readNext();
  });
}

I will explain what I am doing below.

AbortController

This has nothing to do with fetching Flux, but it is necessary if you want to cancel fetch in the middle.

  const abortController = new AbortController();
  const { signal } = abortController;

  //start fetch
  fetch(url, {
    signal,
    headers: {
      Accept: "application/stream+json"
    }
  })

If you pass signal to the option of fetch.

abortController.abort()

Then you can stop fetch in the middle. At this time, it seems even better to interrupt the process if it is canceled even on the Java Controller side.

	@GetMapping
	public Flux<Hoge> all() {
		return Flux.create(sink -> {
			// ...
			if (sink.isCancelled()) {
				//Since it has been canceled, let's interrupt the process.
			}
			// ...
		})
	}

You can also use ʻon Cancel`.

Receive response in Stream

Refer to ReadableStream.getReader () | MDN.

Using ReadableStream, it seems that the response can be received by Stream as follows.

  fetch(...).then(response => {
    // ...
    //Create a stream reader
    const reader = response.body.getReader();
    function readNext() {
      return reader.read().then(({ done, value }) => {
        if (!done) {
          /*Handle value*/
          // ...
          return readNext();
        }

        return; /*End*/
      });
    }
    return readNext();
  })

decode value

The value obtained from ReadableStream is [Uint8Array](https://developer.mozilla.org/en/ It seems to come by docs / Web / JavaScript / Reference / Global_Objects / Uint8Array), so decode it using TextDecoder.

const decoder = new TextDecoder();

// ...

const sValue = decoder.decode(value) //Decode Uint8Array and convert it to string

Handle chunk

Up to this point, we have reached the point where we can get the contents of Stream as a character string. Here we will actually parse JSON.

    let buffer = "";
    /**
     *Process the substring read from the stream
     * @param {string}chunk Read string
     * @returns {void}
     */
    function consumeChunk(chunk) {
      buffer += chunk;

      //Split by line feed code and fetch each JSON line
      // https://en.wikipedia.org/wiki/JSON_streaming#Line-delimited_JSON
      // http://jsonlines.org/
      const re = /(.*?)(\r\n|\r|\n)/g;
      let result;
      let lastIndex = 0;
      while ((result = re.exec(buffer))) {
        const data = result[1];
        if (data.trim()) {
          callback(JSON.parse(data)); //Pass JSON to the callback to process.
        }
        ({ lastIndex } = re);
      }
      //If there is no newline code, the rest of the string will be combined with the next read for processing.
      buffer = buffer.slice(lastIndex);
    }

I'm doing something, but I'll explain it later.

Split JSON with line feed code

ʻApplication / stream + json` returns the following response as described above.

{"key":"value" ... }
{"key":"value" ... }
{"key":"value" ... }

This response is JSON separated by a newline code. So, each line JSON is JSON.parse separated by a line feed code.

Combine chunks

The string obtained with read () from ReadableStream is not a complete JSON. It is just a part of the whole response.

Therefore, if the chunk does not contain a line feed code, it is combined with the next chunk for processing.


That's all for the explanation.

Impressions

The method using EventSource may not be able to pass header information, and may require some ingenuity or modification depending on the application to be embedded. (For example, when processing on the server side on the assumption that the authentication token is placed in the header.)

However, the method using this Fetch API is almost the same as normal HTTP access, so I had the impression that it could be used easily (if the browser supports Fetch API).

Recommended Posts

Get Flux result of Spring Web Flux from JS with Fetch API
Get Body part of HttpResponse with Filter of Spring
◆ Get API created by Spring Boot from React
Create a web api server with spring boot
[Spring Boot] Get user information with Rest API (beginner)
Test Web API with junit
Get started with Spring boot
Link API with Spring + Vue.js
Implement a simple Web REST API server with Spring Boot + MySQL
Sample of using Salesforce's Bulk API from Java client with PK-chunking
[Beginner] Let's write REST API of Todo application with Spring Boot
Spring with Kotorin --4 REST API design
Get validation results with Spring Boot
Filter the result of BindingResult [Spring]
[Java] Get Json from URL and handle it with standard API (javax.script)
[Java] Get MimeType from the contents of the file with Apathce Tika [Kotlin]