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 响应的主要内容,如果未能解决你的问题,请参考以下文章

JavaScript fetch()无法捕获404的错误

如何使用 fetch 将 FormData 从 javascript 发送到 ASP.NET Core 2.1 Web API

Javascript fetch api:无法检索某些响应标头

JavaScript 中的 Promises/Fetch:如何从文本文件中提取文本

如何从 fetch javascript 请求的响应中获取数据

javascript 从服务器请求数据(fetch,XHR和jQuery Ajax)