Javascript fetch 无法从 GMail API Batch 请求中读取 Multipart 响应
Posted
技术标签:
【中文标题】Javascript fetch 无法从 GMail API Batch 请求中读取 Multipart 响应【英文标题】:Javascript fetch fails to read Multipart response from GMail API Batch request 【发布时间】:2021-05-14 21:28:40 【问题描述】:我需要使用 GMail API 来检索多封电子邮件数据,所以我使用 Batch API。我终于可以制作一个足够好的请求,但问题是 javascript 似乎无法正确解析响应。请注意,这是纯浏览器 Javascript,我没有使用任何服务器。
请参考下面的代码。检查后请求/响应很好,但在我调用 r.formData()
方法的那一行,我收到了这个错误,没有进一步的解释:
TypeError: 获取失败
async getGmailMessageMetadatasAsync(ids: string[], token: string): Promise<IGmailMetaData[]>
if (!ids.length)
return [];
const url = `https://gmail.googleapis.com/batch/gmail/v1`;
const body = new FormData();
for (let id of ids)
const blobContent = `GET /gmail/v1/users/me/messages/$encodeURI(id)?format=METADATA`;
const blob = new Blob([blobContent],
type: "application/http",
);
body.append("dummy", blob);
const r = await fetch(url,
body: body,
method: "POST",
headers: this.getAuthHeader(token),
);
if (!r.ok)
throw r;
try
const content = await r.formData(); // This won't work
debugger;
for (let key of content)
catch (e)
console.error(e);
debugger;
return <any>[];
如果我用r.text()
替换r.formData()
,它可以工作,但是我必须自己解析文本,我认为这不好。响应有正确的content-type: multipart/form-data; boundary=batch_HViQtsA3Z_aYrPoOlukRFgkPEUDoDh23
,正文如下所示:
"
--batch_HViQtsA3Z_aYrPoOlukRFgkPEUDoDh23
Content-Type: application/http
Content-ID: response-
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Vary: Origin
Vary: X-Origin
Vary: Referer
"id": "1778c9cc9345a9f4",
"threadId": "1778c9cc9345a9f4",
"labelIds": [
"IMPORTANT",
"CATEGORY_PERSONAL",
"INBOX"
],
<More content>
如何正确解析此响应并获取每封电子邮件的 JSON 内容?
【问题讨论】:
嗨!我相信你没有正确使用FormData(),请参考文档。此外,我认为formData()
不会得到你想要的东西(JSON 对象)。您是否尝试过使用 JSON.pase(response) 代替?
您好,我确定我使用的是formData()
,而不是您提到的FormData
(尽管结果应该是FormData
类型)。对于 JSON.parse 不幸的是,它不能立即工作,因为我仍然需要从响应文本中解析该 JSON 部分。
请注意,我发布的文本内容是完整的正文文本,而不仅仅是 JSON(因为正文内容是一个多部分,每个请求包含多个 JSON)。
我相信您的主要问题是您正在使用r.formData()
,而实际上r
已经是内容类型所述的表单数据对象。您是否尝试过使用insread r.getAll()
或任何其他方法,例如formData methods 中的r.entries()
?另外,您不想进行字符串搜索吗?这就是为什么像this one 这样的答案不会为您完成这项工作,对吗?
@MateoRandwolf 感谢您的回复,很抱歉回复晚了,我正在做另一个项目。 r
不是 formData
而是 Response
(我也用 Javascript 确认了这一点,而不仅仅是依赖于 TypeScript)。无法在r
上执行for in/of
(它不可迭代,只是一个非常标准的Response
对象)。 r
没有 getAll()
或 entries()
方法。是的,你是对的,我想避免进行字符串搜索(至少我自己)。但如果我找不到其他解决方案,我会这样做。
【参考方案1】:
我无法测试您的代码示例。但是reading the documentation 我在您的代码中发现了一个潜在的错误。实际上,您在请求中使用了 Content-Type
multipart/form-data
,但根据 Google 文档,您应该改用 multipart/mixed
Content-Type
。引用文档:
批处理请求是包含多个 Gmail API 调用的单个标准 HTTP 请求,使用 multipart/mixed 内容类型。在该主 HTTP 请求中,每个部分都包含一个嵌套的 HTTP 请求。
我的猜测是 Google API 很乐意接受您的 Content-Type: multipart/form-data
并在响应中返回相同的 Content-Type
标头即使内容本身是可能不是 表单-data1 在RFC7578 中指定。这可能是Response.formData()
API无法解析内容的原因。
1 我自愿保持谨慎,因为 [再次免责声明] 我没有测试您的代码,因此没有看到响应的完整结果。
【讨论】:
这是一个有趣的观点。我尝试手动制作multipart/mixed
请求(因为我看不到使用FormData
发送它的方法),响应确实有content-type: multipart/mixed; boundary=batch_bCj2rPNTx_nohFC8LwscDqtbtZIpux37
。响应文本是相同的。但是调用r.text()
和r.formData()
与multipart/form-data
响应具有相同的行为:( 猜测它需要解析字符串。我认为v8 的formData()
方法有问题,因为在另一个客户端失眠症中,应用程序可以解析响应分成多个部分。
我不认为 fetch API 有问题,目标是解析 FormData
,在这里你给出了其他东西。实际上,响应是多部分的,但不是multipart/form-data
。所以对我来说,如果你给它其他东西,formData()
失败是有道理的。
哦,对不起,我很笨,我没有注意到multipart/form-data
的正文与multipart/mixed
有很大的不同。是的,我认为您是对的,不知何故,Google 只是复制了请求 Content-Type
并给出了 multipart/mixed
正文。
很高兴它有帮助。 :-)
@Mxngls 将正文添加到我下面的答案中。【参考方案2】:
感谢另一个答案,我意识到响应正文是multipart/mixed
,而不是multipart/form-data
,所以我自己编写了这个解析器。
export class MultipartMixedService
static async parseAsync(r: Response): Promise<MultipartMixedEntry[]>
const text = await r.text();
const contentType = r.headers.get("Content-Type");
return this.parse(text, contentType);
static parse(body: string, contentType: string): MultipartMixedEntry[]
const result: MultipartMixedEntry[] = [];
const contentTypeData = this.parseHeader(contentType);
const boundary = contentTypeData.directives.get("boundary");
if (!boundary)
throw new Error("Invalid Content Type: no boundary");
const boundaryText = "--" + boundary;
let line: string;
let pos = -1;
let currEntry: MultipartMixedEntry = null;
let parsingEntryHeaders = false;
let parsingBodyHeaders = false;
let parsingBodyFirstLine = false;
do
[line, pos] = this.nextLine(body, pos);
if (line.length == 0 || line == "\r") // Empty Line
if (parsingEntryHeaders)
// Start parsing Body Headers
parsingEntryHeaders = false;
parsingBodyHeaders = true;
else if (parsingBodyHeaders)
// Start parsing body
parsingBodyHeaders = false;
parsingBodyFirstLine = true;
else if (currEntry != null)
// Empty line in body, just add it
currEntry.body += (parsingBodyFirstLine ? "" : "\n") + "\n";
parsingBodyFirstLine = false;
// Else, it's just empty starting lines
else if (line.startsWith(boundaryText))
// Remove one extra line from the body
if (currEntry != null)
currEntry.body = currEntry.body.substring(0, currEntry.body.length - 1);
// Check if it is the end
if (line.endsWith("--"))
return result;
// If not, it's the start of new entry
currEntry = new MultipartMixedEntry();
result.push(currEntry);
parsingEntryHeaders = true;
else
if (!currEntry)
// Trash content
throw new Error("Error parsing response: Unexpected data.");
// Add content
if (parsingEntryHeaders || parsingBodyHeaders)
// Headers
const headers = parsingEntryHeaders ? currEntry.entryHeaders : currEntry.bodyHeaders;
const headerParts = line.split(":", 2);
if (headerParts.length == 1)
headers.append("X-Extra", headerParts[0].trim());
else
headers.append(headerParts[0]?.trim(), headerParts[1].trim());
else
// Body
currEntry.body += (parsingBodyFirstLine ? "" : "\n") + line;
parsingBodyFirstLine = false;
while (pos > -1);
return result;
static parseHeader(headerValue: string): HeaderData
if (!headerValue)
throw new Error("Invalid Header Value: " + headerValue);
var result = new HeaderData();
result.fullText = headerValue;
const parts = headerValue.split(/;/g);
result.value = parts[0];
for (var i = 1; i < parts.length; i++)
const part = parts[i].trim();
const partData = part.split("=", 2);
result.directives.append(partData[0], partData[1]);
return result;
private static nextLine(text: string, lastPos: number): [string, number]
const nextLinePos = text.indexOf("\n", lastPos + 1);
let line = text.substring(lastPos + 1, nextLinePos == -1 ? null : nextLinePos);
while (line.endsWith("\r"))
line = line.substr(0, line.length - 1);
return [line, nextLinePos];
export class MultipartMixedEntry
entryHeaders: Headers = new Headers();
bodyHeaders: Headers = new Headers();
body: string = "";
json<T = any>(): T
return JSON.parse(this.body);
export class HeaderData
fullText: string;
value: string;
directives: Headers = new Headers();
用法:
const r = await fetch(url,
body: body,
method: "POST",
headers: headers,
);
if (!r.ok)
throw r;
try
const contentData = await MultipartMixedService.parseAsync(r);
// Other code
有人要求提供请求和响应正文,这是一个示例(我审查了 Bearer 令牌和我的电子邮件):
fetch("https://gmail.googleapis.com/batch/gmail/v1",
"headers":
"accept": "*/*",
"accept-language": "en-US,en;q=0.9,vi;q=0.8,fr;q=0.7",
"authorization": "Bearer <YOUR TOKEN>",
"content-type": "multipart/form-data; boundary=----WebKitFormBoundaryZ9nvH6zUTGoR7aAs",
"sec-ch-ua": "\" Not A;Brand\";v=\"99\", \"Chromium\";v=\"90\", \"Microsoft Edge\";v=\"90\"",
"sec-ch-ua-mobile": "?0",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "cross-site"
,
"referrerPolicy": "strict-origin-when-cross-origin",
"body": "------WebKitFormBoundaryZ9nvH6zUTGoR7aAs\r\nContent-Disposition: form-data; name=\"dummy\"; filename=\"blob\"\r\nContent-Type: application/http\r\n\r\nGET /gmail/v1/users/me/messages/1799c0f9031dc75a?format=METADATA\r\n------WebKitFormBoundaryZ9nvH6zUTGoR7aAs--\r\n",
"method": "POST",
"mode": "cors",
"credentials": "include"
);
--batch_jb1MbufS6_fEEIu5e6taSCLa9ZOYifdP
Content-Type: application/http
Content-ID: response-
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Vary: Origin
Vary: X-Origin
Vary: Referer
"id": "1799c0f9031dc75a",
"threadId": "1799c0f9031dc75a",
"labelIds": [
"UNREAD",
"SENT",
"INBOX"
],
"payload":
"partId": "",
"headers": [
"name": "Return-Path",
"value": "\u003c******@gmail.com\u003e"
,
"name": "Received",
"value": "from LukePC ([****:***:****:****:d906:d8c4:10f6:6146]) by smtp.gmail.com with ESMTPSA id u6sm8286689pjy.51.2021.05.23.18.48.55 for \u003c******@gmail.com\u003e (version=TLS1_2 cipher=ECDHE-ECDSA-AES128-GCM-SHA256 bits=128/128); Sun, 23 May 2021 18:48:56 -0700 (PDT)"
,
"name": "From",
"value": "\u003c******@gmail.com\u003e"
,
"name": "To",
"value": "\u003c******@gmail.com\u003e"
,
"name": "Subject",
"value": "Test Email"
,
"name": "Date",
"value": "Mon, 24 May 2021 08:48:52 +0700"
,
"name": "Message-ID",
"value": "\u003c000201d7503e$f42b1ea0$dc815be0$@gmail.com\u003e"
,
"name": "MIME-Version",
"value": "1.0"
,
"name": "Content-Type",
"value": "multipart/alternative; boundary=\"----=_NextPart_000_0003_01D75079.A089F6A0\""
,
"name": "X-Mailer",
"value": "Microsoft Outlook 16.0"
,
"name": "Thread-Index",
"value": "AddQPvAK354ufYfSQqqfwTDwp7zDCQ=="
,
"name": "Content-Language",
"value": "en-us"
]
,
"sizeEstimate": 2750,
"historyId": "197435",
"internalDate": "1621820932000"
--batch_jb1MbufS6_fEEIu5e6taSCLa9ZOYifdP--
【讨论】:
以上是关于Javascript fetch 无法从 GMail API Batch 请求中读取 Multipart 响应的主要内容,如果未能解决你的问题,请参考以下文章
如何使用 fetch 将 FormData 从 javascript 发送到 ASP.NET Core 2.1 Web API
Javascript fetch api:无法检索某些响应标头
JavaScript 中的 Promises/Fetch:如何从文本文件中提取文本