HarmonyOS 鸿蒙Next中resourceHandler.didReceiveResponseBody拦截网络请求失败

HarmonyOS 鸿蒙Next中resourceHandler.didReceiveResponseBody拦截网络请求失败 基于schemeHandler.onRequestStart的resourceHandler来拦截Web内核的请求,并为被拦截的请求提供自定义的响应信息,按照官方文档(https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/web-scheme-handler)操作一直拦截不成功,还请各位大牛指教,多谢了哈。

Web({ src: '', controller: this.controller}) 
.javaScriptAccess(true) 
.domStorageAccess(true) 
.onControllerAttached(() => {
     this.controller.loadUrl(this.url)
     this.handleControllerAttached() 
}) }
handleControllerAttached() {
  try {
    this.schemeHandler.onRequestStart((request: webview.WebSchemeHandlerRequest,resourceHandler: webview.WebResourceHandler) => {

      const type = request.getRequestResourceType();
      const stream = request.getHttpBodyStream()

      if (!stream || type !== webview.WebResourceType.XHR) {
        return false
      }

      if (request.getRequestUrl().includes('xx.yy.api')) {
        stream.initialize().then(async () => {
          if (!stream || stream.getSize() <= 0) {
            return;
          }
          const buffer = await stream.read(stream.getSize()).catch(() => null)
          const obj = arrayBufferToObject(buffer)

          if (obj?.functionId == 'xxx_api') {
            try {

              // mock网络请求数据
              const result: ESObject = this.getRawFileJsonData()
              // 将Object格式的数据转化为ArrayBuffer格式的数据
              let arrayBuffer = objectToArrayBuffer(result) as ArrayBuffer;
              if (arrayBuffer && resourceHandler) {

                // 调用didReceiveResponseBody将构造的响应体传递给被拦截的请求。
                resourceHandler.didReceiveResponseBody(arrayBuffer);

                // 调用didFinish通知Web组件被拦截的请求已经完成。
                resourceHandler.didFinish();
              } else {
                resourceHandler.didFail(WebNetErrorList.ERR_FAILED);
              }
            } catch (error) {
             
            }
          }
        }).catch((error: BusinessError) => {
          
        })
        return true
      }
      return false
    })

    this.schemeHandler.onRequestStop((request: webview.WebSchemeHandlerRequest) => {
      console.info('getHttpBodyStream: onRequestStop');
    });

    this.controller.setWebSchemeHandler('https', this.schemeHandler);

  } catch (error) {
  
  }
}

更多关于HarmonyOS 鸿蒙Next中resourceHandler.didReceiveResponseBody拦截网络请求失败的实战教程也可以访问 https://www.itying.com/category-93-b0.html

8 回复

【背景知识】 Web组件拦截H5页面的网络请求有以下三种方式:

  • onInterceptRequest:可以拦截url并返回响应,但是获取的对象里面不包含请求体内容。
  • 网络拦截接口(arkweb_scheme_handler.h):可以对Web组件发出的请求进行拦截,并为被拦截的请求提供自定义的响应头以及响应体,通过OH_ArkWebResourceRequest_*接口获取被拦截请求的信息。可以获取url、method、referrer、headers、resourceType等信息。
  • WebSchemeHandler:可以拦截指定scheme的请求,可通过WebSchemeHandlerRequest.getHttpBodyStream()方法获取POST、PUT请求的数据体,支持BYTES、FILE、BLOB、CHUNKED类型的数据。

【解决方案】

import { WebNetErrorList, webview } from '@kit.ArkWeb';
import { BusinessError } from '@kit.BasicServicesKit';
import { buffer } from '@kit.ArkTS';
import { util } from '@kit.ArkTS';


@Entry
@Component
struct WebComponent {
  controller: webview.WebviewController = new webview.WebviewController();
  schemeHandler: webview.WebSchemeHandler = new webview.WebSchemeHandler();
  htmlData: string = "<html><body bgcolor=\"white\">Source:<pre>source</pre></body></html>";

  aboutToAppear() {
    // 配置Web开启调试模式
    webview.WebviewController.setWebDebuggingAccess(true);
  }

