详解 POST 请求中的 Content-Type

在工作中,熟悉常见的Content-Type类型对于处理POST请求至关重要,它可以帮助我们更好地使用和解决问题。接下来我们一起学习POST请求相关的知识要点。

介绍

POST请求是一种常见的数据请求方式,相对于 GET 请求更安全、更灵活。一个标准的 POST 请求由以下三个部分组成:

  1. 请求行:包含了请求方法、URL和HTTP协议版本。
  2. 请求头:包含了关于请求的附加信息,常见的请求头字段有 Content-TypeAuthorization 等。
  3. 请求主体:请求的数据存储在请求主体中,具体的数据格式和编码方式由 Content-Type 字段确定。服务端会根据 Content-Type 字段使用不同的方式对请求体进行解析。

常见的 Content-Type

application/x-www-form-urlencoded

这是POST请求中最常见的一种编码方式,适用于表单数据提交。它是原生表单 POST 提交(enctype)的默认值,大部分服务端语言都对这种方式有很好的支持。

当使用 application/x-www-form-urlencoded 提交数据时,需要对参数进行 urlencoded 编码和序列化。数据被编码成以 & 分隔的键值对,同时以 = 分隔键和值,非字母或数字的字符会被 percent-encoding(百分比编码)。

例如,表单提交参数为:

makefile 复制代码
param1:website,
param2:https://www.google.com

经过 urlencoded 编码后:

makefile 复制代码
param1:website
param2:https%3A%2F%2Fwww.google.com

再经过序列化,得到结果:

ini 复制代码
param1=website&param2=https%3A%2F%2Fwww.google.com

采用这种格式发送到服务器的 HTTP 消息的主体本质上是一个巨大的查询字符串,结构简单。

但是由于需要对数据进行编码,这意味着值中存在的每个非字母数字,将需要三个字节来表示它。对于大型二进制文件,请求体将增加三倍,非常低效。

另外由于对数据进行了编码,非 ASCII 码的数据会丢失,所以类似传文件的场景也不能使用这种方式。

适用场景:数据量小的简单数据


multipart/form-data

这种编码方式主要用于文件上传,可以有效地传输二进制(非字母数字)数据,同时还可以携带其他信息。

multipart/form-data 格式中,数据被划分为多个部分(part),每个部分都以一个标头(headers)开始,并使用一组空行 来分隔。同时,每个部分还包含一个唯一的 boundary 字符串,用于标识不同部分之间的边界。

每个部分包含以下组成:

  • 边界:--boundary
  • 标头:元数据,如 Content-TypeContent-Disposition 等。
  • 空行:用于分隔标头和正文数据。
  • 正文数据:包含实际的文件数据或表单字段的值。

最后以 --boundary-- 表示请求体结束

一个简单的例子如下:

css 复制代码
POST http://www.example.com/upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryABC123

------WebKitFormBoundaryABC123
Content-Disposition: form-data; name="username"

zhang san
------WebKitFormBoundaryABC123
Content-Disposition: form-data; name="avatar"; filename="avatar.png"
Content-Type: image/png

...二进制数据...
------WebKitFormBoundaryABC123--

在这个例子中,请求头中指定了 Content-Type: multipart/form-data,并设置了一个唯一的 boundary 字符串作为分隔符(在此例中为 ----WebKitFormBoundaryABC123)。

在请求体中,我们可以看到两个部分。

  • 第一部分:以 --boundary 开始,并包含了一个标头 Content-Disposition: form-data; name="username"。这表示这个部分是一个表单字段,字段名为 username,字段值为 zhang san

  • 第二部分:以 --boundary 开始,并包含了两个标头。首先是 Content-Disposition: form-data; name="avatar"; filename="avatar.png"。然后是 Content-Type: image/png,指定了这个部分的数据类型为 image/png。接着是二进制文件数据主体。

最后以 --boundary-- 表示结束。

multipart/form-data 也可以用于传递普通数据,但是由于每个字段都有自己的一组MIME标头和边界字符串,这会增加数据包的大小。相对于其他简单的编码方式(如 application/x-www-form-urlencoded),它在处理普通数据时效率低,开销更大。

适用场景:文件上传、带有二进制数据的请求


application/json

使用 application/json 编码方式,可以将数据序列化成一个JSON字符串作为请求主体。相比其他只能传输键值对的方式,这种编码方式更加灵活和简单高效。它支持复杂的嵌套结构、数组、对象等,非常适合传输包含多层级数据关系的数据。

bash 复制代码
POST http://www.example.com HTTP/1.1
Content-Type: application/json;charset=utf-8

{"name":"test", "age": 24, "hobby":["a","b","c"]}

适用场景:复杂的结构化的数据

使用 Axios 发送 POST 请求

当我们工作中使用 axios 发送请求时,axios 会对请求头和请求参数进行一些额外的处理,方便我们使用。接下来我们通过源码来进行一个简单的分析。

首先我们找到源码中关于请求头中 Content-Type 处理的代码(不同版本代码略有不同):

js 复制代码
// https://github.com/axios/axios/blob/v2.x/lib/defaults/index.js

var DEFAULT_CONTENT_TYPE = {
  'Content-Type': 'application/x-www-form-urlencoded'
};

// 使用该方法会判定果用户有没有设置 Content-Type
// 没有则会设置相应的类型值
function setContentTypeIfUnset(headers, value) {
  if (!utils.isUndefined(headers) && utils.isUndefined(headers['Content-Type'])) {
    headers['Content-Type'] = value;
  }
}

function transformRequest(data, headers) {

  var contentType = headers && headers['Content-Type'] || '';
  var hasJSONContentType = contentType.indexOf('application/json') > -1;
  var isObjectPayload = utils.isObject(data);


  // 如果参数是个对象并且是 HTML Form 标签,会将参数转化为 FormData 对象
  // 补充:如果 FormData 构造函数的参数是一个 DOM 的表单元素,构造函数会自动处理表单的键值对,
  // 将 HTML 表单直接读取为 FormData
  if (isObjectPayload && utils.isHTMLForm(data)) {
    data = new FormData(data);
  }

  var isFormData = utils.isFormData(data);

  if (isFormData) {
    if (!hasJSONContentType) {
      return data;
    }
    
    // 如果参数是 FormData 格式
    // 并且用户设置了 Content-Type 是  application/json 格式,会将参数转化为 JSON 字符串
    // 否则不处理
    return hasJSONContentType ? JSON.stringify(formDataToJSON(data)) : data;
  }

  // 如果请求参数是 ArrayBuffer、Buffer、Stream、File、Blob 这几种格式的不做任何处理
  if (
    utils.isArrayBuffer(data) ||
    utils.isBuffer(data) ||
    utils.isStream(data) ||
    utils.isFile(data) ||
    utils.isBlob(data)
  ) {
    return data;
  }

  // 如果请求参数是 ArrayBuffer 格式的则返回 data.buffer
  if (utils.isArrayBufferView(data)) {
    return data.buffer;
  }

  // 如果请求参数是一个 URLSearchParams 对象
  // 会设置 Content-Type 为 application/x-www-form-urlencoded
  // 并调用 URLSearchParams.toString() 方法
  // 将参数转为序列化字符串 'foo=bar&hello=world'
  if (utils.isURLSearchParams(data)) {
    setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
    return data.toString();
  }

  var isFileList;

  // 如果参数是对象类型
  if (isObjectPayload) {

    // 如果用户设置的 Content-Type 是 application/x-www-form-urlencoded 格式
    // 则对参数进行URL编码并序列化 'foo=bar&hello=world'
    if (contentType.indexOf('application/x-www-form-urlencoded') !== -1) {
      return toURLEncodedForm(data, this.formSerializer).toString();
    }

    // 如果参数是 一个 FileList 对象
    // 或者用户设置了 Content-Type 是 multipart/form-data
    // 那么会将参数对象转为 FormData 对象
    if ((isFileList = utils.isFileList(data)) || contentType.indexOf('multipart/form-data') > -1) {
      var _FormData = this.env && this.env.FormData;

      return toFormData(
        isFileList ? {'files[]': data} : data,
        _FormData && new _FormData(),
        this.formSerializer
      );
    }
  }

  // 如果参数是对象类型
  // 或者设置的 Content-Type 是 application/json
  // 会设置 Content-Type 为 application/json
  // 并将参数转化为 一个 JSON 字符串
  if (isObjectPayload || hasJSONContentType ) {
    setContentTypeIfUnset(headers, 'application/json');
    return stringifySafely(data);
  }

  return data;
}

// 如果用户没有设置请求头,同时也没有命中 transformRequest 中对 Content-Type 的处理
// 则会使用默认值 DEFAULT_CONTENT_TYPE
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  defaults.headers[method] = utils.merge(DEFAULT_CONTENT_TYPE);
});

如果请求参数是 FormData 类型的,在最终发送请求前(在 dispatchRequest 里,在拦截器 request 之后),还会进行一次判断处理

js 复制代码
// axios/lib/adapters/xhr.js
module.exports = function xhrAdapter(config) {
  // ...... 已省略部分代码 ......

  return new Promise(function dispatchXhrRequest(resolve, reject) {

    // 发送ajax之前,如果发现请求参数是 FormData 类型的
    // 会删除请求头中 'Content-Type' 的设置,让浏览器自己选择
    // multipart/form-data

    if (utils.isFormData(requestData) && utils.isStandardBrowserEnv()) {
      delete requestHeaders['Content-Type']; // Let the browser set it
    }
  })
}

当 axios 使用 application/x-www-form-urlencoded 请求时,如果参数不是 URLSearchParams 类型的,则可能需要我们使用 qs.stringify 方法对参数进行序列化。

总结

本文详细介绍了与POST请求相关的Content-Type类型。通过了解这些Content-Type类型可以在开发过程中快速的排查并解决相关问题,帮助我们提高开发效率。

往期文章推荐:

相关推荐
神夜大侠3 分钟前
VUE 实现公告无缝循环滚动
前端·javascript·vue.js
明辉光焱5 分钟前
【Electron】Electron Forge如何支持Element plus?
前端·javascript·vue.js·electron·node.js
柯南二号38 分钟前
HarmonyOS ArkTS 下拉列表组件
前端·javascript·数据库·harmonyos·arkts
究极无敌暴龙战神X1 小时前
前端学习之ES6+
开发语言·javascript·ecmascript
明辉光焱1 小时前
【ES6】ES6中,如何实现桥接模式?
前端·javascript·es6·桥接模式
nameofworld1 小时前
前端面试笔试(二)
前端·javascript·面试·学习方法·数组去重
pumpkin845142 小时前
客户端发送http请求进行流量控制
python·网络协议·http
hummhumm2 小时前
第 12 章 - Go语言 方法
java·开发语言·javascript·后端·python·sql·golang
hummhumm2 小时前
第 8 章 - Go语言 数组与切片
java·开发语言·javascript·python·sql·golang·database
zhanghaisong_20153 小时前
Caused by: org.attoparser.ParseException:
前端·javascript·html·thymeleaf