使用内置 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 库的人,后者也会为您解决这个问题.
所以我需要将两个图像和一个 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 库的人,后者也会为您解决这个问题.