  build() {
    Column() {
      Web({
        src: 'https://developer.huawei.com/consumer/cn/doc/harmonyos-releases-V5/releasenotes-V5',
        controller: this.controller
      })
        .onControllerAttached(() => {
          try {
            this.schemeHandler.onRequestStart((request: webview.WebSchemeHandlerRequest,
              resourceHandler: webview.WebResourceHandler) => {
              console.info("[schemeHandler] onRequestStart");
              try {
                // 可以指定请求不做拦截
                if (request.getRequestUrl().endsWith("example.com")) {
                  return false;
                }

                // 获取请求头
                let header = request.getHeader();
                for (let i = 0; i < header.length; i++) {
                  console.info("[schemeHandler] onRequestStart header:" + header[i].headerKey + " " +
                  header[i].headerValue);
                }
                // 获取请求体
                let stream = request.getHttpBodyStream();
                if (stream) {
                  stream.initialize().then(() => {
                    if (!stream) {
                      console.error('[schemeHandler] HttpBodyStream initialize failed.')
                      return;
                    }
                    let size = stream.getSize();
                    console.info(`[schemeHandler] HttpBodyStream size is ${size}`);
                    stream.read(size).then((result: ArrayBuffer) => {
                      console.info(`[schemeHandler] HttpBodyStream buffer length is ${result.byteLength}`);
                      // 从buffer中转换请求体内容
                      let decoder = util.TextDecoder.create('utf-8')
                      let requestBodyStr = decoder.decodeToString(new Uint8Array(result));
                      console.info(`[schemeHandler] HttpBodyStream requestBody is ${requestBodyStr}`);
                    }).catch((error: BusinessError) => {
                      console.error(`[schemeHandler] HttpBodyStream read failed, message is ${error.message}`);
                    });
                  })
                } else {
                  console.info("[schemeHandler] onRequestStart has no http body stream");
                }
              } catch (error) {
                console.error(`[schemeHandler] ErrorCode: ${(error as BusinessError).code},  Message: ${(error as BusinessError).message}`);
              }

              // 构造响应体返回
              let response = new webview.WebSchemeHandlerResponse();
              try {
                response.setNetErrorCode(WebNetErrorList.NET_OK);
                response.setStatus(200);
                response.setStatusText("OK");
                response.setMimeType("text/html");
                response.setEncoding("utf-8");
                response.setHeaderByName("header1", "value1", false);
              } catch (error) {
                console.error(`[schemeHandler] ErrorCode: ${(error as BusinessError).code},  Message: ${(error as BusinessError).message}`);
              }

              // 调用didFinish/didFail前需要优先调用didReceiveResponse将构造的响应头传递给被拦截的请求。
              let buf = buffer.from(this.htmlData)
              try {
                if (buf.length == 0) {
                  console.info("[schemeHandler] length 0");
                  resourceHandler.didReceiveResponse(response);
                  // 如果认为buf.length为0是正常情况,则调用resourceHandler.didFinish,否则调用resourceHandler.didFail
                  resourceHandler.didFail(WebNetErrorList.ERR_FAILED);
                } else {
                  console.info("[schemeHandler] length 1");
                  resourceHandler.didReceiveResponse(response);
                  resourceHandler.didReceiveResponseBody(buf.buffer);
                  resourceHandler.didFinish();
                }
              } catch (error) {
                console.error(`[schemeHandler] ErrorCode: ${(error as BusinessError).code},  Message: ${(error as BusinessError).message}`);
              }
              return true;
            })

            this.schemeHandler.onRequestStop((request: webview.WebSchemeHandlerRequest) => {
              console.info("[schemeHandler] onRequestStop");
            });

            this.controller.setWebSchemeHandler('https', this.schemeHandler);
          } catch (error) {
            console.error(`ErrorCode: ${(error as BusinessError).code},  Message: ${(error as BusinessError).message}`);
          }
        })
        .javaScriptAccess(true)
        .domStorageAccess(true)
    }
  }
}

更多关于HarmonyOS 鸿蒙Next中resourceHandler.didReceiveResponseBody拦截网络请求失败的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


拦截失败是因为您缺少了设置响应头的必要步骤,这是拦截流程中的核心环节。

问题根因分析

