使用OkHttp上传文件时出现奇怪的数据包

3

我正在尝试通过okhttp将文件上传到服务器,使用表单所以要用MultipartBuilder。

OkHttpClient client = new OkHttpClient();
MediaType OCTET_STREAM = MediaType.parse("application/octet-stream");
RequestBody body = new MultipartBuilder()
            .type(MultipartBuilder.FORM)
            .addPart(Headers.of("Content-Disposition",
                                "form-data; name=\"breadcrumb\"; filename=\"myfile.bin\"",
                                "Content-Transfer-Encoding", "binary"),
                     RequestBody.create(OCTET_STREAM, file))
            .build();

Request request = new Request.Builder()
            .header("Authorization", cred)
            .url(url)
            .post(body)
            .build();

Response response = client.newCall(request).execute();

但是当追踪电线上发生的事情时:

POST /breadcrumb/ HTTP/1.1
Authorization: Basic aHl6OmhvbGExMjM=
Content-Type: multipart/form-data; boundary=9bc835d6-24b8-42c4-ae8d-5bc89b3fe68f
Transfer-Encoding: chunked
Host: myurl:8000
Connection: Keep-Alive
Accept-Encoding: gzip

10e
--9bc835d6-24b8-42c4-ae8d-5bc89b3fe68f
Content-Disposition: form-data; name="breadcrumb"; filename="myfile.bin"
Content-Transfer-Encoding: binary
Content-Type: application/octet-stream
Content-Length: 21

some file content in binary

--9bc835d6-24b8-42c4-ae8d-5bc89b3fe68f--
0

当我使用传统的Apache http构建器时,它看起来很相似,但我没有看到开头和结尾的奇怪字符(10e, 0)。有什么想法吗?
感谢您的帮助。

