Skip to content

Commit 31234c5

Browse files
author
githubnull
committed
fix: 修复fetch解析器无法处理转义引号问题
- 重写matchJsString函数正确解析JavaScript转义字符 - 重写findMatchingBrace函数处理嵌套大括号 - 重写extractHeaders逐字符解析转义引号 - 重写extractBody支持转义引号内容 - 修复sec-ch-ua等header值被截断问题 - 修复body中JSON转义引号被截断问题
1 parent b8318d6 commit 31234c5

1 file changed

Lines changed: 225 additions & 30 deletions

File tree

src/frontEnd/src/utils/httpRequestParser/parsers/fetchParser.ts

Lines changed: 225 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
* HTTP请求解析器 - Fetch格式解析模块
33
*
44
* 支持解析浏览器fetch API和Node.js fetch格式
5+
*
6+
* 特殊处理:
7+
* - JavaScript字符串中的转义引号 \"
8+
* - 嵌套的JSON对象
9+
* - 多行格式
510
*/
611

712
import { parseUrl } from '../urlParser'
@@ -11,39 +16,185 @@ import type { ParsedHttpRequest } from '../types'
1116
* 从fetch调用中提取URL
1217
*/
1318
function extractUrl(input: string): string {
14-
const urlMatch = input.match(/fetch\s*\(\s*['"]([^'"]+)['"]/i)
15-
return urlMatch && urlMatch[1] ? urlMatch[1] : ''
19+
// 支持双引号和单引号
20+
const urlMatch = input.match(/fetch\s*\(\s*(['"])((?:(?!\1)[^\\]|\\.)*)\1/i)
21+
return urlMatch && urlMatch[2] ? urlMatch[2] : ''
22+
}
23+
24+
/**
25+
* 匹配JavaScript字符串(处理转义引号)
26+
* 返回匹配的字符串内容(不含引号)
27+
*/
28+
function matchJsString(str: string, startIndex: number): { value: string; endIndex: number } | null {
29+
if (startIndex >= str.length) return null
30+
31+
const quoteChar = str[startIndex]
32+
if (quoteChar !== '"' && quoteChar !== "'") return null
33+
34+
let i = startIndex + 1
35+
let value = ''
36+
37+
while (i < str.length) {
38+
const char = str[i]
39+
40+
if (char === '\\' && i + 1 < str.length) {
41+
// 处理转义字符
42+
const nextChar = str[i + 1]
43+
if (nextChar === '"' || nextChar === "'" || nextChar === '\\') {
44+
value += nextChar
45+
i += 2
46+
continue
47+
} else if (nextChar === 'n') {
48+
value += '\n'
49+
i += 2
50+
continue
51+
} else if (nextChar === 'r') {
52+
value += '\r'
53+
i += 2
54+
continue
55+
} else if (nextChar === 't') {
56+
value += '\t'
57+
i += 2
58+
continue
59+
}
60+
// 其他转义保持原样
61+
value += char
62+
i++
63+
} else if (char === quoteChar) {
64+
// 找到结束引号
65+
return { value, endIndex: i }
66+
} else {
67+
value += char
68+
i++
69+
}
70+
}
71+
72+
return null // 未找到结束引号
73+
}
74+
75+
/**
76+
* 查找匹配的大括号位置
77+
*/
78+
function findMatchingBrace(str: string, startIndex: number): number {
79+
let depth = 0
80+
let inString = false
81+
let stringChar = ''
82+
83+
for (let i = startIndex; i < str.length; i++) {
84+
const char = str[i]
85+
86+
if (inString) {
87+
if (char === '\\' && i + 1 < str.length) {
88+
i++ // 跳过转义字符
89+
continue
90+
}
91+
if (char === stringChar) {
92+
inString = false
93+
}
94+
} else {
95+
if (char === '"' || char === "'") {
96+
inString = true
97+
stringChar = char
98+
} else if (char === '{') {
99+
depth++
100+
} else if (char === '}') {
101+
depth--
102+
if (depth === 0) {
103+
return i
104+
}
105+
}
106+
}
107+
}
108+
109+
return -1
16110
}
17111

18112
/**
19113
* 从fetch options对象中提取method
20114
*/
21115
function extractMethod(optionsStr: string): string {
116+
// 匹配 "method": "POST" 或 method: "POST"
22117
const methodMatch = optionsStr.match(/['"]?method['"]?\s*:\s*['"](\w+)['"]/i)
23118
return methodMatch && methodMatch[1] ? methodMatch[1].toUpperCase() : 'GET'
24119
}
25120

26121
/**
27122
* 从fetch options对象中提取headers
123+
* 正确处理包含转义引号的header值
28124
*/
29125
function extractHeaders(optionsStr: string): Record<string, string> {
30126
const headers: Record<string, string> = {}
31127

32-
const headersMatch = optionsStr.match(/['"]?headers['"]?\s*:\s*(\{[^}]+\})/i)
33-
if (!headersMatch || !headersMatch[1]) {
128+
// 找到 headers: { 的位置
129+
const headersStartMatch = optionsStr.match(/['"]?headers['"]?\s*:\s*\{/)
130+
if (!headersStartMatch || headersStartMatch.index === undefined) {
131+
return headers
132+
}
133+
134+
const braceStart = headersStartMatch.index + headersStartMatch[0].length - 1
135+
const braceEnd = findMatchingBrace(optionsStr, braceStart)
136+
137+
if (braceEnd === -1) {
34138
return headers
35139
}
36140

37-
const headersStr = headersMatch[1]
141+
// 提取headers对象内容
142+
const headersContent = optionsStr.substring(braceStart + 1, braceEnd)
38143

39-
// 解析 "key": "value" 格式
40-
const headerPairs = headersStr.match(/['"]([^'"]+)['"]\s*:\s*['"]([^'"]+)['"]/g)
41-
if (headerPairs) {
42-
for (const pair of headerPairs) {
43-
const pairMatch = pair.match(/['"]([^'"]+)['"]\s*:\s*['"]([^'"]+)['"]/)
44-
if (pairMatch && pairMatch[1] && pairMatch[2]) {
45-
headers[pairMatch[1]] = pairMatch[2]
144+
// 解析每个 key: value 对
145+
let i = 0
146+
while (i < headersContent.length) {
147+
// 跳过空白和逗号
148+
let currentChar = headersContent.charAt(i)
149+
while (i < headersContent.length && /[\s,]/.test(currentChar)) {
150+
i++
151+
currentChar = headersContent.charAt(i)
152+
}
153+
154+
if (i >= headersContent.length) break
155+
156+
// 查找key
157+
const keyResult = matchJsString(headersContent, i)
158+
if (!keyResult) {
159+
// 尝试匹配不带引号的key
160+
const unquotedKeyMatch = headersContent.substring(i).match(/^(\w+)\s*:/)
161+
if (unquotedKeyMatch && unquotedKeyMatch[1]) {
162+
const unquotedKey = unquotedKeyMatch[1]
163+
i += unquotedKeyMatch[0].length
164+
// 跳过空白
165+
currentChar = headersContent.charAt(i)
166+
while (i < headersContent.length && /\s/.test(currentChar)) {
167+
i++
168+
currentChar = headersContent.charAt(i)
169+
}
170+
const valueResult = matchJsString(headersContent, i)
171+
if (valueResult) {
172+
headers[unquotedKey] = valueResult.value
173+
i = valueResult.endIndex + 1
174+
}
175+
} else {
176+
i++
46177
}
178+
continue
179+
}
180+
181+
const key = keyResult.value
182+
i = keyResult.endIndex + 1
183+
184+
// 跳过 : 和空白
185+
currentChar = headersContent.charAt(i)
186+
while (i < headersContent.length && /[\s:]/.test(currentChar)) {
187+
i++
188+
currentChar = headersContent.charAt(i)
189+
}
190+
191+
// 查找value
192+
const valueResult = matchJsString(headersContent, i)
193+
if (valueResult) {
194+
headers[key] = valueResult.value
195+
i = valueResult.endIndex + 1
196+
} else {
197+
i++
47198
}
48199
}
49200

@@ -52,21 +203,44 @@ function extractHeaders(optionsStr: string): Record<string, string> {
52203

53204
/**
54205
* 从fetch options对象中提取body
206+
* 正确处理包含转义引号的body
55207
*/
56208
function extractBody(optionsStr: string): string {
57-
// 尝试匹配直接字符串body
58-
const stringBodyMatch = optionsStr.match(/['"]?body['"]?\s*:\s*['"]([^'"]+)['"]/i)
59-
if (stringBodyMatch && stringBodyMatch[1]) {
60-
return stringBodyMatch[1]
209+
// 找到 body: 的位置
210+
const bodyMatch = optionsStr.match(/['"]?body['"]?\s*:\s*/)
211+
if (!bodyMatch || bodyMatch.index === undefined) {
212+
return ''
61213
}
62214

63-
// 尝试匹配JSON.stringify格式
64-
const jsonBodyMatch = optionsStr.match(/['"]?body['"]?\s*:\s*JSON\.stringify\s*\(([^)]+)\)/i)
65-
if (jsonBodyMatch && jsonBodyMatch[1]) {
66-
let body = jsonBodyMatch[1]
67-
// 简单清理:将单引号转换为双引号
68-
body = body.replace(/'/g, '"')
69-
return body
215+
const valueStart = bodyMatch.index + bodyMatch[0].length
216+
217+
// 检查是否是 JSON.stringify
218+
const jsonStringifyMatch = optionsStr.substring(valueStart).match(/^JSON\.stringify\s*\(/)
219+
if (jsonStringifyMatch) {
220+
// 找到匹配的括号
221+
const parenStart = valueStart + jsonStringifyMatch[0].length - 1
222+
let depth = 1
223+
let i = parenStart + 1
224+
225+
while (i < optionsStr.length && depth > 0) {
226+
const char = optionsStr[i]
227+
if (char === '(') depth++
228+
else if (char === ')') depth--
229+
i++
230+
}
231+
232+
if (depth === 0) {
233+
let content = optionsStr.substring(parenStart + 1, i - 1).trim()
234+
// 将单引号转换为双引号
235+
content = content.replace(/'/g, '"')
236+
return content
237+
}
238+
}
239+
240+
// 尝试匹配字符串值
241+
const valueResult = matchJsString(optionsStr, valueStart)
242+
if (valueResult) {
243+
return valueResult.value
70244
}
71245

72246
return ''
@@ -76,23 +250,44 @@ function extractBody(optionsStr: string): string {
76250
* 从fetch调用中提取options对象
77251
*/
78252
function extractOptions(input: string): string | null {
79-
const optionsMatch = input.match(/fetch\s*\([^,]+,\s*(\{[\s\S]*?\})\s*\)/)
80-
return optionsMatch && optionsMatch[1] ? optionsMatch[1] : null
253+
// 找到第一个参数后的逗号位置
254+
const fetchMatch = input.match(/fetch\s*\(\s*(['"])((?:(?!\1)[^\\]|\\.)*)\1\s*,\s*/)
255+
if (!fetchMatch || fetchMatch.index === undefined) {
256+
return null
257+
}
258+
259+
const optionsStart = fetchMatch.index + fetchMatch[0].length
260+
261+
// 找到 { 的位置
262+
const bracePos = input.indexOf('{', optionsStart)
263+
if (bracePos === -1) {
264+
return null
265+
}
266+
267+
const braceEnd = findMatchingBrace(input, bracePos)
268+
if (braceEnd === -1) {
269+
return null
270+
}
271+
272+
return input.substring(bracePos, braceEnd + 1)
81273
}
82274

83275
/**
84276
* 解析 fetch (JavaScript) 格式
85277
*
86-
* 支持浏览器fetch API格式
278+
* 支持浏览器fetch API格式,正确处理转义引号
87279
*
88280
* @param input - fetch调用代码字符串
89281
* @returns 解析后的HTTP请求,失败返回null
90282
*
91283
* @example
92-
* parseFetch(`fetch('https://api.example.com/users', {
93-
* method: 'POST',
94-
* headers: { 'Content-Type': 'application/json' },
95-
* body: JSON.stringify({ name: 'test' })
284+
* parseFetch(`fetch("https://api.example.com/users", {
285+
* "headers": {
286+
* "content-type": "application/json",
287+
* "sec-ch-ua": "\"Chromium\";v=\"138\""
288+
* },
289+
* "body": "{\"name\":\"test\"}",
290+
* "method": "POST"
96291
* })`)
97292
*/
98293
export function parseFetch(input: string): ParsedHttpRequest | null {

0 commit comments

Comments
 (0)