background

Recently, the company has to engage in front-end monitoring, to intercept requests, interface error or non-200-400 error data are reported, convenient investigation. Ajax and Axios are both easy to say. The key is that fetch has encountered many problems in hijacking.

preface

Compared to XMLHttpRequest, fetch is simple and straightforward to write, as long as the entire configuration item is passed in when the request is made. It also provides more control parameters than XHR, such as whether to carry cookies, whether to manually jump, and so on. In addition, the Fetch API is based on the Promise chain invocation, which partly avoids some callback hell. For example, here is a simple fetch request:

fetch('https://example.org/foo', {
    method: 'POST'.mode: 'cors'.headers: {
        'content-type': 'application/json'
    },
    credentials: 'include'.redirect: 'follow'.body: JSON.stringify({ foo: 'bar' })
}).then(res= > res.json()).then(...)
Copy the code

To see how XMLHttpRequest works, in a nutshell, you need to call the open() method to open a request, then call another method or set parameters to define the request, and finally call the send() method to initiate the request, The onload or onReadyStatechange event processes the data:

var xhr = new XMLHttpRequest();
// Set the timeout period for XHR requests
xhr.timeout = 3000;
// Format the data to be returned in the response
xhr.responseType = "text";
// Create a POST request, asynchronously
xhr.open('POST'.'/server'.true);
// Register the related event callback handler
xhr.onload = function(e) { 
  if(this.status == 200||this.status == 304){
      alert(this.responseText); }}; xhr.ontimeout =function(e) {... }; xhr.onerror =function(e) {... }; xhr.upload.onprogress =function(e) {... };// Send dataxhr.send(...) ;Copy the code

Weaknesses of the Fetch API

It looks like the Fetch API has a lot of advantages over traditional XHR, but before we get to “True smell”, let’s look at three features that are easy to implement on XHR:

  1. How do I interrupt a request?

The XMLHttpRequest object has an abort() method that breaks a request. XHR also has an onabort event that listens for the interruption of a request and responds to it. 2. How do I timeout a request?

The XMLHttpRequest object has a timeout attribute, which is assigned to interrupt the request automatically if it has not completed by a specified time. In addition, XHR has an onTimeout event that listens for timeout interrupts of requests and responds to them. 3. How to intercept the request and wrap the response/request content?

XMLHttpRequest has responseText that parses the response data, but for fetch we need to call res.json()/res.text() to get the data, but if we call the JSON method before the user, we will throw an error, turning on MDN, Take a closer look at the introduction of fetch() ‘s Response:

Response.json() reads the Response object and sets it as read (since the Responses objects are set to stream, they can only be read once) and returns a Promise object parsed into JSON format.

Uh… , this can only be read once is a bit of a bug. But is there really nothing to be done?

There is a good solution to the first problem, but the Fetch API is nearly three years late in browser implementation. With AbortController and AbortSignal fully implemented in all browsers, the Fetch API can interrupt a request just like XHR, but with a slight twist. By creating an AbortController instance, we get a controller that controls interrupts natively supported by the Fetch API. The signal argument to this instance is an AbortSignal instance, which also provides an abort() method to send the interrupt signal

For the second question, since you’ve already taken a slight detour to implement the interrupt request, why not take a longer detour? Just use AbortController with setTimeout() to achieve a similar effect.

For the third question, I found this sentence on Fetch API MDN, which gave me the idea to solve the question:

The ReadableStream interface in the stream operations API renders a readable binary stream operation. The Fetch API provides a concrete ReadableStream object via the Body property of Response.

ReadableStream??? !!!!!!!!! Oh roar! Is there any method related to Clone, tee and pipe that can be used since it is a stream?

What is the Streams

Streams break up the resource you want to receive over the network into smaller chunks and process it bit by bit. This is exactly what browsers do when they receive resources to display web pages — video buffers and more content can play gradually, and sometimes you can see images gradually display as content loads.

But at one time these were not available for JavaScript. Previously, if we wanted to process a resource (video, text file, etc.), we had to download the complete file, wait for it to be deserialized into the appropriate format, and then process it after receiving all the content in its entirety.

Everything changes with the use of streams in JavaScript — as long as the raw data is available on the client side, you can use JavaScript to process it bit by bit, without the need for buffers, strings, or blobs.

There are more advantages — you can detect when a stream starts or ends, link streams together, handle errors and cancel streams as needed, and react to the read speed of the stream.

The basic application of streams revolves around making responses processable by streams. For example, a successful fetch Request response Body is exposed as ReadableStream, which you can then read using the reader created by readableStream.getreader (), Cancel it with readableStream.cancel () and so on.

ReadableStream

Tee () Tee method (tee means to place golf balls on tees) tees are readable streams, returning an array containing two branches of ReadableStream instances, each element receiving the same transfer data.

So we can use this feature to split a stream into two streams, using one stream to output the data we intercept and the other stream to return directly to the user:

const getFetchRes = (res) = > {
    const [hijackStream, returnStream] = res.body.tee();
   
	const getBody = function(response: Response) {
	  return new Promise((reslve, reject) = > {
	    if (response.headers.get('content-type') = = ='application/json') {
	      response
	        .json()
	        .then(function(json) {
	          reslve(json)
	        })
	        .catch(error= > {
	          reject(error)
	        })
	    } else {
	      response
	        .text()
	        .then(function(text) {
	          reslve(text)
	        })
	        .catch(error= > {
	          reject(error)
	        })
	    }
	  })
	}

	getBody.then((res) = >{
		report({...})
	})
	
    return new Response(returnStream, { headers: res.headers })
};
fetch('/foo').then(logProgress).then(res= > res.json()).then((data) = >{... })Copy the code

Good, so we have solved the problem that the fetch returned data cannot be hijacked.

Locking mechanism for streams

Streams can only have one active reader at a time. When a stream is being used by a reader, the stream is locked by that reader, and the locked attribute is true. If the stream needs to be read by another reader, the currently active reader can call the reader.releaselock () method to release the lock. The closed attribute of a reader is a Promise that will be resolved when a reader is closed or the lock is released.

reader.closed.then(() = > {
  console.log('reader closed');
});
reader.releaseLock();
Copy the code

Let’s take a look at the specification documentation for the Fetch API, which in 5.2. Body mixin reads as follows:

Objects implementing the Body mixin also have an associated consume body algorithm, given a type, runs these steps:

1.If this object is disturbed or locked, return a new promise rejected with a TypeError.

2.Let stream be body’s stream if body is non-null, or an empty ReadableStream object otherwise. 3.Let reader be the result of getting a reader from stream. If that threw an exception, return a new promise rejected with that exception.

4.Let promise be the result of reading all bytes from stream with reader.

5.Return the result of transforming promise by a fulfillment handler that returns the result of the package data algorithm with its first argument, type and this object’s MIME type.

In simple terms, when we call the method on Body, the browser implicitly creates a Reader that reads the stream of returned data and creates a Promise instance that resolves and returns the formatted data after all data has been read. So, when we call the Body method, we create a reader that we can’t touch, and the stream is locked and can’t be canceled from outside.

References:

MDN web docs – Streams API

From Fetch to Streams — Processing network requests from the perspective of Streams — Netease Cloud Music big front-end team