通过POST请求上传文件时,NodeJS获取失败(object2不可迭代)。

17

我正在尝试使用NodeJS中的原生fetch上传文件(在17.5版本中添加,参见https://nodejs.org/ko/blog/release/v17.5.0/)。

但是,我一直收到以下错误 -

TypeError: fetch failed
at Object.processResponse (node:internal/deps/undici/undici:5536:34)
at node:internal/deps/undici/undici:5858:42
at node:internal/process/task_queues:140:7
at AsyncResource.runInAsyncScope (node:async_hooks:202:9)
at AsyncResource.runMicrotask (node:internal/process/task_queues:137:8)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
cause: TypeError: object2 is not iterable
at action (node:internal/deps/undici/undici:1660:39)
at action.next (<anonymous>)
at Object.pull (node:internal/deps/undici/undici:1708:52)
at ensureIsPromise (node:internal/webstreams/util:172:19)
at readableStreamDefaultControllerCallPullIfNeeded
node:internal/webstreams/readablestream:1884:5)
at node:internal/webstreams/readablestream:1974:7
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)

      

我正在使用以下代码来创建并提交表单响应 -

function upload(hub_entity_id, document_path) {
  let formData = new FormData();
  formData.append("type", "Document");
  formData.append("name", "ap_test_document.pdf");
  formData.append("file", fs.createReadStream("ap_test_document.pdf"));
  formData.append("entity_object_id", hub_entity_id);

  const form_headers = {
    Authorization: auth_code,
    ...formData.getHeaders(),
  };

  console.log(
    `Uploading document ap_test_document.pdf to hub (${hub_entity_id}) `
  );
  console.log(formData);

  let raw_response = await fetch(urls.attachments, {
    method: "POST",
    headers: form_headers,
    body: formData,
  });

  console.log(raw_response);
}

看起来可能是fs.createReadStream()出了问题。你为什么要用两个?你检查过文件路径是否正确吗? - Phil
@Phil 我的错,不该把那个留下来。我把它移除了,但仍然遇到错误,所以我更新了原始问题。是的,我检查了文件路径,看起来没问题。 - Harrison Broadbent
你正在使用哪个版本的nodeJS? - Ukor
@Ukor 这是在 Node v18.5 中。 - Harrison Broadbent
1
嘿@HarrisonBroadbent,你能解决这个问题吗?我也遇到了同样的错误,不确定该如何修复它。 - Delali
显示剩余4条评论
5个回答

10

form-data包的问题:

formData结构不可被Node.js解析,因此会抛出错误:object2 is not iterable

另一方面,令人遗憾的是,formData将不再得到维护, 你可能已经注意到自最后一个版本发布以来已经过去了两年。因此,他们正式宣布formData将被归档: formData的最后一击

这是否是废弃的时间?form-data已经有一段时间没有更新了,它仍然缺少一些应根据规范提供的方法。node-fetch@3建议不要使用form-data,因为它与符合规范的FormData存在不一致性,并建议人们使用内置的FormData或支持迭代所有字段并具有Blob/File支持的规范化formdata polyfill。


解决方案

1. 使用form-data包:

简单来说,我们需要将form-data流转换为node.js流。可以通过以下几种方法中的一种使用某些流方法来实现:

stream.Transform:

使用Node.js stream的stream.Transform类并传递form-data实例,我们可以使用内置的fetch API发送请求。

来自Node.js文档:

Transform流是Duplex流,其中输出与输入在某种程度上相关。像所有Duplex流一样,Transform流实现了可读和可写接口。

因此,我们可以这样实现:

import { Transform } from 'stream';

// rest of code

const tr = new Transform({
  transform(chunk, encoding, callback) {
    callback(null, chunk);
  },
});
formData.pipe(tr);

const request = new Request(url, {
  method: 'POST',
  headers: form_headers,
  duplex: 'half',
  body: tr
})

let raw_response = await fetch(request);

stream.PassThrough:

使用stream.PassThrough,而不是返回流的每个数据块:

import { PassThrough } from 'stream';

// rest of code

