UNPKG

@nsnanocat/util

Version:

Pure JS's util module for well-known iOS network tools

204 lines (202 loc) 6.84 kB
import { $app } from "../lib/app.mjs"; import { Console } from "./Console.mjs"; import { Lodash as _ } from "./Lodash.mjs"; import { StatusTexts } from "./StatusTexts.mjs"; /** * fetch * * @link https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API * @export * @async * @param {object|string} resource * @param {object} [options] * @returns {Promise<object>} */ export async function fetch(resource, options = {}) { // 初始化参数 switch (typeof resource) { case "object": resource = { ...options, ...resource }; break; case "string": resource = { ...options, url: resource }; break; case "undefined": default: throw new TypeError(`${Function.name}: 参数类型错误, resource 必须为对象或字符串`); } // 自动判断请求方法 if (!resource.method) { resource.method = "GET"; if (resource.body ?? resource.bodyBytes) resource.method = "POST"; } // 移除请求头中的部分参数, 让其自动生成 delete resource.headers?.Host; delete resource.headers?.[":authority"]; delete resource.headers?.["Content-Length"]; delete resource.headers?.["content-length"]; // 定义请求方法(小写) const method = resource.method.toLocaleLowerCase(); // 转换请求超时时间参数 if (!resource.timeout) resource.timeout = 5; if (resource.timeout) { resource.timeout = Number.parseInt(resource.timeout, 10); // 转换为秒,大于500视为毫秒,小于等于500视为秒 if (resource.timeout > 500) resource.timeout = Math.round(resource.timeout / 1000); } // 判断平台 switch ($app) { case "Loon": case "Surge": case "Stash": case "Egern": case "Shadowrocket": default: // 转换请求参数 if (resource.timeout) { switch ($app) { case "Loon": resource.timeout = resource.timeout * 1000; break; case "Shadowrocket": case "Stash": case "Egern": case "Surge": default: break; } } if (resource.policy) { switch ($app) { case "Loon": resource.node = resource.policy; break; case "Stash": _.set(resource, "headers.X-Stash-Selected-Proxy", encodeURI(resource.policy)); break; case "Shadowrocket": _.set(resource, "headers.X-Surge-Proxy", resource.policy); break; } } if (typeof resource.redirection === "boolean") resource["auto-redirect"] = resource.redirection; // 转换请求体 if (resource.bodyBytes && !resource.body) { resource.body = resource.bodyBytes; resource.bodyBytes = undefined; } // 判断是否请求二进制响应体 switch ((resource.headers?.Accept || resource.headers?.accept)?.split(";")?.[0]) { case "application/protobuf": case "application/x-protobuf": case "application/vnd.google.protobuf": case "application/vnd.apple.flatbuffer": case "application/grpc": case "application/grpc+proto": case "application/octet-stream": resource["binary-mode"] = true; break; } // 发送请求 return await new Promise((resolve, reject) => { $httpClient[method](resource, (error, response, body) => { if (error) reject(error); else { response.ok = /^2\d\d$/.test(response.status); response.statusCode = response.status; response.statusText = StatusTexts[response.status]; if (body) { response.body = body; if (resource["binary-mode"] == true) response.bodyBytes = body; } resolve(response); } }); }); case "Quantumult X": // 转换请求参数 resource.timeout = resource.timeout * 1000; if (resource.policy) _.set(resource, "opts.policy", resource.policy); if (typeof resource["auto-redirect"] === "boolean") _.set(resource, "opts.redirection", resource["auto-redirect"]); // 转换请求体 if (resource.body instanceof ArrayBuffer) { resource.bodyBytes = resource.body; resource.body = undefined; } else if (ArrayBuffer.isView(resource.body)) { resource.bodyBytes = resource.body.buffer.slice(resource.body.byteOffset, resource.body.byteLength + resource.body.byteOffset); resource.body = undefined; } else if (resource.body) resource.bodyBytes = undefined; // 发送请求 return Promise.race([ await $task.fetch(resource).then( response => { response.ok = /^2\d\d$/.test(response.statusCode); response.status = response.statusCode; response.statusText = StatusTexts[response.status]; switch ((response.headers?.["Content-Type"] ?? response.headers?.["content-type"])?.split(";")?.[0]) { case "application/protobuf": case "application/x-protobuf": case "application/vnd.google.protobuf": case "application/vnd.apple.flatbuffer": case "application/grpc": case "application/grpc+proto": case "application/octet-stream": response.body = response.bodyBytes; break; case undefined: default: break; } response.bodyBytes = undefined; return response; }, reason => Promise.reject(reason.error), ), new Promise((resolve, reject) => { setTimeout(() => { reject(new Error(`${Function.name}: 请求超时, 请检查网络后重试`)); }, resource.timeout); }), ]); case "Node.js": { const nodeFetch = globalThis.fetch ? globalThis.fetch : require("node-fetch"); const fetchCookie = globalThis.fetchCookie ? globalThis.fetchCookie : require("fetch-cookie").default; const fetch = fetchCookie(nodeFetch); // 转换请求参数 resource.timeout = resource.timeout * 1000; resource.redirect = resource.redirection ? "follow" : "manual"; const { url, ...options } = resource; // 发送请求 return Promise.race([ await fetch(url, options) .then(async response => { const bodyBytes = await response.arrayBuffer(); let headers; try { headers = response.headers.raw(); } catch { headers = Array.from(response.headers.entries()).reduce((acc, [key, value]) => { acc[key] = acc[key] ? [...acc[key], value] : [value]; return acc; }, {}); } return { ok: response.ok ?? /^2\d\d$/.test(response.status), status: response.status, statusCode: response.status, statusText: response.statusText, body: new TextDecoder("utf-8").decode(bodyBytes), bodyBytes: bodyBytes, headers: Object.fromEntries(Object.entries(headers).map(([key, value]) => [key, key.toLowerCase() !== "set-cookie" ? value.toString() : value])), }; }) .catch(error => Promise.reject(error.message)), new Promise((resolve, reject) => { setTimeout(() => { reject(new Error(`${Function.name}: 请求超时, 请检查网络后重试`)); }, resource.timeout); }), ]); } } }