在您的代码中,直接调用了 resourceHandler.didReceiveResponseBody() 来发送响应体,但没有先调用 didReceiveResponse() 设置响应头。完整的拦截响应流程必须是:

  1. 首先设置响应头didReceiveResponse(response)
  2. 然后发送响应体didReceiveResponseBody(data)
  3. 最后完成请求didFinish()didFail()

您跳过了第1步,导致Web内核无法正确解析响应。

完整修复方案

handleControllerAttached() {
  try {
    this.schemeHandler.onRequestStart((request: webview.WebSchemeHandlerRequest, resourceHandler: webview.WebResourceHandler) => {
      const type = request.getRequestResourceType();
      const stream = request.getHttpBodyStream();

      if (!stream || type !== webview.WebResourceType.XHR) {
        return false;
      }

      if (request.getRequestUrl().includes('xx.yy.api')) {
        stream.initialize().then(async () => {
          if (!stream || stream.getSize() <= 0) {
            return;
          }
          const buffer = await stream.read(stream.getSize()).catch(() => null);
          const obj = arrayBufferToObject(buffer);

          if (obj?.functionId == 'xxx_api') {
            try {
              // 【关键修复】首先创建并设置响应头
              let response = new webview.WebSchemeHandlerResponse();
              response.setStatus(200); // HTTP状态码
              response.setMimeType('application/json'); // 根据实际数据类型设置
              response.setCharset('utf-8');
              response.setHeaderByName('content-length', '1024', false); // 根据实际数据长度设置
              
              // 必须先调用didReceiveResponse设置响应头
              resourceHandler.didReceiveResponse(response);

              // mock网络请求数据
              const result: ESObject = this.getRawFileJsonData();
              let arrayBuffer = objectToArrayBuffer(result) as ArrayBuffer;
              
              if (arrayBuffer && resourceHandler) {
                // 现在可以安全地发送响应体
                resourceHandler.didReceiveResponseBody(arrayBuffer);
                resourceHandler.didFinish();
              } else {
                resourceHandler.didFail(WebNetErrorList.ERR_FAILED);
              }
            } catch (error) {
              console.error('拦截处理异常:', error);
              resourceHandler.didFail(WebNetErrorList.ERR_FAILED);
            }
          }
        }).catch((error: BusinessError) => {
          console.error('流初始化失败:', error);
        });
        return true; // 拦截此请求
      }
      return false; // 不拦截其他请求
    });

    this.schemeHandler.onRequestStop((request: webview.WebSchemeHandlerRequest) => {
      console.info('请求结束:', request.getRequestUrl());
    });

    this.controller.setWebSchemeHandler('https', this.schemeHandler);
  } catch (error) {
    console.error('设置拦截器失败:', error);
  }
}

技术要点说明

  1. 响应头必须优先设置
    • didReceiveResponse() 告诉Web内核准备接收响应
    • 缺少这一步会导致后续的响应体数据被忽略
  2. 响应头关键参数
    • setStatus(200): 设置HTTP状态码(200表示成功)
    • setMimeType(): 设置数据格式(JSON/HTML等)
    • setCharset(): 设置字符编码
    • setHeaderByName(): 设置Content-Length等头部信息

我这边即便设置了didReceiveResponse依然拦截不到接口请求: 完整代码如下:

Index.ets

import { WebNetErrorList, webview } from "@kit.ArkWeb";
import { BusinessError } from "@kit.BasicServicesKit";
import { buffer } from "@kit.ArkTS";
import { arrayBufferToObject, objectToArrayBuffer } from "./Utils";
import { common } from "@kit.AbilityKit";

@Entry
@Component
struct Index {
  controller: webview.WebviewController = new webview.WebviewController();
  schemeHandler: webview.WebSchemeHandler = new webview.WebSchemeHandler();
  url: string = 'https://pro.m.jd.com/hm/active/3mvMVoZb71igim1FHko9mSTeAneK/index.html?has_native=0';

  getRawFileJsonData(): ESObject {
    let context = this.getUIContext().getHostContext() as common.UIAbilityContext;
    let value: Uint8Array = context.resourceManager.getRawFileContentSync('mock.json');
    let mockStr = buffer.from(value.buffer).toString();
    let mockObj: ESObject = JSON.parse(mockStr)
    return mockObj
  }

  build() {
    Column() {
      Web({ src: '', controller: this.controller }).onControllerAttached(() => {

        this.controller.loadUrl(this.url)

        try {
          this.schemeHandler.onRequestStart((request: webview.WebSchemeHandlerRequest, resourceHandler: webview.WebResourceHandler) => {

            const type = request.getRequestResourceType();
            const stream = request.getHttpBodyStream()

            if (!stream || type !== webview.WebResourceType.XHR) {
              return false
            }

            if (request.getRequestUrl().includes('api.m.jd.com')) {
              stream.initialize().then(async () => {
                if (!stream || stream.getSize() <= 0) {
                  return;
                }
                const buffer = await stream.read(stream.getSize()).catch(() => null)
                const obj = arrayBufferToObject(buffer)

                console.info("[schemeHandler]: url", request.getRequestUrl());
                console.info("[schemeHandler]: body", JSON.stringify(obj));

                if (obj?.functionId == 'findBeanSceneNew') {
                  try {
                    // mock网络请求数据
                    const result: ESObject = this.getRawFileJsonData()
                    console.info("[schemeHandler]: response", JSON.stringify(result));
                    let arrayBuffer = objectToArrayBuffer(result) as ArrayBuffer;

                    const response = new webview.WebSchemeHandlerResponse();
                    // 设置网络错误代码为OK,表示请求成功。
                    response.setNetErrorCode(WebNetErrorList.NET_OK);
                    // 设置HTTP状态码为200,表示请求处理成功。
                    response.setStatus(200);
                    // 设置状态文本为"OK",用于描述状态码。
                    response.setStatusText("OK");
                    // 设置MIME类型为"application/json"
                    response.setMimeType("application/json");
                    // 设置编码方式为"utf-8",指明内容使用UTF-8编码。
                    response.setEncoding("utf-8");
                    // 根据实际数据长度设置
                    response.setHeaderByName('content-length', arrayBuffer.byteLength.toString(), false);
                    // 必须先调用didReceiveResponse设置响应头
                    resourceHandler.didReceiveResponse(response);

                    if (arrayBuffer && resourceHandler) {
                      // 调用didReceiveResponseBody将构造的响应体传递给被拦截的请求。
                      resourceHandler.didReceiveResponseBody(arrayBuffer);
                      // 调用didFinish通知Web组件被拦截的请求已经完成。
                      resourceHandler.didFinish();
                    } else {
                      resourceHandler.didFail(WebNetErrorList.ERR_FAILED);
                    }
                  } catch (error) {
                    console.error(`[schemeHandler]: ErrorCode: ${(error as BusinessError).code},  Message: ${(error as BusinessError).message}`);
                  }
                }
              }).catch((error: BusinessError) => {
                console.info(`[schemeHandler]: stream error: ${error.code},  Message: ${error.message}`);
              })
              return true
            }
            return false
          })

          this.schemeHandler.onRequestStop((request: webview.WebSchemeHandlerRequest) => {
            console.info('[schemeHandler] onRequestStop');
          });

          this.controller.setWebSchemeHandler('https', this.schemeHandler);
        } catch (error) {
          console.error(`ErrorCode: ${(error as BusinessError).code},  Message: ${(error as BusinessError).message}`);
        }
      })
        .javaScriptAccess(true)
        .domStorageAccess(true)
    }
  }
}

Utils.ets

import { util } from "@kit.ArkTS";
import Url from '@ohos.url';

/**
 * 将Object转换为ArrayBuffer
 * @param obj 要转换的对象
 * @returns 对应的ArrayBuffer
 */
export function objectToArrayBuffer(obj: Object): ArrayBuffer {
  try {
    // 1. 将对象序列化为JSON字符串
    const jsonString: string = JSON.stringify(obj);
    console.info('JSON String: ' + jsonString);

    // 2. 使用TextEncoder将字符串编码为Uint8Array
    const textEncoder = new util.TextEncoder();
    const uint8Array: Uint8Array = textEncoder.encodeInto(jsonString);

    // 3. 直接返回,让编译器推断类型
    return uint8Array.buffer;
  } catch (error) {
    console.error('Error converting Object to ArrayBuffer: ' + error.message);
    return new ArrayBuffer(0);
  }
}

