携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情
这个问题在我做的app里发现,上传手机文件到服务器时候,需要携带参数,这时候服务器能接收到,但是返回错误代码,具体原因还是因为上传的参数携带了content-length,导致服务器无法识别,虽然大部分都可以,但是个别服务器不会忽略这个字段,导致本来的参数被拼接上了content-length,使得服务器无法识别参数,上传就失败了。
出现的问题情况:
--4978cfbb-4d8d-438a-9500-724d88778909
Content-Disposition: form-data; name="key"
content-length 5
value
--4978cfbb-4d8d-438a-9500-724d88778909
Content-Disposition: form-data; name="file"; filename="图片.png"
Content-Type: application/octet-stream
.....
正常下服务器是可以忽略掉content-length,但问题就在这里,服务器无法忽略content-length,导致参数"value"被服务器认为是"content-length5 value"
而我们想要的传递参数是这样的
--4978cfbb-4d8d-438a-9500-724d88778909
Content-Disposition: form-data; name="key"
value
--4978cfbb-4d8d-438a-9500-724d88778909
Content-Disposition: form-data; name="file"; filename="图片.png"
Content-Type: application/octet-stream
.....
当你使用任何以okhttp为基础封装的第三方请求框架,都会出现这个问题,原因就是okhttp自身在拼接时候将content-length拼接到了参数中,要做的就是去掉这个content-length。 (并不是header里的content-length)
下面是如何纠正这个错误的办法:
- 将okhttp里的MultipartBody类复制出来,注释掉此处代码
- 然后使用自定义的MultipartBody类传递参数即可
MyMultipartBody.Builder multiBuilder = new MyMultipartBody.Builder();
//注意header里的content-type一定要设置
multiBuilder.setType(MultipartBody.FORM);
//添加要传递的参数
multiBuilder.addFormDataPart("k", "v");
...更多参数
//添加要传递的file
RequestBody requestBody = RequestBody.create(MediaType.parse("application/octet-stream"), file);
multiBuilder.addFormDataPart("file", file.getName(), requestBody);
//最后以okgo里的upload方法举例,如何添加自定义的MultipartBody。(很多第三方框架都可以添加自定义的MultipartBody,具体参考框架使用文档)
RequestBody multiBody = multiBuilder.build();
PostRequest<String> postRequest = OkGo.<String>post(url)
.upRequestBody(multiBody)
.converter(new StringConvert());
这样就可以了,下面附上修改过的MultipartBody类
public final class MyMultipartBody extends RequestBody {
/**
* The "mixed" subtype of "multipart" is intended for use when the body parts are independent and
* need to be bundled in a particular order. Any "multipart" subtypes that an implementation does
* not recognize must be treated as being of subtype "mixed".
*/
public static final MediaType MIXED = MediaType.get("multipart/mixed");
/**
* The "multipart/alternative" type is syntactically identical to "multipart/mixed", but the
* semantics are different. In particular, each of the body parts is an "alternative" version of
* the same information.
*/
public static final MediaType ALTERNATIVE = MediaType.get("multipart/alternative");
/**
* This type is syntactically identical to "multipart/mixed", but the semantics are different. In
* particular, in a digest, the default {@code Content-Type} value for a body part is changed from
* "text/plain" to "message/rfc822".
*/
public static final MediaType DIGEST = MediaType.get("multipart/digest");
/**
* This type is syntactically identical to "multipart/mixed", but the semantics are different. In
* particular, in a parallel entity, the order of body parts is not significant.
*/
public static final MediaType PARALLEL = MediaType.get("multipart/parallel");
/**
* The media-type multipart/form-data follows the rules of all multipart MIME data streams as
* outlined in RFC 2046. In forms, there are a series of fields to be supplied by the user who
* fills out the form. Each field has a name. Within a given form, the names are unique.
*/
public static final MediaType FORM = MediaType.get("multipart/form-data");
private static final byte[] COLONSPACE = {':', ' '};
private static final byte[] CRLF = {'\r', '\n'};
private static final byte[] DASHDASH = {'-', '-'};
private final ByteString boundary;
private final MediaType originalType;
private final MediaType contentType;
private final List<Part> parts;
private long contentLength = -1L;
MyMultipartBody(ByteString boundary, MediaType type, List<Part> parts) {
this.boundary = boundary;
this.originalType = type;
this.contentType = MediaType.get(type + "; boundary=" + boundary.utf8());
this.parts = Util.immutableList(parts);
}
public MediaType type() {
return originalType;
}
public String boundary() {
return boundary.utf8();
}
/**
* The number of parts in this multipart body.
*/
public int size() {
return parts.size();
}
public List<Part> parts() {
return parts;
}
public Part part(int index) {
return parts.get(index);
}
/**
* A combination of {@link #type()} and {@link #boundary()}.
*/
@Override
public MediaType contentType() {
return contentType;
}
@Override
public long contentLength() throws IOException {
long result = contentLength;
if (result != -1L) return result;
return contentLength = writeOrCountBytes(null, true);
}
@Override
public void writeTo(BufferedSink sink) throws IOException {
writeOrCountBytes(sink, false);
}
/**
* Either writes this request to {@code sink} or measures its content length. We have one method
* do double-duty to make sure the counting and content are consistent, particularly when it comes
* to awkward operations like measuring the encoded length of header strings, or the
* length-in-digits of an encoded integer.
*/
private long writeOrCountBytes(
@Nullable BufferedSink sink, boolean countBytes) throws IOException {
long byteCount = 0L;
Buffer byteCountBuffer = null;
if (countBytes) {
sink = byteCountBuffer = new Buffer();
}
for (int p = 0, partCount = parts.size(); p < partCount; p++) {
Part part = parts.get(p);
Headers headers = part.headers;
RequestBody body = part.body;
sink.write(DASHDASH);
sink.write(boundary);
sink.write(CRLF);
if (headers != null) {
for (int h = 0, headerCount = headers.size(); h < headerCount; h++) {
sink.writeUtf8(headers.name(h))
.write(COLONSPACE)
.writeUtf8(headers.value(h))
.write(CRLF);
}
}
MediaType contentType = body.contentType();
if (contentType != null) {
sink.writeUtf8("Content-Type: ")
.writeUtf8(contentType.toString())
.write(CRLF);
}
if (countBytes) {
// We can't measure the body's size without the sizes of its components.
byteCountBuffer.clear();
return -1L;
}
sink.write(CRLF);
if (countBytes) {
byteCount += contentLength;
} else {
body.writeTo(sink);
}
sink.write(CRLF);
}
sink.write(DASHDASH);
sink.write(boundary);
sink.write(DASHDASH);
sink.write(CRLF);
if (countBytes) {
byteCount += byteCountBuffer.size();
byteCountBuffer.clear();
}
return byteCount;
}
/**
* Appends a quoted-string to a StringBuilder.
*
* <p>RFC 2388 is rather vague about how one should escape special characters in form-data
* parameters, and as it turns out Firefox and Chrome actually do rather different things, and
* both say in their comments that they're not really sure what the right approach is. We go with
* Chrome's behavior (which also experimentally seems to match what IE does), but if you actually
* want to have a good chance of things working, please avoid double-quotes, newlines, percent
* signs, and the like in your field names.
*/
static StringBuilder appendQuotedString(StringBuilder target, String key) {
target.append('"');
for (int i = 0, len = key.length(); i < len; i++) {
char ch = key.charAt(i);
switch (ch) {
case '\n':
target.append("%0A");
break;
case '\r':
target.append("%0D");
break;
case '"':
target.append("%22");
break;
default:
target.append(ch);
break;
}
}
target.append('"');
return target;
}
public static final class Part {
public static Part create(RequestBody body) {
return create(null, body);
}
public static Part create(@Nullable Headers headers, RequestBody body) {
if (body == null) {
throw new NullPointerException("body == null");
}
if (headers != null && headers.get("Content-Type") != null) {
throw new IllegalArgumentException("Unexpected header: Content-Type");
}
if (headers != null && headers.get("Content-Length") != null) {
throw new IllegalArgumentException("Unexpected header: Content-Length");
}
return new Part(headers, body);
}
public static Part createFormData(String name, String value) {
return createFormData(name, null, RequestBody.create(null, value));
}
public static Part createFormData(String name, @Nullable String filename, RequestBody body) {
if (name == null) {
throw new NullPointerException("name == null");
}
StringBuilder disposition = new StringBuilder("form-data; name=");
appendQuotedString(disposition, name);
if (filename != null) {
disposition.append("; filename=");
appendQuotedString(disposition, filename);
}
Headers headers = new Headers.Builder()
.addUnsafeNonAscii("Content-Disposition", disposition.toString())
.build();
return create(headers, body);
}
final @Nullable
Headers headers;
final RequestBody body;
private Part(@Nullable Headers headers, RequestBody body) {
this.headers = headers;
this.body = body;
}
public @Nullable
Headers headers() {
return headers;
}
public RequestBody body() {
return body;
}
}
public static final class Builder {
private final ByteString boundary;
private MediaType type = MIXED;
private final List<Part> parts = new ArrayList<>();
public Builder() {
this(UUID.randomUUID().toString());
}
public Builder(String boundary) {
this.boundary = ByteString.encodeUtf8(boundary);
}
/**
* Set the MIME type. Expected values for {@code type} are {@link #MIXED} (the default), {@link
* #ALTERNATIVE}, {@link #DIGEST}, {@link #PARALLEL} and {@link #FORM}.
*/
public Builder setType(MediaType type) {
if (type == null) {
throw new NullPointerException("type == null");
}
if (!type.type().equals("multipart")) {
throw new IllegalArgumentException("multipart != " + type);
}
this.type = type;
return this;
}
/**
* Add a part to the body.
*/
public Builder addPart(RequestBody body) {
return addPart(Part.create(body));
}
/**
* Add a part to the body.
*/
public Builder addPart(@Nullable Headers headers, RequestBody body) {
return addPart(Part.create(headers, body));
}
/**
* Add a form data part to the body.
*/
public Builder addFormDataPart(String name, String value) {
return addPart(Part.createFormData(name, value));
}
/**
* Add a form data part to the body.
*/
public Builder addFormDataPart(String name, @Nullable String filename, RequestBody body) {
return addPart(Part.createFormData(name, filename, body));
}
/**
* Add a part to the body.
*/
public Builder addPart(Part part) {
if (part == null) throw new NullPointerException("part == null");
parts.add(part);
return this;
}
/**
* Assemble the specified parts into a request body.
*/
public MyMultipartBody build() {
if (parts.isEmpty()) {
throw new IllegalStateException("Multipart body must have at least one part.");
}
return new MyMultipartBody(boundary, type, parts);
}
}
}