一个由Content-Disposition引发的血案

7,327 阅读6分钟

起缘

最近项目组一同事小明在往服务器上传文件/图片时,说传不上去,据后台的同事反馈,没有接收到对应的文件字段,无法判定你是否传的文件。

这个时候小明就不服了,撕逼开始。Android组小明认为这是接口的问题,后端同事告诉小明是他的锅,需要添加标识文件,否则无法处理。小明就不爽了,明明自己试过上传到别人家的服务器上都好用,到你这就不行了,凭什么是我的锅?

虽然很不爽,但是迫于生活,小明还是妥协了,去寻找原因,经过一番交涉,小明发现了那个项目添加了个库,库中上传文件部分的代码居然采用的是volley网络请求框架封装的MultipartRequest,(这个年头谁还用volley呀)很快,小明凭借着自己的丰富经验(装B),定位到了问题的所在,那个封装的MultipartRequest中,并没有添加Content-Disposition,Content-Type等标识,那自然是会存在问题的。很快他就往这个Request请求中添加了如下的代码,重写getHeader(); 心里暗喜,轻松解决。

运行,同样还是无法顺利上传文件。这就让小明很苦恼了,怎么不按套路来呢?我再看看...

关于网络请求

网络请求内容涉及甚多,在这里,只是针对组内遇到的这个问题来讨论对应网络请求的部分,那就是请求报文响应报文。 本文也只针对POST表单的网络请求报文和响应报文做讨论。如果对于文中提及的Content-Type,Content-Position还不是很了解的,可以参考以下文章,也详尽的介绍了Http网络请求其他部分的相关内容。

HTTP

Content-Type

请求报文

简而言之,请求报文的组成:请求行+请求头+请求体

我们通过一段利用OkHttp上传图片的网络请求日志来看看(当然了,这格式跟标准的HTTP网络请求格式不完全一致,大体内容相同),在进行请求时,这个数据日志是怎样的一个格式。

上面的日志分析显示,一个POST表单请求,请求体是被一个名为:boundary定义的内容划分为一部分一部分的,这种情况是在进行文件上传时展现出来的格式。即:每个字段/文件都被你定义的boundary分成单独的一段。并且在请求头的第一行中,制定了Content-Type:multipart/form-data;标明这种请求体的数据提交方式是multipart/form-data,另外还有两种常见请求体形式

1、application/json 在POST JSON数据时,一般都需要自己进行解析处理。

2、application/x-www-form-urlencoded,这也是最常见的提交数据的方式了。多个键值对之间用&连接,只能用ASCII字符,非ASCII字符需要采用URLEncode编码。

  • 请求行

    在请求行中,可以看到,首先是请求方法,一般我们只用GET和POST,其他不常用的还有包括:PUT、DELETE、HEAD、TRACE、CONNECT、PATCH。更具体的可以参考:紧接着是对应的请求URL地址。在OkHttp这个日志的输出中,我们看到请求行就这两个了,其实请求行中在请求的URL地址后,还会有协议名称及版本号:如HTTP/1.1。这里面可能格式化输出没有加上这个。

  • 请求头

    这部分是HTTP的报文头,包含了一些属性,格式都是key:value,服务端是根据这些key-value来获取客户端的配置信息。

  • 请求体

    是报文体,如果一个请求中包含有表单数据, 这部分,也是我们上传文件请求时的重要处理部分了。上面小明同学重写了getHeader,往Header里面添加了Content-Disposition以及Content-Type其实只是添加到了请求头中,我们后台有对这部分做了数据验证,故无法正确获取上传的文件。

关于Content-Disposition

简单介绍:Content-Disposition是 MIME 协议的扩展,MIME 协议指示 MIME 用户代理如何显示附加的文件。

  • (1)作为消息主体中的消息头

      在HTTP场景中,第一个参数或者是inline(默认值,表示回复中的消息体会以页面的一部分或者整个页面的形式展示)
      ,或者是attachment(意味着消息体应该被下载到本地;大多数浏览器会呈现一个“保存为”的对话框,将filename的值
      预填为下载后的文件名,假如它存在的话)。
    

    如:Content-Disposition: attachment; filename="filename.jpg",这种情况下的网页或者网页中的附件会触发“另存为”的对话框。

  • (2)作为multipart body中的消息头

    常见有如下:

    Content-Disposition: form-data
    Content-Disposition: form-data; name="fieldName"
    Content-Disposition: form-data; name="fieldName"; filename="filename.jpg"
    
    

看到这,我想大家都已经很清楚,上面小明的案例应该要怎么修改了。说干就干,小明在理解了这部分知识后,最后新建了个文件上传请求继承于volley的request,重写getBody,通过拼接给字段/文件添加上Content-Disposition以及Content-Type这些描述,一顿操作下来。针对本次项目中使用到的那部分,主要添加的代码如下:(非OkHttp操作,OkHttp和Retrofit操作更加简便,如有需要可以另外自行参考别处)

响应报文

这里也顺带说一下响应报文。响应报文的组成:响应行+响应头+响应体

同样贴出一个响应的样例:也是以OKHttp打印的日志为例,与标准的格式会有些出入,但是大体结构还是如此。

另外附带一个粗略描述:

响应行
响应头
空行
响应体

  • 响应行

    状态行由 HTTP 协议版本字段、状态码和状态码的描述文本 3 个部分组成,他们之间使用空格隔开,如上面的 200 OK表示的是状态码+描述。一个标准的格式应该是:HTTP/1.1 200 OK

  • 响应头

    头部字段名称:值+回车符+换行符

  • 响应体

    如上面的json字符串则为响应的包体

结束语

对于上传文件的网络请求,区分POST字段内容是否被当成文件的关键是 Content-Disposition 是否包含 filename,因为文件有不同的类型,所以还要使用 Content-Type 指示文件的类型,如果不知道是什么类型取值可以为 application/octet-stream 表示文件是一个二进制的文件,如果不是文件则 Content-Type 可以省略。

至此,本次案情已经分析完,由于Http部分涉及内容太广,只讨论文件上传部分涉及的内容,如有不当,还望各位指出赐教,定当加以改正。感谢!