1
这是分块传输编码在发挥作用 (http://en.wikipedia.org/wiki/Chunked_transfer_encoding)。 - Anton
2个回答

1
你没有指定确切的Content-Length头,因此OkHttpClient开始使用分块传输编码。
在HTTP协议中,接收方在内容实际发送到服务器之前必须始终知道内容的确切长度(以便分配内存或其他资源)。有两种方法可以发送它 - 在Content-Length头中发送整个内容长度,或者如果无法在请求开始时计算出内容长度,则使用分块编码。
这一行:
10e

这句话的意思是在该行之后,客户端将发送一部分长度为0x10e(270)字节的数据。


我正在使用OkHttp 2.0.0,我相信这个版本中已经修复了一个漏洞,其他使用该库的用户是否也看到了同样的问题? - hector
1
这完全不是一个 bug。它必须是这样的。这是 HTTP 1.1 标准中包含的标准特性。它这样做是因为必须这样做。如果客户端没有指定 Content-Length,并且不使用分块传输编码,那么服务器就无法理解需要分配多少资源。 - rufanov

0

当前的MultipartBuilder实现不支持设置固定的Content-Length。一种选择是实现一个FixedMultipartBuilder,它在public long contentLength()方法中完成其工作。它不再返回-1L,而是计算长度,例如:

import com.squareup.okhttp.Headers;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.MultipartBuilder;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.internal.Util;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import okio.BufferedSink;
import okio.ByteString;

/**
 * Fluent API to build <a href="http://www.ietf.org/rfc/rfc2387.txt">RFC
 * 2387</a>-compliant request bodies.
 */
public final class FixedMultipartBuilder {

  private static final byte[] COLONSPACE = { ':', ' ' };
  private static final byte[] CRLF = { '\r', '\n' };
  private static final byte[] DASHDASH = { '-', '-' };

  private final ByteString boundary;
  private MediaType type = MultipartBuilder.MIXED;

  // Parallel lists of nullable headers and non-null bodies.
  private final List<Headers> partHeaders = new ArrayList<>();
  private final List<RequestBody> partBodies = new ArrayList<>();

  /** Creates a new multipart builder that uses a random boundary token. */
  public FixedMultipartBuilder() {
    this(UUID.randomUUID().toString());
  }

  /**
   * Creates a new multipart builder that uses {@code boundary} to separate
   * parts. Prefer the no-argument constructor to defend against injection
   * attacks.
   */
  public FixedMultipartBuilder(String boundary) {
    this.boundary = ByteString.encodeUtf8(boundary);
  }

  /**
   * Set the MIME type. Expected values for {@code type} are
   * {@link com.squareup.okhttp.MultipartBuilder#MIXED} (the default),
   * {@link com.squareup.okhttp.MultipartBuilder#ALTERNATIVE},
   * {@link com.squareup.okhttp.MultipartBuilder#DIGEST},
   * {@link com.squareup.okhttp.MultipartBuilder#PARALLEL} and
   * {@link com.squareup.okhttp.MultipartBuilder#FORM}.
   */
  public FixedMultipartBuilder type(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 FixedMultipartBuilder addPart(RequestBody body) {
    return addPart(null, body);
  }

  /** Add a part to the body. */
  public FixedMultipartBuilder addPart(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");
    }

    partHeaders.add(headers);
    partBodies.add(body);
    return this;
  }

  /**
   * 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.
   */
  private 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;
  }

  /** Add a form data part to the body. */
  public FixedMultipartBuilder addFormDataPart(String name, String value) {
    return addFormDataPart(name, null, RequestBody.create(null, value));
  }

  /** Add a form data part to the body. */
  public FixedMultipartBuilder addFormDataPart(String name, String filename, RequestBody value) {
    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);
    }

    return addPart(Headers.of("Content-Disposition", disposition.toString()), value);
  }

  /** Assemble the specified parts into a request body. */
  public RequestBody build() {
    if (partHeaders.isEmpty()) {
      throw new IllegalStateException("Multipart body must have at least one part.");
    }
    return new MultipartRequestBody(type, boundary, partHeaders, partBodies);
  }

  private static final class MultipartRequestBody extends RequestBody {
    private final ByteString boundary;
    private final MediaType contentType;
    private final List<Headers> partHeaders;
    private final List<RequestBody> partBodies;

    public MultipartRequestBody(MediaType type, ByteString boundary, List<Headers> partHeaders,
        List<RequestBody> partBodies) {
      if (type == null) throw new NullPointerException("type == null");

      this.boundary = boundary;
      this.contentType = MediaType.parse(type + "; boundary=" + boundary.utf8());
      this.partHeaders = Util.immutableList(partHeaders);
      this.partBodies = Util.immutableList(partBodies);
    }

    @Override public MediaType contentType() {
      return contentType;
    }

    private long contentLengthForPart(Headers headers, RequestBody body) throws IOException {
      // Check if the body has an contentLength != -1, otherwise cancel!
      long bodyContentLength = body.contentLength();
      if(bodyContentLength < 0L) {
        return -1L;
      }

      long length = 0;

      length += DASHDASH.length + boundary.size() + CRLF.length;

      if (headers != null) {
        for (int h = 0, headerCount = headers.size(); h < headerCount; h++) {
          length += headers.name(h).getBytes().length
                  + COLONSPACE.length
                  + headers.value(h).getBytes().length
                  + CRLF.length;
        }
      }

      MediaType contentType = body.contentType();
      if (contentType != null) {
        length += "Content-Type: ".getBytes().length
                + contentType.toString().getBytes().length
                + CRLF.length;
      }

      length += CRLF.length;
      length += bodyContentLength;
      length += CRLF.length;

      return length;
    }

    @Override public long contentLength() throws IOException {
      long length = 0;

      for (int p = 0, partCount = partHeaders.size(); p < partCount; p++) {
        long contentPartLength = contentLengthForPart(partHeaders.get(p), partBodies.get(p));

        if(contentPartLength < 0) {
          // Too bad, can't get contentPartLength!
          return -1L;
        }

        length += contentPartLength;
      }

      length += DASHDASH.length + boundary.size() + DASHDASH.length + CRLF.length;

      return length;
    }

    @Override public void writeTo(BufferedSink sink) throws IOException {
      for (int p = 0, partCount = partHeaders.size(); p < partCount; p++) {
        Headers headers = partHeaders.get(p);
        RequestBody body = partBodies.get(p);

        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);
        }

        // Skipping the Content-Length for individual parts

        sink.write(CRLF);
        partBodies.get(p).writeTo(sink);
        sink.write(CRLF);
      }

      sink.write(DASHDASH);
      sink.write(boundary);
      sink.write(DASHDASH);
      sink.write(CRLF);
    }
  }
}

抱歉列表很长...不幸的是,MultipartBuilderfinal的,所以我们必须复制大部分源代码而不是简单地扩展它。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接