interface GeneratedObjectLiteralInterface_1 {
  appId: string | null;
  functionId: string | null;
  bodyJsonString: string | null;
}

export function arrayBufferToObject(buffer: ArrayBuffer | null): GeneratedObjectLiteralInterface_1 | null {
  try {
    if (buffer === null) {
      return null
    }
    // 1. 将ArrayBuffer转换为Uint8Array
    const uint8Array: Uint8Array = new Uint8Array(buffer);

    // 2. 使用TextDecoder将Uint8Array解码为字符串
    const textDecoder: util.TextDecoder = new util.TextDecoder;
    const jsonString: string = textDecoder.decode(uint8Array);

    const urlParams = new Url.URLParams(jsonString);
    const appId = urlParams.get('appid');
    const functionId = urlParams.get('functionId');
    const bodyJsonString = urlParams.get('body');

    // 3. 将JSON字符串解析为Object
    const obj: GeneratedObjectLiteralInterface_1 = {
      appId: appId,
      functionId: functionId,
      bodyJsonString: bodyJsonString
    }

    return obj;
  } catch (error) {
    console.error('Error converting ArrayBuffer to Object: ' + error.message);
    return null; // 返回一个空对象或者在失败时根据需求处理
  }
}

mock.json

{
  "code": "0",
  "data": {
    "status": "这是web容器填充的数据",
    "userState": "1",
    "beanUserType": 1,
    "totalUserBean": "47",
    "continuousDays": "1",
    "tomorrowSendBeans": 0,
    "todaySendBeans": 0
  }
}

报错原因:resourceHandler.didReceiveResponseBody拦截网络请求失败,可以通过错误码来分析,请求拦截了,说明请求成功了,而拦截失败,说明请求失败了,请求失败的原因可以追踪下错误信息,大概原因是响应头构造问题

解决方案:在resourceHandler.didFinish();方法之前必须调用resourceHandler.didReceiveResponse(response);

详细步骤:

//此处省略....

   // 调用didReceiveResponse将构造的响应头传递给被拦截的请求。
   resourceHandler.didReceiveResponse(response);

   // 调用didReceiveResponseBody将构造的响应体传递给被拦截的请求。
   resourceHandler.didReceiveResponseBody(buf.buffer);

   // 调用didFinish通知Web组件被拦截的请求已经完成。
   resourceHandler.didFinish();

   //.......

我看官网有完整示例 你要不下载下来看看 GuideSnippets: HarmonyOS 指南代码同源仓。 - Gitee.com
你可以从下面两个方面直接定位问题 这样好找到答案

  1. 打印拦截过程日志:在onRequestStart中打印urltype是否进入拦截逻辑,确认目标请求是否被捕获;
  2. 简化拦截逻辑:先忽略请求体解析,直接返回固定 mock 数据,验证基础拦截是否生效;

从以下问题进行一些检查和改进

1.根据官方API定义,调用didReceiveResponseBody前必须通过didReceiveResponse()设置响应头。未设置会导致内核无法正确处理响应数据。

