拦截JavaScript中的fetch() API请求和响应

60

我想在JavaScript中拦截fetch API请求和响应。

例如,在发送请求之前,我想要拦截请求URL。 我也希望在响应到达时拦截响应。

以下代码用于拦截所有XMLHTTPRequest的响应。

(function(open) {
  XMLHttpRequest.prototype.open = function(XMLHttpRequest) {
    var self = this;
    this.addEventListener("readystatechange", function() {
      if (this.responseText.length > 0 && 
          this.readyState == 4 && 
          this.responseURL.indexOf('www.google.com') >= 0) {

        Object.defineProperty(self, 'response', {
          get: function() { return bValue; },
          set: function(newValue) { bValue = newValue; },
          enumerable: true,
          configurable: true
        });
        self.response = 'updated value' // Intercepted Value 
      }
    }, false);
    open.apply(this, arguments);
  };
})(XMLHttpRequest.prototype.open);

我想要为fetch() API实现相同的功能。我该怎么做?


听起来你想要入侵Window.Request接口 https://fetch.spec.whatwg.org/#request https://developer.mozilla.org/en-US/docs/Web/API/Request 以执行类似于问题中代码示例所做的操作。我个人无法提供更具体的指导,只能说那可能是你想要开始尝试实验的地方。 - sideshowbarker
有没有办法检测所有fetch API调用的成功回调?例如: $(document).ajaxSuccess(function( event, xhr, settings ) { }); - Hariharan Subramanian
你检查响应状态的唯一方法是检查响应对象的 ok 属性 https://developer.mozilla.org/zh-CN/docs/Web/API/Response/ok: fetch(someURL).then(function(response) { if(response.ok) { /* do something */} - sideshowbarker
1
谢谢@sidehowbarker。我想为站点中的所有fetch请求添加成功回调。我将在应用程序顶部运行我的代码。我不知道有多少fetch请求在应用程序中注册,基于回调中的请求URL,我需要执行一些功能。 - Hariharan Subramanian
5个回答

72

现有的答案展示了在浏览器中模拟fetch的一般结构,但忽略了重要的细节。

接受的答案展示了用自定义实现替换window.fetch函数的一般模式,该模式拦截调用并将参数转发给fetch。然而,所展示的模式不允许拦截函数对响应进行任何处理(例如,读取状态或正文或注入模拟),因此仅适用于记录请求参数。这是一个相当狭窄的用例。

这个答案使用了一个async函数,使拦截器可以await fetch的promise,并可能与响应进行操作(模拟、读取等),但(在撰写本文时)它有一个多余的闭包,并且没有展示如何以非破坏性的方式读取响应正文。它还包含一个变量别名错误导致堆栈溢出。

这个答案到目前为止是最完整的,但在回调中有一些无关的噪音,并且没有提及克隆响应以使拦截器能够收集响应体。它没有说明如何返回一个模拟响应。

下面是一个最简、最完整的示例,纠正了这些问题,展示了如何处理参数日志记录,读取响应体而不损害原始调用者通过克隆响应,并(可选地)提供模拟响应。

const {fetch: origFetch} = window;
window.fetch = async (...args) => {
  console.log("fetch called with args:", args);
  const response = await origFetch(...args);

  /* work with the cloned response in a separate promise
     chain -- could use the same chain with `await`. */
  response
    .clone()
    .json()
    .then(data => console.log("intercepted response data:", data))
    .catch(err => console.error(err));

  /* the original response can be resolved unmodified: */
  //return response;

  /* or mock the response: */
  return new Response(JSON.stringify({
    userId: 1,
    id: 1,
    title: "Mocked!!",
    completed: false
  }));
};

// test it out with a typical fetch call
fetch("https://jsonplaceholder.typicode.com/todos/1")
  .then(response => response.json())
  .then(data => console.log("original caller received:", data))
  .catch(err => console.error(err));


9
这是保持稳定的最佳答案。 - stupidbodo
为什么在返回响应对象中的JSON时需要使用异步? - Sarthak Gupta
1
我们不进行修改。这是为了与原始的fetch API保持一致,其中 response.json() 返回一个promise。 - ggorlen

47

我们可以采用以下方法拦截fetch请求和参数。这种方法解决了我的问题。

 const constantMock = window.fetch;
 window.fetch = function() {
     // Get the parameter in arguments
     // Intercept the parameter here 
    return constantMock.apply(this, arguments)
 }

如何使用这段代码?在这段代码之后,我们需要调用constantMock吗? - Sunny
只需按照常规方式调用fetch API。我们已经定义了自己的fetch,因此该调用将在我们的函数定义中被拦截。 - Hariharan Subramanian
这是获取所有浏览器API吗?我的应用程序已加载SSO(OKTA),想要更改一个API响应,这是否可能? - nisar
是的,它将拦截所有的fetch API。但你可以在函数内部进行条件检查,根据你的需要进行操作。只需在控制台记录参数和此对象,即可查看所有值。 - Hariharan Subramanian

25
要拦截响应体,你需要创建一个新的 Promise,并在 "then" 代码中解析或拒绝当前 Promise。这对我来说解决了问题,并保留了真实应用程序的内容,例如 React 等。
const constantMock = window.fetch;
window.fetch = function() {
  console.log(arguments);

  return new Promise((resolve, reject) => {
    constantMock
      .apply(this, arguments)
      .then((response) => {
        if (response.url.indexOf("/me") > -1 &&
            response.type != "cors") {
          console.log(response);
          // do something for specificconditions
        }
        resolve(response);
      })
      .catch((error) => {
        reject(error);
      })
  });
}

22
希望添加一个细节:如果需要对响应体进行“特定条件下的处理”,请不要忘记克隆响应,否则 Promise 的最终用户将收到“TypeError: Body has already been consumed” 错误。因此,请使用 “response.clone().json()” 或者 “response.clone().text()” 来获取响应体。 - Michael

18
const fetch = window.fetch;
window.fetch = (...args) => (async(args) => {
    var result = await fetch(...args);
    console.log(result); // intercept response here
    return result;
})(args);

6
这在 Chrome 83.0 中会导致堆栈溢出。使用 const origFetch = window.fetchawait origFetch(...args) 可以解决问题。另外,我不确定为什么需要最外层的函数。你可以直接使用 fetch = async (...args) => ... 并跳过 IIFE。 - ggorlen

1

在Hariharan的回答之后,我在每次获取请求之前和之后更新Redux中的spinner状态的方法如下:

import store from './../store';

// Set up interceptor on all fetch API calls
// Increments redux spinner state when api is called
// Decrements redux spinner state again when it is returned
(function() {
    const originalFetch = window.fetch;
    window.fetch = function() {
        store.dispatch({type: 'show-spinner'})
        return originalFetch.apply(this, arguments)
            .then((res) => {
                store.dispatch({type: 'hide-spinner'})
                return res;
            })
    }
})();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接