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>
Showing
4 changed files
with
76 additions
and
15 deletions
| ... | @@ -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; | ... | ... |
-
Please register or login to post a comment