2.setWebSchemeHandler默认不支持拦截https协议(需使用自定义scheme如myapp://)。若需拦截标准协议,需检查系统兼容性配置。

3.stream.initialize()为异步操作,可能导致回调执行时resourceHandler已失效。需在Promise链中保持Handler有效性。

在鸿蒙Next中,resourceHandler.didReceiveResponseBody 拦截网络请求失败通常与资源管理器配置或拦截器生命周期有关。请检查 ResourceManager 是否正确初始化,并确保拦截器在请求发起前已注册。同时,确认网络权限已开启,且拦截逻辑未在异步操作中失效。

根据你提供的代码,拦截失败的核心原因在于异步处理逻辑resourceHandler 的生命周期管理存在问题。

主要问题分析:

  1. didReceiveResponseBody 调用时机不当:你在 stream.initialize().then() 这个异步回调中调用 resourceHandler.didReceiveResponseBody()onRequestStart 回调是同步的,它需要你立即决定是否接管请求(返回 truefalse)。然而,你返回 true 接管请求后,实际的响应体 (arrayBuffer) 却在未来的某个异步时刻才准备好并调用 didReceiveResponseBody。这很可能导致 Web 内核在等待响应时超时或状态不一致,从而拦截失败。

  2. resourceHandler 可能已失效:在异步操作执行期间,如果原始网络请求发生错误、被取消或超时,与之关联的 resourceHandler 可能会变为无效状态。此时再调用其 didReceiveResponseBodydidFinish 方法将不起作用。

修改建议:

你需要确保在 onRequestStart 回调中同步地完成对请求的响应,或者以同步方式启动一个能立即提供响应数据的机制。

重构方案示例:

handleControllerAttached() {
  try {
    this.schemeHandler.onRequestStart((request: webview.WebSchemeHandlerRequest, resourceHandler: webview.WebResourceHandler) => {
      const type = request.getRequestResourceType();
      const url = request.getRequestUrl();

      // 1. 更精确地筛选需要拦截的请求
      if (type !== webview.WebResourceType.XHR || !url.includes('xx.yy.api')) {
        return false; // 不拦截
      }

      // 2. 【关键】尝试同步或快速获取Mock数据,避免复杂异步链
      // 例如,你的 `getRawFileJsonData` 方法应该是同步的,或返回一个预先准备好的缓存数据。
      let mockDataResult: ESObject | null = null;
      let targetFunctionId: string | null = null;

      // 2.1 尝试快速读取请求体(对于POST请求)
      const stream = request.getHttpBodyStream();
      if (stream) {
        // 注意:这里简化处理,实际生产环境需要更完善的异步转同步或快速解析逻辑。
        // 理想情况是能同步解析出 functionId,或者拦截逻辑不依赖请求体。
        // 如果必须依赖异步读取请求体,则此方案不适用,需考虑其他拦截策略。
        // 以下仅为示例,表示“如果能快速得到结果”:
        // const buffer = someSyncOrVeryFastRead(stream);
        // const obj = arrayBufferToObject(buffer);
        // targetFunctionId = obj?.functionId;
      }

      // 2.2 判断是否需要拦截(这里假设通过URL已足够判断,或已同步获取到targetFunctionId)
      // if (url.includes('specific_api_path') || targetFunctionId === 'xxx_api') {
      if (url.includes('xxx_api')) { // 示例:直接通过URL判断
        mockDataResult = this.getRawFileJsonData(); // 假设这是同步方法
      }

      if (mockDataResult) {
        // 3. 【关键】同步地构造响应并提交
        try {
          const arrayBuffer = objectToArrayBuffer(mockDataResult) as ArrayBuffer;
          if (arrayBuffer) {
            // 立即调用,无需等待异步操作
            resourceHandler.didReceiveResponseBody(arrayBuffer);
            resourceHandler.didFinish();
            return true; // 已成功拦截并处理
          } else {
            resourceHandler.didFail(WebNetErrorList.ERR_FAILED);
            return true; // 已接管,但告知失败
          }
        } catch (error) {
          resourceHandler.didFail(WebNetErrorList.ERR_FAILED);
          return true;
        }
      }

      // 4. 如果不满足拦截条件,或无法同步提供数据,则返回false,让请求继续。
      return false;
    });

    this.controller.setWebSchemeHandler('https', this.schemeHandler);
  } catch (error) {
    console.error('Failed to setup scheme handler:', error);
  }
}

核心要点总结:

  • 同步响应onRequestStart 中的拦截决策和响应提交应尽可能同步完成。
  • 简化逻辑:避免在 onRequestStart 中执行耗时的异步操作(如文件读取、网络请求等)。Mock数据应预先加载到内存中。
  • 准确筛选:使用 request.getRequestUrl()request.getRequestResourceType() 精确识别需要拦截的请求。如果必须依赖请求体 (getHttpBodyStream) 内容做判断,你需要评估其读取速度,或者重新设计拦截策略(例如,在更早的阶段通过其他方式标记请求)。
  • 生命周期:一旦返回 true,必须在该同步上下文或一个能确保 resourceHandler 有效的极短时间内调用 didReceiveResponseBodydidFinish(或 didFail)。

请根据上述要点调整你的代码,重点是将异步的Mock数据准备过程提前,在 onRequestStart 中只进行同步的判断和响应

回到顶部