Skip to content

如何使用 JavaScript 跟踪网页上的所有 HTTP 请求

发布于  at 15:30
const K = 2 ** 10;
const UNITS = ["B", "KiB", "MiB", "TiB", "PiB"];
export function formatSize(B: number = 0): string {
  let n = B;
  let i = 0;
  while (n > K && i < UNITS.length - 1) {
    n /= K;
    i += 1;
  }
  const unit = UNITS[i];
  if (i === 0) return n + unit;
  return n.toFixed(2) + unit;
}

export async function formatBlob(blob: Blob): Promise<string> {
  const size = formatSize(blob.size);
  let head = `[Blob ${size}`;
  if (blob.type) {
    head += ` ${blob.type}`;
  }
  if (blob instanceof File) {
    head += ` ${blob.name}`;
  }
  head += "]:";
  head += await blob.text();
  return head;
}

export async function formatBody(body: unknown) {
  // body stringify
  if (typeof body === "string") {
    return body;
  }
  if (body instanceof Blob) {
    return formatBlob(body);
  }
  if (body instanceof FormData) {
    let theText: string = "[FormData]";
    if (!body.entries) {
      return theText;
    }
    // let totalSize = 0;
    // let theJson: Record<string, string> = {};
    for (let [k, v] of body.entries()) {
      // if (totalSize > K) break;
      if (v instanceof Blob) {
        theText += `\n${k}: ${await formatBlob(v)}`;
        // totalSize += v.size;
        // theJson[k] = await formatBlob(v);
      } else if (typeof v === "string") {
        // const s = sizeOfString(v);
        // totalSize += s;
        // theJson[k] = v;
        theText += `\n${k}: ${v}`;
      }
    }
    return `${theText}`;
  }
  if (body) {
    return Object.prototype.toString.call(body);
  }
}

export const stringifyJSON = (circ: unknown) => {
  let cache: unknown[] = [];
  return JSON.stringify(circ, (key, value) => {
    if (typeof value === "object" && value !== null) {
      // console.log(key, value);
      if (cache.includes(value)) return;
      cache.push(value);
    }
    return value;
  });
};

function overrideMethod<T extends Record<PropertyKey, any>, K extends keyof T>(
  target: T,
  key: K,
  replacement: (f: T[K]) => T[K]
) {
  if (key in target) {
    const originFn = target[key];
    const replaced = replacement(originFn);
    if (typeof replaced === "function") {
      // eslint-disable-next-line no-param-reassign
      target[key] = replaced;
    }
  }
}

declare global {
  interface XMLHttpRequest {
    // 增加自定义属性以方便记录
    requestHeaders?: Record<string, string>;
    method?: string;
    url?: string | URL;
    requestText?: string;
  }
}

// 记录请求头
overrideMethod(
  XMLHttpRequest.prototype,
  "setRequestHeader",
  originFn =>
    function (this: XMLHttpRequest, ...args: Parameters<XMLHttpRequest["setRequestHeader"]>) {
      try {
        if (!this.requestHeaders) this.requestHeaders = {};
        const [key, value] = args;
        if (this.requestHeaders[key]) {
          this.requestHeaders[key] += `, ${value}`;
        } else {
          this.requestHeaders[key] = value;
        }
      } catch (e) {
        // ignore
      }
      return originFn.apply(this, args);
    }
);

// 记录请求方法和地址
overrideMethod(
  XMLHttpRequest.prototype,
  "open",
  originFn =>
    function (this: XMLHttpRequest, method: string, url: string | URL, ...rest: any[]) {
      try {
        this.method = method;
        this.url = url;
      } catch (e) {
        // ignore
      }
      return originFn.apply(this, [method, url, ...rest]);
    }
);

