hookehuyr

fix(apifox): 改进 HTTPS 请求处理和数据验证

- 使用 Buffer.concat() 正确处理二进制数据流
- 添加空响应处理,避免解析错误
- 移除 UTF-8 BOM 标记(\uFEFF)
- 使用 Array.isArray() 验证响应数据类型
- 统一改进 apifox-sync.js、apifox-to-openapi.js、test-apifox-connection.js

提升 Apifox API 集成的稳定性和容错能力

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
...@@ -50,6 +50,25 @@ ...@@ -50,6 +50,25 @@
50 50
51 --- 51 ---
52 52
53 +## [2026-02-02] - 修复 Apifox 响应解析
54 +
55 +### 修复
56 +- 修复 Apifox 空响应导致 JSON 解析失败的问题
57 + - 空响应时按空数据处理并返回可识别状态
58 + - 接口列表解析增加空响应提示
59 + - 影响文件:scripts/test-apifox-connection.js, scripts/apifox-sync.js, scripts/apifox-to-openapi.js
60 +
61 +---
62 +
63 +**详细信息**
64 +- **影响文件**: scripts/test-apifox-connection.js, scripts/apifox-sync.js, scripts/apifox-to-openapi.js
65 +- **技术栈**: Node.js
66 +- **测试状态**: ✅ 已通过(pnpm api:test, pnpm lint;lint 有现存警告)
67 +- **备注**:
68 + - 接口列表为空时不会中断连接测试
69 +
70 +---
71 +
53 ## [2026-02-02] - 修复 Apifox Token 验证 72 ## [2026-02-02] - 修复 Apifox Token 验证
54 73
55 ### 修复 74 ### 修复
......
...@@ -78,19 +78,31 @@ function loadEnv() { ...@@ -78,19 +78,31 @@ function loadEnv() {
78 function httpsRequest(options) { 78 function httpsRequest(options) {
79 return new Promise((resolve, reject) => { 79 return new Promise((resolve, reject) => {
80 const req = https.request(options, (res) => { 80 const req = https.request(options, (res) => {
81 - let data = ''; 81 + const chunks = [];
82 82
83 res.on('data', chunk => { 83 res.on('data', chunk => {
84 - data += chunk; 84 + chunks.push(chunk);
85 }); 85 });
86 86
87 res.on('end', () => { 87 res.on('end', () => {
88 + const raw = Buffer.concat(chunks).toString('utf8').trim();
89 +
90 + if (!raw) {
91 + if (res.statusCode >= 200 && res.statusCode < 300) {
92 + resolve({ data: null, total: 0, __empty: true, statusCode: res.statusCode });
93 + return;
94 + }
95 +
96 + reject(new Error(`HTTP ${res.statusCode}: Empty response`));
97 + return;
98 + }
99 +
88 try { 100 try {
89 - const json = JSON.parse(data); 101 + const json = JSON.parse(raw.replace(/^\uFEFF/, ''));
90 if (res.statusCode === 200) { 102 if (res.statusCode === 200) {
91 resolve(json); 103 resolve(json);
92 } else { 104 } else {
93 - reject(new Error(`HTTP ${res.statusCode}: ${json.message || data}`)); 105 + reject(new Error(`HTTP ${res.statusCode}: ${json.message || raw}`));
94 } 106 }
95 } catch (err) { 107 } catch (err) {
96 reject(new Error(`解析响应失败: ${err.message}`)); 108 reject(new Error(`解析响应失败: ${err.message}`));
...@@ -119,7 +131,7 @@ async function fetchApis() { ...@@ -119,7 +131,7 @@ async function fetchApis() {
119 131
120 try { 132 try {
121 const response = await httpsRequest(options); 133 const response = await httpsRequest(options);
122 - const apis = response.data || []; 134 + const apis = Array.isArray(response.data) ? response.data : [];
123 135
124 log(`✅ 成功获取 ${apis.length} 个接口`, 'green'); 136 log(`✅ 成功获取 ${apis.length} 个接口`, 'green');
125 return apis; 137 return apis;
......
...@@ -84,19 +84,31 @@ function loadEnv() { ...@@ -84,19 +84,31 @@ function loadEnv() {
84 function httpsRequest(options) { 84 function httpsRequest(options) {
85 return new Promise((resolve, reject) => { 85 return new Promise((resolve, reject) => {
86 const req = https.request(options, (res) => { 86 const req = https.request(options, (res) => {
87 - let data = ''; 87 + const chunks = [];
88 88
89 res.on('data', chunk => { 89 res.on('data', chunk => {
90 - data += chunk; 90 + chunks.push(chunk);
91 }); 91 });
92 92
93 res.on('end', () => { 93 res.on('end', () => {
94 + const raw = Buffer.concat(chunks).toString('utf8').trim();
95 +
96 + if (!raw) {
97 + if (res.statusCode >= 200 && res.statusCode < 300) {
98 + resolve({ data: null, total: 0, __empty: true, statusCode: res.statusCode });
99 + return;
100 + }
101 +
102 + reject(new Error(`HTTP ${res.statusCode}: Empty response`));
103 + return;
104 + }
105 +
94 try { 106 try {
95 - const json = JSON.parse(data); 107 + const json = JSON.parse(raw.replace(/^\uFEFF/, ''));
96 if (res.statusCode === 200) { 108 if (res.statusCode === 200) {
97 resolve(json); 109 resolve(json);
98 } else { 110 } else {
99 - reject(new Error(`HTTP ${res.statusCode}: ${json.message || data}`)); 111 + reject(new Error(`HTTP ${res.statusCode}: ${json.message || raw}`));
100 } 112 }
101 } catch (err) { 113 } catch (err) {
102 reject(new Error(`解析响应失败: ${err.message}`)); 114 reject(new Error(`解析响应失败: ${err.message}`));
...@@ -125,7 +137,7 @@ async function fetchApis() { ...@@ -125,7 +137,7 @@ async function fetchApis() {
125 137
126 try { 138 try {
127 const response = await httpsRequest(options); 139 const response = await httpsRequest(options);
128 - const apis = response.data || []; 140 + const apis = Array.isArray(response.data) ? response.data : [];
129 141
130 log(`✅ 成功获取 ${apis.length} 个接口`, 'green'); 142 log(`✅ 成功获取 ${apis.length} 个接口`, 'green');
131 return apis; 143 return apis;
......
...@@ -100,19 +100,31 @@ function httpsRequest(options) { ...@@ -100,19 +100,31 @@ function httpsRequest(options) {
100 log(`📡 发送请求到: https://${options.hostname}${options.path}`, 'blue'); 100 log(`📡 发送请求到: https://${options.hostname}${options.path}`, 'blue');
101 101
102 const req = https.request(options, (res) => { 102 const req = https.request(options, (res) => {
103 - let data = ''; 103 + const chunks = [];
104 104
105 res.on('data', chunk => { 105 res.on('data', chunk => {
106 - data += chunk; 106 + chunks.push(chunk);
107 }); 107 });
108 108
109 res.on('end', () => { 109 res.on('end', () => {
110 + const raw = Buffer.concat(chunks).toString('utf8').trim();
111 +
112 + if (!raw) {
113 + if (res.statusCode >= 200 && res.statusCode < 300) {
114 + resolve({ data: null, total: 0, __empty: true, statusCode: res.statusCode });
115 + return;
116 + }
117 +
118 + reject(new Error(`HTTP ${res.statusCode}: Empty response`));
119 + return;
120 + }
121 +
110 try { 122 try {
111 - const json = JSON.parse(data); 123 + const json = JSON.parse(raw.replace(/^\uFEFF/, ''));
112 if (res.statusCode === 200) { 124 if (res.statusCode === 200) {
113 resolve(json); 125 resolve(json);
114 } else { 126 } else {
115 - reject(new Error(`HTTP ${res.statusCode}: ${json.message || data}`)); 127 + reject(new Error(`HTTP ${res.statusCode}: ${json.message || raw}`));
116 } 128 }
117 } catch (err) { 129 } catch (err) {
118 reject(new Error(`解析响应失败: ${err.message}`)); 130 reject(new Error(`解析响应失败: ${err.message}`));
...@@ -141,6 +153,10 @@ async function testProjectInfo(config) { ...@@ -141,6 +153,10 @@ async function testProjectInfo(config) {
141 153
142 try { 154 try {
143 const response = await httpsRequest(options); 155 const response = await httpsRequest(options);
156 + if (!response || !response.data) {
157 + throw new Error('响应为空或缺少 data 字段');
158 + }
159 +
144 const project = response.data; 160 const project = response.data;
145 161
146 log('✅ 项目信息获取成功', 'green'); 162 log('✅ 项目信息获取成功', 'green');
...@@ -186,7 +202,7 @@ async function testApiList(config) { ...@@ -186,7 +202,7 @@ async function testApiList(config) {
186 202
187 try { 203 try {
188 const response = await httpsRequest(options); 204 const response = await httpsRequest(options);
189 - const apis = response.data || []; 205 + const apis = Array.isArray(response.data) ? response.data : [];
190 206
191 log('✅ 接口列表获取成功', 'green'); 207 log('✅ 接口列表获取成功', 'green');
192 log(` 共找到 ${response.total || apis.length} 个接口`, 'blue'); 208 log(` 共找到 ${response.total || apis.length} 个接口`, 'blue');
...@@ -205,6 +221,8 @@ async function testApiList(config) { ...@@ -205,6 +221,8 @@ async function testApiList(config) {
205 if (response.total > 10) { 221 if (response.total > 10) {
206 log(`\n ... 还有 ${response.total - 10} 个接口`, 'yellow'); 222 log(`\n ... 还有 ${response.total - 10} 个接口`, 'yellow');
207 } 223 }
224 + } else if (response.__empty) {
225 + log('⚠️ 接口列表响应为空,已按空列表处理', 'yellow');
208 } 226 }
209 227
210 return true; 228 return true;
......