const pt = new PassThrough()
formData.pipe(pt);

const request = new Request(url, {
  method: 'POST',
  headers: form_headers,
  duplex: 'half',
  body: pt
})

let raw_response = await fetch(request);

重要提示:如果您没有通过duplex: 'half',则会出现以下错误:

duplex option is required when sending a body

2. 使用内置的form-data

目前,处理fetch的Node.js核心部分被命名为undici

幸运的是,在处理任何表单数据时,您无需使用任何第三方模块,因为undici已将其实现,并且现在是Node.js核心的一部分。

不幸的是,使用部分undici流式传输并不容易和直截了当。不过,您仍然可以实现它。

import { Readable } from 'stream';
// For now, it is essential for encoding the header part, or you can skip importing this module and instead implement it by yourself. 
import { FormDataEncoder } from 'form-data-encoder';

// This is a built-in FormData class, as long as you're using Node.js version 18.x and above, 
// no need to import any third-party form-data packages from NPM.
const formData = new FormData();
formData.set('file', {
  name: 'ap_test_document.pdf',
  [Symbol.toStringTag]: 'File',
  stream: () => fs.createReadStream(filePath) 
});

const encoder = new FormDataEncoder(formData)

const request = new Request(url, {
  method: 'POST',
  headers: {
    'content-type': encoder.contentType,
    Authorization: auth_code
  },
  duplex: 'half',
  body: Readable.from(encoder)
})
let raw_response = await fetch(request);

P.S:关于编码部分,您可能需要阅读这个问题


