应用笔记 / 经验分享 · 2023年3月12日

C# HttpClient上传文件到api IFormFile接口,提示“missing content-type boundary“的解决

.net framework 4.5, HttpClient 4.0.0, 后台.net 6.0, 使用IFormFile接口接收文件,提示上述错误,手工指定boundary出现新错误。简单来说,就是multipartContent.Headers.ContentType不能设置,设置了就出现上述问题,注释掉这行问题解决,想知道具体原因的客观可以继续往下看。

问题描述:
同事在使用HttpClient4.4.1版本上传文件时,接口报错“missing content-type boundary”。找我过来一起分析原因。部分代码如下:

HttpPut uploadFile = new HttpPut(uri);
uploadFile.setHeader(“Authorization”, “”);
uploadFile.setHeader(“Content-Type”, “multipart/form-data”);
uploadFile.setHeader(“Ocp-Apim-Subscription-Key”, key);
uploadFile.setHeader(“Authorization”, bearer);

//HttpMultipartMode.RFC6532参数的设定是为避免文件名为中文时乱码
MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create().setMode(HttpMultipartMode.RFC6532);
multipartEntityBuilder.addBinaryBody(“file”,new FileInputStream(uploadFile), ContentType.APPLICATION_OCTET_STREAM,uploadFile.getName());
HttpEntity httpEntity = multipartEntityBuilder.build();
httpPut.setEntity(httpEntity);

// 设置请求超时
RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(2000).build();
httpPut.setConfig(requestConfig);

// 发送请求
CloseableHttpResponse response = httpClient.execute(httpPut);
问题处理:
经过网上一顿分析查找,网上说把

//uploadFile.setHeader(“Content-Type”, “multipart/form-data”);
这行代码注释掉就可以。我们一试,果然接口成功调用。

问题分析:
想必大家都和我有同样的疑问:为什么注释掉那行代码就可以了?

1、boundary是什么?有什么作用?
官方解析:

当content-type为multipart/form-data类型时,需要用boundary指定分隔符。所以boundary后面跟的随机数,就是分隔符,后端就是通过解析到boundary的值作为分隔符来分隔参数的。

想必大家和我一样看到这段解析时,也是不太理解。既然boundary是来分割参数的,参数是在Entity里。那么我们来看下MultipartFormEntity.writeTo()方法的源码,看看能不能找到答案。

private final AbstractMultipartForm multipart;
@Override
public void writeTo(final OutputStream outstream) throws IOException {
this.multipart.writeTo(outstream);
}
可以看到调用的是AbstractMultipartForm.doWriteTo()

private static final ByteArrayBuffer CR_LF = encode(MIME.DEFAULT_CHARSET, “\r\n”);
private static final ByteArrayBuffer TWO_DASHES = encode(MIME.DEFAULT_CHARSET, “–“);

public void writeTo(final OutputStream out) throws IOException {
doWriteTo(out, true);
}

void doWriteTo(
final OutputStream out,
final boolean writeContent) throws IOException {

final ByteArrayBuffer boundaryEncoded = encode(this.charset, this.boundary);
for (final FormBodyPart part: getBodyParts()) {
writeBytes(TWO_DASHES, out);
writeBytes(boundaryEncoded, out);
writeBytes(CR_LF, out);
//以–boundaryEncoded\r\n为分割符 分割多个文件或键值对
formatMultipartHeader(part, out);

writeBytes(CR_LF, out);

if (writeContent) {
part.getBody().writeTo(out);
}
writeBytes(CR_LF, out);
}
// 以–boundaryEncoded\r\n为分割符 结束
writeBytes(TWO_DASHES, out);
writeBytes(boundaryEncoded, out);
writeBytes(TWO_DASHES, out);
writeBytes(CR_LF, out);
}
从上面源码可知boundary作为分割符,将请求的输出流分割开。

就是当http请求Content-Type为multipart/form-data时,它会将表单的数据处理为一条消息,以标签为单元,用分隔符boundary分开。既可以上传键值对,也可以上传文件。当上传的字段是文件时,会有Content-Type来表名文件类型。

由于有boundary隔离,所以multipart/form-data既可以上传文件,也可以上传键值对,它采用了键值对的方式,所以可以上传多个文件。

 

 

经过上面的分析,我们知道boundary就是在Content-Type为multipart/form-data的情况下,作为一个随机数分隔符,来实现既可以上传多个文件,也可以上传键值对。

2、那么问题来了,boundary是如何产生的呢?
大家可能会想到,我自己生成一个boundary行不行:

uploadFile.setHeader(“Content-Type”, “multipart/form-data;—-ba77f35b192c8918628309c77e6add06”);
遗憾的是不行,又报出新的错误:“the content may have already been read by another component”。

那么最大嫌疑就只能是MultipartEntityBuilder.build()方法,是不是这里生成了boundary?我们来验证下。

public HttpEntity build() {
return buildEntity();
}
可以看到 MultipartEntityBuilder.build()调用的是MultipartEntityBuilder.buildEntity()