overrideMethod(
  XMLHttpRequest.prototype,
  "send",
  originFn =>
    function (this: XMLHttpRequest, ...args: Parameters<XMLHttpRequest["send"]>) {
      try {
        let { method = "GET", url = "", responseURL = "" } = this;
        url = url || responseURL;
        const urlString: string = url instanceof URL ? url.href : url || responseURL;
        const body = args[0];

        // body stringify
        formatBody(body).then(s => {
          this.requestText = s;
        });
        const sendTime = Date.now();

        this.addEventListener("readystatechange", async () => {
          if (this.readyState === XMLHttpRequest.DONE) {
            let response: string = "";
            switch (this.responseType) {
              case "":
              case "text":
                response = this.responseText;
                break;
              case "json":
                response = stringifyJSON(this.response);
                break;
              case "blob":
                response = await formatBlob(this.response);
                break;
              default:
                response = `unhandle responseType: ${this.responseType}`;
            }

            console.log({
              category: "http",
              type: "xhr",
              time: Date.now(),
              sendTime,
              data: {
                status: this.status,
                method,
                url: urlString,
                requestHeaders: this.requestHeaders,
                response,
                request: this.requestText,
              },
            });
          }
        });
      } catch (error) {
        // ignore
      }

      return originFn.apply(this, args);
    }
);

if ("fetch" in window) {
  overrideMethod(
    window,
    "fetch",
    originFn =>
      function (...args) {
        let url: string;
        let method = "GET";
        const [requestInfo, requestInit] = args;
        const sendTime = Date.now();
        let requestText: string | undefined;
        const requestHeaders: Record<string, string> = {};

        try {
          if (typeof requestInfo === "string") {
            url = requestInfo;
            method = requestInit?.method ?? "GET";
          } else if (requestInfo instanceof Request) {
            const request = requestInfo.clone();
            // 后面的 requestInit 会覆盖前面的 request
            url = request.url;
            method = requestInit?.method || request.method;
            // body stringify
            const body = requestInit?.body || request.body;
            // if (body instanceof ReadableStream) {
            //   body = body.clone();
            // }
            formatBody(body).then(s => {
              requestText = s;
            });

            // requestHeaders
            const headers: Headers | string[][] | Record<string, string> | undefined =
              requestInit?.headers || request.headers;

            if (headers instanceof Headers) {
              for (const [k, v] of headers.entries()) {
                // console.log(`${k}: ${v}`);
                requestHeaders[k] = v;
              }
            } else if (Array.isArray(headers)) {
              headers.forEach(([k, v]) => {
                if (requestHeaders[k]) {
                  requestHeaders[k] += `, ${v}`;
                } else {
                  requestHeaders[k] = v;
                }
              });
            } else if (typeof headers === "object") {
              Object.entries(headers).forEach(([k, v]) => {
                // console.log(`${k}: ${v}`);
                requestHeaders[k] = v;
              });
            } else console.log("unknown req header", headers);
          }
        } catch (error) {
          // ignore
        }

        return originFn
          .apply(window, args)
          .then(async (response: Response) => {
            // logIndicator('Fetch', method, url);
            try {
              const clonedResponse = response.clone();
              const contentType = clonedResponse.headers.get("Content-Type") || "";
              let responseText = contentType;
              if (/^(application\/(xml|json))|(text\/\w+)/.test(contentType)) {
                responseText = await clonedResponse.text();
              } else {
                const blob = await clonedResponse.blob();
                responseText = await formatBlob(blob);
              }
              console.log({
                category: "http",
                type: "fetch",
                time: Date.now(),
                sendTime,
                data: {
                  status: clonedResponse.status,
                  method,
                  url: url || clonedResponse.url,
                  request: requestText,
                  requestHeaders: Object.keys(requestHeaders).length === 0 ? undefined : requestHeaders,
                  response: responseText,
                },
              });
            } catch (error) {
              // ignore
            }
            return response;
          })
          .catch((error: Error) => {
            // 这里只能是发送失败了,跟上面不太一样
            console.log({
              category: "http",
              type: "fetch",
              time: Date.now(),
              sendTime,
              data: {
                status: 0,
                method,
                url,
              },
            });
            throw error;
          });
      }
  );
}
分享到: