Content-Type引发的服务端收不到HTTP请求参数的问题

问题现象:

前端POST请求参数已经传过来了,Java后端Debug也能进到请求里,可就是取不到请求参数。


用Chrome 开发者工具可以看到请求的不同:



可以看到请求参数一个在Form Data中,一个在Request Payload中,而且格式也不同。

不同的原因就在于Content-Type设置不同。


HTTP Content-Type 

用于标识传输数据的类型。在请求中,Content-Type告诉服务端实际请求内容的类型;在响应中,Content-Type告诉客户端实际返回内容的类型。

HTTP定义的Content-Type类型有近200种(https://www.w3cschool.cn/http/ahkmgfmz.html),其中最常用的是以下三种:

1、application/x-www-form-urlencoded
请求参数在Form Data中,只能上传键值对,并且键值对都是间隔分开的。
参数形式:  name1=value1&name2=value2

2、multipart/form-data
请求参数在Request Payload中,既可以上传文件等二进制数据,也可以上传表单键值对,只是最后会转化为一条信息。
浏览器将表单内的数据和文件放在一起发送。这种方式会定义一个不可能在数据中出现的字符串作为分隔符,然后用它将各个数据段分开。

3、application/json
请求参数在Request Payload中
参数形式: {key1:value1,key2:value2}


multipart/form-data; boundary=${bound}   // 其中${bound}是一个占位符,代表我们规定的分割符,可以自己任意规定,但为了避免和正常文本重复了,尽量要使用复杂一点的内容。

以下是一个multipart/form-data类型的请求:



重点:对应以上三种类型Java服务端获取请求参数的方法也不同(伪代码)

1、application/x-www-form-urlencoded

1)注解@RequestParam(value="name1") String name1  
2)注解@ModelAttribute 绑定请求参数到指定对象
3)HttpServletRequest.getParameter("name1")

2、multipart/form-data

流HttpServletRequest.getInputStream()或者HttpServletRequest.getReader()

3、application/json
1)注解@RequestBody 
2)流HttpServletRequest.getInputStream()或者HttpServletRequest.getReader()

Tips: request.getParameter()、 request.getInputStream()、request.getReader()这三种方法是有冲突的,因为流只能被读一次,所以只有第一次能取到参数。


原理

那么是什么导致Content-Type类型不同取参方式也不同呢?
由于Tomcat对于Content-Type multipart/form-data(文件上传)和application/x-www-form-urlencoded(POST请求)做了“特殊处理”:
Tomcat的HttpServletRequest类的实现类为org.apache.catalina.connector.Request,而它处理请求参数的方法为protected void parseParameters(),这个方法中对Content-Type multipart/form-data(文件上传)和application/x-www-form-urlencoded(POST请求)做了处理,会解析表单数据放到request parameter map中。其他请求不会解析表单数据,所以通过request.getParameter()是获取不到的。

服务器为什么会对Content-Type application/x-www-form-urlencoded做特殊处理?

因为表单提交数据是名值对的方式,而其他的post请求(Content-Type不是application/x-www-form-urlencoded)数据格式不固定,不一定是名值对的方式,服务器无法知道具体的处理方式,所以只能通过获取原始数据流的方式来进行解析。

tomcat部分源码:

protectedvoid parseParameters() {  
           //省略部分代码......  
           parameters.handleQueryParameters();// 这里是处理url中的参数  
           //省略部分代码......  
           if ("multipart/form-data".equals(contentType)) { // 这里是处理文件上传请求  
                parseParts();  
                success = true;  
                return;  
           }  
   
           if(!("application/x-www-form-urlencoded".equals(contentType))) {// 这里如果是非POST请求直接返回,不再进行处理  
                success = true;  
                return;  
           }  
           //下面的代码才是处理POST请求参数  
           //省略部分代码......  
           try {  
                if (readPostBody(formData, len)!= len) { // 读取请求体数据  
                    return;  
                }  
           } catch (IOException e) {  
                // Client disconnect  
                if(context.getLogger().isDebugEnabled()) {  
                    context.getLogger().debug(  
                            sm.getString("coyoteRequest.parseParameters"),e);  
                }  
                return;  
           }  
           parameters.processParameters(formData, 0, len); // 处理POST请求参数,把它放到requestparameter map中(即request.getParameterMap获取到的Map,request.getParameter(name)也是从这个Map中获取的)  
           // 省略部分代码......  
}

protected int readPostBody(byte body[], int len)  
   throws IOException {  

   int offset = 0;  
   do {  
	   int inputLen = getStream().read(body, offset, len - offset);  
	   if (inputLen <= 0) {  
			return offset;  
	   }  
	   offset += inputLen;  
   } while ((len - offset) > 0);  
   return len;  
}

实际开发中具体选用哪种Content-Type呢?
Form解析可以直接从Request对象中获取请求参数,这样对象转换与处理相对容易,但在大片JSON数据需要提交时,可能会出现大量的数据拆分与处理工作,另外针对集合类型的处理,也是其比较孱弱的地方。
而Payload的优势是一次可以提交大量JSON字符串,但无法从Request从获取参数,也会受限于JSON解析的深度(尤其是有多层对象级联的情况,最底层的对象几乎无法转换为具体类型)。


其他
jQuery在执行post请求时,会默认设置Content-Type为application/x-www-form-urlencoded,所以服务器能够正确解析,
而使用原生ajax请求时,如果不显示的设置Content-Type,那么默认是text/plain,这时服务器就不知道怎么解析数据了,所以才只能通过获取原始数据流的方式来进行解析请求数据。

jQuery中的dataType指的是预期服务器返回的数据类型,而不是发送的数据类型。如果不指定,jQuery 将自动根据 HTTP 包 MIME 信息来智能判断,比如 XML MIME 类型就被识别为 XML。这样服务端返回json数据,前端就会获取不到返回值。


### 如何正确获取 HTTP 响应头中的 `Content-Type` 字段 当尝试通过 JavaScript 或其他前端框架访问 HTTP 响应头时,可能会遇到某些字段无法被读取的情况。这是因为跨域资源共享(CORS)策略限制了哪些响应头可以暴露给客户端[^4]。 #### 跨域资源共享 (CORS) 对响应头的影响 默认情况下,只有以下标准头部会被浏览器自动暴露给前端脚本: - Cache-Control - Content-Language - Content-Type - Expires - Last-Modified - Pragma 如果自定义的响应头(如 `Content-Disposition`)未显式声明为可公开,则它们会被浏览器提供给前端代码[^3]。对于这种情况,服务器端需要设置 CORS 头部 `Access-Control-Expose-Headers` 来指定额外允许暴露的响应头名称列表。 #### 正确配置服务器端以支持获取特定响应头 为了使前端能够成功读取 `Content-Type` 或者其他的非默认响应头,比如 `Content-Disposition`,服务端应当调整其 CORS 配置如下: ```java // Java Servlet 示例 设置 Access-Control-Expose-Headers response.setHeader("Access-Control-Expose-Headers", "Content-Type, Content-Disposition"); ``` 上述代码片段展示了如何向响应添加 `Access-Control-Expose-Headers` ,其中包含了两个具体的头部名字:`Content-Type` 和 `Content-Disposition` 。这一步骤确保这些头部能被跨源请求所识别并利用。 #### 使用 Angular 拦截器处理响应头 即使已经设置了正确的 CORS 参数,在像 Angular 这样的现代前端框架中使用拦截器捕获完整的响应信息也需要特别注意方法的选择。例如,Angular 的 HttpClient 默认只返回 body 数据部分而包括所有的元数据除非明确指定了观察选项 'observe' 为 'response': ```typescript this.http.get('/api/endpoint', { observe: 'response' }).subscribe(resp => { console.log(resp.headers.get('Content-Type')); // 获取 Content-Type }); ``` 此 TypeScript 片段演示了一个 GET 请求的例子,它仅接收到了 API endpoint 返回的主要 payload (主体),还连同整个 HttpResponse 结构一起接收到,从而使得我们可以安全地调用 `.headers.get()` 方法来提取任意已知存在的响应头项。 ### 总结 要解决无法获得 `response.headers['content-type']` 的问题,需确认两点:一是服务器是否正当地设定了 `Content-Type` 并将其列入到 `Access-Control-Expose-Headers` 中;二是前端应用是否采用了恰当的方式去解析完整的 HTTP Response 对象而非仅仅关注于 Body 内容本身。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值