MultipartFormEntity buildEntity() {
String boundaryCopy = boundary;
if (boundaryCopy == null && contentType != null) {
boundaryCopy = contentType.getParameter(“boundary”);
}
if (boundaryCopy == null) {
//生成随机数Boundary
boundaryCopy = generateBoundary();
}
Charset charsetCopy = charset;
if (charsetCopy == null && contentType != null) {
charsetCopy = contentType.getCharset();
}
final List<NameValuePair> paramsList = new ArrayList<NameValuePair>(2);
paramsList.add(new BasicNameValuePair(“boundary”, boundaryCopy));
if (charsetCopy != null) {
paramsList.add(new BasicNameValuePair(“charset”, charsetCopy.name()));
}
final NameValuePair[] params = paramsList.toArray(new NameValuePair[paramsList.size()]);

//contentType 为空 默认设置为multipart/form-data private final static String DEFAULT_SUBTYPE = “form-data”;
final ContentType contentTypeCopy = contentType != null ?
contentType.withParameters(params) :
ContentType.create(“multipart/” + DEFAULT_SUBTYPE, params);

final List<FormBodyPart> bodyPartsCopy = bodyParts != null ? new ArrayList<FormBodyPart>(bodyParts) :
Collections.<FormBodyPart>emptyList();
final HttpMultipartMode modeCopy = mode != null ? mode : HttpMultipartMode.STRICT;
final AbstractMultipartForm form;
switch (modeCopy) {
case BROWSER_COMPATIBLE:
form = new HttpBrowserCompatibleMultipart(charsetCopy, boundaryCopy, bodyPartsCopy);
break;
case RFC6532:
form = new HttpRFC6532Multipart(charsetCopy, boundaryCopy, bodyPartsCopy);
break;
default:
form = new HttpStrictMultipart(charsetCopy, boundaryCopy, bodyPartsCopy);
}
return new MultipartFormEntity(form, contentTypeCopy, form.getTotalLength());
}
/**
* The pool of ASCII chars to be used for generating a multipart boundary.
*/
private final static char[] MULTIPART_CHARS =
“-_1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ”
.toCharArray();
//生成一个30到40位长的随机数
private String generateBoundary() {
final StringBuilder buffer = new StringBuilder();
final Random rand = new Random();
final int count = rand.nextInt(11) + 30; // a random size from 30 to 40
for (int i = 0; i < count; i++) {
buffer.append(MULTIPART_CHARS[rand.nextInt(MULTIPART_CHARS.length)]);
}
return buffer.toString();
}
从上面源码可以看到,原来MultipartEntityBuilder默认context-type为 multipart/form-data,并且会自动帮我们生成一个生成一个30到40位长的随机数boundary。

总结
这就解释的通为什么注释掉

//uploadFile.setHeader(“Content-Type”, “multipart/form-data”);
还能调用成功,因为MultipartEntityBuilder帮我们生成了boundary,并用这个boundary分割请求内容,并且默认context-type为 multipart/form-data。

所以当自己设置boundary时:

uploadFile.setHeader(“Content-Type”, “multipart/form-data;—-ba77f35b192c8918628309c77e6add06”);
又和MultipartEntityBuilder生成的boundary不一样,一样报错调用不成功。

扩展
“missing content-type boundary” 错误其实和网上大家经常遇到的错误“the request was rejected because no multipart boundary was found”是一样的,应该是不同框架,报错的提示不一样。

以tomcat为例:boundary为空的处理在FileUploadBase. new FileItemIteratorImpl(),留个引子待后续继续研究。

FileItemIteratorImpl(RequestContext ctx)
throws FileUploadException, IOException {
if (ctx == null) {
throw new NullPointerException(“ctx parameter”);
}

String contentType = ctx.getContentType();
if ((null == contentType)
|| (!contentType.toLowerCase(Locale.ENGLISH).startsWith(MULTIPART))) {
throw new InvalidContentTypeException(String.format(
“the request doesn’t contain a %s or %s stream, content type header is %s”,
MULTIPART_FORM_DATA, MULTIPART_MIXED, contentType));
}

final long requestSize = ((UploadContext) ctx).contentLength();

InputStream input; // N.B. this is eventually closed in MultipartStream processing
if (sizeMax >= 0) {
if (requestSize != -1 && requestSize > sizeMax) {
throw new SizeLimitExceededException(String.format(
“the request was rejected because its size (%s) exceeds the configured maximum (%s)”,
Long.valueOf(requestSize), Long.valueOf(sizeMax)),
requestSize, sizeMax);
}
// N.B. this is eventually closed in MultipartStream processing
input = new LimitedInputStream(ctx.getInputStream(), sizeMax) {
@Override
protected void raiseError(long pSizeMax, long pCount)
throws IOException {
FileUploadException ex = new SizeLimitExceededException(
String.format(“the request was rejected because its size (%s) exceeds the configured maximum (%s)”,
Long.valueOf(pCount), Long.valueOf(pSizeMax)),
pCount, pSizeMax);
throw new FileUploadIOException(ex);
}
};
} else {
input = ctx.getInputStream();
}

String charEncoding = headerEncoding;
if (charEncoding == null) {
charEncoding = ctx.getCharacterEncoding();
}

boundary = getBoundary(contentType);
if (boundary == null) {
IOUtils.closeQuietly(input); // avoid possible resource leak
throw new FileUploadException(“the request was rejected because no multipart boundary was found”);
}

notifier = new MultipartStream.ProgressNotifier(listener, requestSize);
try {
multi = new MultipartStream(input, boundary, notifier);
} catch (IllegalArgumentException iae) {
IOUtils.closeQuietly(input); // avoid possible resource leak
throw new InvalidContentTypeException(
String.format(“The boundary specified in the %s header is too long”, CONTENT_TYPE), iae);
}
multi.setHeaderEncoding(charEncoding);

skipPreamble = true;
findNextItem();
}