首页 > 解决方案 > 使用内置 http 模块使用带有 node.js 的 multipart/form-data 上传图像

问题描述

因此,我需要将两个图像和一个 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))

标签: javascriptnode.jshttpimage-uploadingmultipartform-data

解决方案


好吧,经过大量的尝试和实验,我弄清楚了为什么上面的代码不起作用。首先,postdata 字符串中的内容类型应该设置为 image/ 但这只是一个小问题,这并不是它不起作用的真正原因。

EPIPE 或网络错误是由于我将 Content-Length 标头设置为错误的长度。与其简单地将其设置为字符串的长度,不如将其设置为字符串的 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);
} 

另外,确保为内容长度标头计算添加一些功能,考虑到正文现在存储在数组中,这样的事情应该可以完成:

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

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

希望这可以帮助任何尝试从头开始构建表单数据发布请求的人,而不是使用第三方库request,这样也可以为您解决这个问题。


推荐阅读