使用内置 http 模块使用 multipart/form-data 和 node.js 上传图像

Uploading Images using multipart/form-data with node.js using the in-build http module

所以我需要将两个图像和一个 api 密钥作为 multipart/form-data http 请求的一部分发送到 api。我从 aws s3 存储桶接收图像并且工作正常,但每当我尝试将图像作为表单数据的一部分发送时,我都会收到 EPIPE http 错误。不知何故,在 api 接收到所有数据之前,请求被取消了。我使用邮递员尝试了同样的方法,一切正常,只是我的节点程序似乎无法实现这一点。请在下面找到代码片段:

const http = require('http')
const https = require('https')
const AWS = require('aws-sdk')
const s3 = new AWS.S3({apiVersion: '2006-03-01'});

//simple http post request, there doesn't seem to be anything wrong with it 
const httpPromise = (protocol, params, postData) => {
return new Promise((resolve, reject) => {
    const requestModule = protocol === 'http' ? http : https;
    const req = requestModule.request(params, res => {
        // grab request status
        const statusCode = res.statusCode;
        if(statusCode < 200 || statusCode > 299) {
            throw new Error(`Request Failed with Status Code: ${status}`);
        }

        let body = '';
        // continuosly update data with incoming data
        res.setEncoding('utf8');
        res.on('data', data => body += data);

        // once all data was received
        res.on('end', () => {
            console.log(body)
            resolve(body)
        });
    })

    // write data to a post request
    if(typeof(params.method) === 'string' && params.method === 'POST' && postData) {
        req.write(postData)
    }

    // bind to the error event
    req.on('error', err => reject(err));

    // end the request
    req.end();
})
}

const handler = async (event) => {

// requestOption parameters
const apiKey = '000000';
const protocol = 'http';
const path = '/verify';
// set to the defined port, if the port is not defined set to default for either http or https
const port = Port ? Port : protocol === 'http' ? 80 : 443;
const hostname ='www.example.com';
const method = "POST";
const boundary = '__X_PAW_BOUNDARY__';

// get correct keys for the relevant images
const image1Key = 'image1Key';
const image2Key = 'image2Key';
const imageKeys = [image1, image2];



try {
    // get the images, this works all as intended
    const s3GetObjectPromises = [];
    imageKeys.forEach(key => s3GetObjectPromises.push(
        s3.getObject({Bucket: BucketName, Key: key})
        .promise()
        .then(res => res.Body)
    ))
    const [image1, image2] = await Promise.all(s3GetObjectPromises);
    //========== ALL GOOD TILL HERE ============



    // THIS IS WHERE IT GETS PROBLEMATIC:
    // create the postData formData string
    const postData = "--" + boundary + "\r\nContent-Disposition: form-data; name=\"key\"\r\n\r\n" + apiKey + "\r\n--" + boundary + "Content-Disposition: form-data; name=\"image1\"; filename=\"IMG_7264.JPG\"\r\nContent-Type: image/jpeg \r\n\r\n" + image1 + "\r\n--" + boundary + "\r\nContent-Disposition: form-data; name=\"image1\"; filename=\"IMG_7264.JPG\"\r\nContent-Type: image/jpeg\r\n\r\n" + image2 + "\r\n--" + boundary + "--";

    // the formData headers
    const headers = {
        "Content-Type":`multipart/form-data; charset=utf-8; boundary=${boundary}`,
        "Content-Length": `${postData.length}`,
        "User-Agent": "Paw/3.1.7 (Macintosh; OS X/10.14.0) GCDHTTPRequest"
    }
    // the options object
    const options = {hostname, port, path, method, headers};

    let result = await httpPromise(protocol, options, postData)
    console.log(result)
    return result;
} catch(err) {
    console.log(err)
    //this either throws an EPIPE error or it simply states that no key was available
    throw err;
}

//execute the handler
handler()
.then(res => console.log(res))
.catch(err => console.log(err))

好吧,经过大量的尝试和实验,我弄清楚了为什么上面的代码不起作用。首先,post 数据字符串中的 content-type 应该设置为 image/ ,但它太小并不是它不起作用的真正原因。

EPIPE 或网络错误是因为我将 Content-Length header 设置为错误的长度。不是简单地将其设置为字符串的长度,而是必须将其设置为字符串的 ByteLength。所以只需将 'Content-Length': postData.length.toString() 替换为 'Content-Length': Buffer.byteLength(postData).toString()。这应该可以解决 EPIPE 错误。

但还有一个问题:我实际上是将整个数据转换为一个大数据字符串 (postData) 并在一个 req.write(postData) 操作中发送整个字符串。所以显然这不是应该怎么做(经过多次实验),而是应该发送一个包含单行数据的数组,然后将数组的每一项写入 http 请求。所以基本上:

// instead of this string: 
const postData = "--" + boundary + "\r\nContent-Disposition: form-data; name=\"key\"\r\n\r\n" + apiKey + "\r\n--" + boundary + "Content-Disposition: form-data; name=\"image1\"; filename=\"IMG_7264.JPG\"\r\nContent-Type: image/jpeg \r\n\r\n" + image1 + "\r\n--" + boundary + "\r\nContent-Disposition: form-data; name=\"image1\"; filename=\"IMG_7264.JPG\"\r\nContent-Type: image/jpeg\r\n\r\n" + image2 + "\r\n--" + boundary + "--";

// use this array: 
const postData = [`--${boundary}`, `\r\nContent-Disposition: form-data; name=\"key\"\r\n\r\n`, apiKey, `\r\n--${boundary}\r\n`, `Content-Disposition: form-data; name=\"image1\"; filename=\"IMG_7264.JPG\"\r\n`, `Content-Type: image/jpeg \r\n\r\n`, image1, `\r\n--${boundary}\r\n`, `Content-Disposition: form-data; name=\"image1\"; filename=\"IMG_7264.JPG\"\r\n`, `Content-Type: image/jpeg\r\n\r\n`, image2, `\r\n--${boundary}--`];

然后在实际请求中你必须将这个数组逐项写入http请求:

// instead of simply
req.write(postData)
// do:
for(let data of postData) {
    req.write(data);
} 

另外,确保为 content-length header 计算添加一些功能,考虑到 body 现在存储在数组中,像这样的事情应该做工作:

const postDataLength = postData.reduce((acc, curr) => acc += Buffer.byteLength(curr), 0)

然后只需将 Content-Length header 属性设置为 postDataLength.

希望这可以帮助任何试图从头开始构建 form-data post 请求而不是使用像 request 这样的 third-party 库的人,后者也会为您解决这个问题.