第4章 发送POST请求:表单提交、JSON提交、文件上传

POST请求,说白了就是客户端往服务端塞数据。但塞的方式不同,服务端解析的逻辑就完全不同。很多新手在这个环节翻车,我就见过不少人把JSON当普通表单发,结果服务端死活拿不到参数。

我个人习惯把POST的三种主流方式记成:古老的传统、主流的现代、还有带附件的特殊场景。咱们一个一个来拆。

POST 请求三种提交方式对比 表单提交 application/x-www-form-urlencoded JSON提交 application/json 文件上传 multipart/form-data Content-Type 关键 application/x-www-form-urlencoded Content-Type 关键 application/json Content-Type 关键 multipart/form-data; boundary=... 格式 key=value&key=value 格式 {"key":"value"} 格式 多部分分隔 boundary

4.1 表单提交:最传统的方式

最早的Web交互就是靠表单。你想想看,用户在网页上填信息,点提交,浏览器自动把表单数据编码成 key=value&key=value 的格式发出去。这就是 application/x-www-form-urlencoded。

这段代码演示了手动构造表单提交的请求体:

POST /api/login HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 35

username=admin&password=123456

看着简单吧?但坑就藏在简单里。我记得有一次排查线上问题,发现某个参数死活收不到。最后查出来——参数名拼错了。别笑,这种事真能发生,尤其是参数一多的时候。

提示:表单提交时,有特殊字符(如中文、空格、&)必须做 URL 编码。比如空格会变成 + 或 %20。大部分现代 HTTP 客户端会自动帮你做,但手动构造请求时别忘。

4.2 JSON提交:现代微服务的主流

现在写后端接口,很少用纯表单了。JSON 格式结构清晰,支持嵌套,而且几乎所有语言都能轻松解析。设置 Content-Type: application/json 就行。

POST /api/user HTTP/1.1
Host: example.com
Content-Type: application/json
Content-Length: 56

{
  "name": "张三",
  "email": "zhangsan@example.com",
  "age": 28
}

这里有个很容易翻车的地方——请求头的 Content-Type 必须和服务端期望的一致。我见过有人明明发的是 JSON 体,Content-Type 却写成了 text/plain。服务端拿到请求,直接返回 415 Unsupported Media Type。

警告:JSON 格式要求严格。末尾多一个逗号、key 没有用双引号包裹,都会导致解析失败。特别是 Java 和 Go 这类强类型语言,JSON 字段名大小写敏感,别指望它能智能匹配。

4.3 文件上传:multipart/form-data

文件上传就有点特殊了。你不能直接把二进制数据塞进 key=value 格式,也做不到把文件塞进 JSON 里(除非用 base64 编码,但那会让体积膨胀约 33%)。

所以 HTTP 设计了 multipart/form-data。它的核心思路是:用 boundary(边界分隔符)把不同的数据块隔开。每个数据块有自己的头部,指明它是普通字段还是文件。

POST /api/upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW

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

admin
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="avatar"; filename="photo.jpg"
Content-Type: image/jpeg

(此处为二进制文件数据,省略)
------WebKitFormBoundary7MA4YWxkTrZu0gW--
提交方式 Content-Type 适用场景 数据格式
表单提交 application/x-www-form-urlencoded 传统 HTML 表单、简单键值对 URL 编码的字符串
JSON 提交 application/json RESTful API、微服务通信 JSON 字符串
文件上传 multipart/form-data; boundary=... 上传文件、混合表单数据 多部分二进制/文本数据

这里有个实践建议:如果你的接口既要传普通字段,又要传文件,那就直接用 multipart/form-data。没必要把字段塞 JSON 里再单独发一次。一个请求搞定所有事,既减少网络开销,又保证数据一致性。

4.4 三种方式的代码示例(Go 语言)

光说理论不够硬。我写三段代码,分别展示用 Go 发送三种 POST 请求。你拿回去就能跑。

表单提交示例

package main

import (
	"fmt"
	"net/http"
	"net/url"
	"strings"
)

func main() {
	data := url.Values{
		"username": {"admin"},
		"password": {"123456"},
	}
	
	resp, err := http.Post("https://example.com/api/login",
		"application/x-www-form-urlencoded",
		strings.NewReader(data.Encode()))
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	fmt.Println(resp.StatusCode)
}

JSON提交示例

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
)

type User struct {
	Name  string `json:"name"`
	Email string `json:"email"`
	Age   int    `json:"age"`
}

func main() {
	user := User{
		Name:  "张三",
		Email: "zhangsan@example.com",
		Age:   28,
	}
	
	jsonData, _ := json.Marshal(user)
	resp, err := http.Post("https://example.com/api/user",
		"application/json",
		bytes.NewReader(jsonData))
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	fmt.Println(resp.StatusCode)
}

文件上传示例

package main

import (
	"bytes"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"os"
)

func main() {
	payload := &bytes.Buffer{}
	writer := multipart.NewWriter(payload)
	
	// 添加普通字段
	_ = writer.WriteField("username", "admin")
	
	// 添加文件字段
	file, _ := os.Open("/path/to/photo.jpg")
	defer file.Close()
	
	part, _ := writer.CreateFormFile("avatar", "photo.jpg")
	_, _ = io.Copy(part, file)
	
	_ = writer.Close()
	
	contentType := writer.FormDataContentType()
	resp, err := http.Post("https://example.com/api/upload",
		contentType, payload)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	fmt.Println(resp.StatusCode)
}

避坑提醒:我曾经犯过一个错误——用 multipart.NewWriter 创建了 writer,但忘记调用 writer.Close()。结果文件数据没写全,服务端收到不完整的 multipart 报文。所以务必在写完所有字段后关闭 writer,否则最后的 boundary 结束符不会写入。

4.5 如何选择

选哪种方式,说白了就三条原则:

  • 纯键值对、无文件 —— 用表单提交。简单高效,浏览器兼容性最广。
  • 复杂结构、微服务调用 —— 用 JSON。可读性强,嵌套结构友好。
  • 有文件、或混合字段 —— 用 multipart/form-data。它是专门为二进制数据传输设计的。

最后提一句,HTTP 不会限制你混用。你完全可以在 JSON 里嵌套 base64 的图片数据,但那就真的不优雅了。按照 HTTP 的设计哲学,什么场景用什么 Content-Type,别自己的折腾自己。