1
即使您使用 fetch({url, {method: "POST", body: formData}),也可以正常工作,无需使用 FormDataEncoder - Heiko Theißen
1
关键在于 [Symbol.toStringTag]: 'File',这使得 FormData 接受具有 stream 方法的对象作为 Blob/File,请参见此处 - Heiko Theißen
你能在上传的同时压缩文件吗? - Heiko Theißen
此外,您的建议压缩了文件,但我必须压缩整个“multipart/form-data”负载。 - Heiko Theißen
那将是一件不同的事情,压缩整个请求可能由gzip头字段处理。至于大小,在这种情况下是可选的,content-lengthform-data-encoder计算。您可能需要阅读form-data-encoder如何在流模式下计算大小。 - Mostafa Fakhraei
显示剩余3条评论

2
使用方式
new Request(url, {
  method: 'POST',
  headers: ...,
  duplex: 'half',
  body: ...
})

在Mostafa Fakhraei的回答中,有必要避免由于formData.set语句中缺少size属性而导致的错误。请参阅this GitHub issue,其中包括将此方法视为对FormData的误用的评论。

为什么这会是对FormData的误用?

我认为这是因为以这种方式在HTTP请求中包含文件流会导致分块请求,而许多服务器不支持这种方式,如here所解释的那样。Transfer-Encoding: chunked通常只支持响应,而不支持请求。

合理使用方式

fetch(new Request(url, {
  method: 'POST',
  body: formData
}));

这个程序逐块读取和发送文件(而不是将整个文件保存在内存中),并且仍然生成非分块请求,如下所示:

var formData = new FormData();
var stream = fs.createReadStream(filepath);
stream.on("data", function(chunk) {
  console.log("<", chunk.length);
});
formData.append("file", {
  [Symbol.toStringTag]: "File",
  size: fs.statSync(filepath).size, // size is necessary
  stream: () => stream
});
var r = new Request("http://localhost:8080", {
  method: "POST",
  body: formData
});
http.createServer(function(req, res) {
  console.log("Transfer-Encoding", req.headers["transfer-encoding"]);
  req.on("data", function(chunk) {
    console.log(">", chunk.length);
  });
})
.listen(8080, function() {
  fetch(r);
});

这会记录一些类似的东西
< 65536
< 65536
< 65536
Transfer-Encoding undefined
> 65254 multipart header Content-Disposition: form-data; name="file"
> 65536
> 65536
> 411
< 65536
< 65536
> 65536
> 65536
... many more such "two-in/two-out quartets"
< 9633 the last chunk
> 9633
> 44 the last line of the multipart body

但这只适用于 formData 具有 size 属性的情况,否则会出现"无效的内容长度标头"错误。

如果将 fetch(r) 替换为

fetch(r.url, r);

此变体不需要 size,但总是在没有 Content-Length 头部的情况下,生成一个带有 Transfer-Encoding: chunked 请求,即使未提供 size,即使给定的大小很小:最终的多部分线。
Note: The translation assumes Simplified Chinese.
------formdata-undici-0.61749873xxx56806--

然后总是一个Transfer-Encoding的独立块。


谢谢,这为我节省了很多时间。即使在传递了“--http-raw-body --http-chunked-input”标志之后,“uwsgi”服务器似乎仍不支持分块请求 :( - Orkhan Alikhanov

0

我也曾在 React 应用程序中遇到过这种类型的表单数据问题。使用单引号而不是双引号解决了我的问题。

尝试这个:

formData.append('type',"Document");

formData.append('name',"ap_test_document.pdf");

formData.append('file',fs.createReadStream("ap_test_document.pdf"));

formData.append('entity_object_id',hub_entity_id);

如果这解决了你的问题,请让我知道。


formDatapackage的实例还是内置的FormData的实例? - Heiko Theißen

0

非常感谢 Mostafa Fakhraei 的示例,但对于任何需要完整示例的人,特别是在使用 formidable 和 Next.js 等工具时,请查看以下代码。我花了几个小时甚至在得到上面的帮助后仍然无法解决问题,所以想发布完整的代码。

请注意,对于我来说,fetch 上的双工属性会给我的 IDE 带来类型错误。如果您在使用 Node 18+,则不必担心这个问题,只需添加 // @ts-ignore

import FormData from 'form-data';
import formidable from 'formidable';
import fs from 'fs';
import type { NextApiRequest, NextApiResponse } from 'next';
import { Transform } from 'stream';


const avatarUpload = async (req: NextApiRequest, res: NextApiResponse) => {
    try {
        const parseFile = () =>
            new Promise<formidable.Files>((resolve, reject) => {
                const form = new formidable.IncomingForm();
                form.parse(req, async (err, _fields, files) => {
                    if (err) {
                        reject(err);
                    }
                    resolve(files);
                });
            });

        const files = await parseFile();
        const formidableFile = Array.isArray(files.file) ? files.file[0] : files.file;
        const file = fs.createReadStream(formidableFile.filepath);

        const formData = new FormData();
        formData.append('file', file);


        const tr = new Transform({
            transform(chunk, _encoding, callback) {
                callback(null, chunk);
            },
        });
        formData.pipe(tr);

        // this is for cloudflare images api. you can use any other image upload api
        const imageReq = await fetch('https://api.cloudflare.com/client/v4/accounts/23a1b30d95b2ebe5e7f5fce83318994c/images/v1', {
            headers: {
                Authorization: `Bearer ${process.env.CLOUDFLARE_IMAGE_API_KEY}`,
                ...formData.getHeaders(),
            },
            method: 'POST',
            body: tr as any,
            duplex: 'half',
        });
        // do what you want with the imageObject here

        const imageObject = (await imageReq.json()).result;


        return res.status(200).json({ imageObject });
    } catch (err) {
        console.error(err);
        return res.status(500).json({ error: 'Something went wrong' });
    }
};

export default function handler(req: NextApiRequest, res: NextApiResponse) {
    if (req.method === 'POST') {
        avatarUpload(req, res);
    } else {
        // Handle any other HTTP method
    }
}

export const config = {
    api: {
        bodyParser: false,
    },
};


-2

使用fs.readFileSync代替createReadStream。

formData.append("file", fs.readFileSync("ap_test_document.pdf"));

同时您可以直接在formData.append中使用文件名。

formData.append("file", fs.readFileSync("ap_test_document.pdf"), "ap_test_document.pdf");


4
这是错误的建议。使用读取流(read stream)和使用 readFile/readFileSync 将整个文件加载到内存中并不是等价的操作。 - Dan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接