ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

HTTP请求方法详解:GET、POST、PUT、DELETE的语义、幂等性与实战排查

HTTP请求方法详解:GET、POST、PUT、DELETE的语义、幂等性与实战排查 HTTP请求方法这四个词几乎是每个开发每天都要打交道的但很多人对它的理解停在“GET是查POST是传”这个粗糙层面。实际项目中因为请求方法选错、用错导致的线上事故我见得太多了——把DELETE接口实现成POST结果调用方一脚踩进坑里GET接口返回体里塞了一堆不该塞的数据把响应时间干到几百毫秒PUT和POST混用数据重复创建还查不出原因。这篇文章准备把GET、POST、PUT、DELETE这四种最常用的HTTP请求方法彻底讲透包括它们背后的设计思路、幂等性原理、实战代码怎么写、抓包怎么验证再加上我这些年踩过的坑和总结的排查技巧。无论你是刚接触HTTP协议的后端新人还是前端经常调接口的同学这篇内容都能让你对“请求方法”这件事的理解直接上一个台阶。1. 内容整体设计与思路拆解1.1 为什么HTTP需要区分多种请求方法HTTP协议设计之初就面临一个问题客户端和服务器之间的交互不只是“我要数据”这么简单。有时候是发一条新内容有时候是改一段旧内容有时候是删掉一条记录还有时候只是探一下服务器能不能连通。如果所有操作都用一个方法来表达那业务逻辑就得靠URL和参数去“猜”代码写起来乱接口文档也没法看。这个设计逻辑其实和我们在现实中的沟通方式很像。你叫服务员点菜不会说“你看着办”你会明确告诉他“这桌来一份菜单上没有的菜”——这是创建你把已经上桌的菜退了说“这个菜不要了”——这是删除你说“这个菜少放盐”——这是修改。语义清晰双方才不会误解。HTTP请求方法就是HTTP协议里的“动词”它告诉服务器你这次请求想做的是查询、创建、修改还是删除。配合URL这个“名词”所指向的资源就构成了一套完整的表达体系。这一套以“资源动词”为核心的风格后来被RESTRepresentational State Transfer表现层状态转移发扬光大成了Web API设计的主流范式。1.2 请求方法选型的核心决策思路选方法这事看着简单真上手时很多人会犹豫。我总结出三个判断维度第一个是语义匹配。你要做的业务操作是什么就选对应语义的方法。查用GET新增用POST整体替换用PUT删除用DELETE局部修改用PATCH。可能有人觉得“反正后端路由都能配方法名无所谓”但语义不清晰会让接口的可维护性大打折扣。你三个月后回来看代码发现新增业务挂在PUT上第一反应肯定是一脸懵。第二个是幂等性。幂等的意思是同一个请求发多少次对服务器产生的效果都是一样的。GET是幂等的你查十次和查一次结果一样PUT是幂等的用同样的数据完整覆盖十次最终状态还是那份数据DELETE通常也是幂等的删一个不存在的资源返回404也说得通但POST不是幂等的同一个创建请求发两次就可能产生两条重复数据。电商下单接口为什么不能用GET就是因为重复提交会造成重复订单这是严重的业务事故。第三个是参数传递方式。GET的参数放在URL的查询字符串里适合小数据量POST、PUT、DELETE的参数可以放在请求体里能承载更大的数据量也方便传JSON结构。你去看那些报错日志很多都是GET请求的URL太长直接撑爆了服务器对请求行的长度限制。2. 核心细节解析与实操要点2.1 GET最常用的查询方法也有自己的边界GET的设计目的是“获取资源”它的语义是客户端向服务器请求某个资源的表示形式服务器不应该对数据做任何修改。GET请求的参数一般拼接在URL上形如/api/users?id123name张伟服务器从查询字符串里取参数。GET有两个关键特性。第一是安全性它不对服务器数据做修改所以可以被搜索引擎抓取、被浏览器缓存、被代理服务器缓存。第二是幂等性同样的GET请求重复执行结果不会变。这两个特性让GET很适合做数据查询和页面展示。但GET也有很多坑。URL长度受限是最常见的——虽然HTTP协议本身没明确规定URL上限但浏览器IE大约2083个字符、Nginx默认8KB、Tomcat默认8KB都会限制所以千万别用GET传大量数据。另外GET的参数会明文暴露在URL里会出现在浏览器历史、服务器访问日志、代理日志中绝对不能用它传密码、Token等敏感信息。注意我在实际项目里排查过一起“接口超时”问题最后发现是前端把一段几百KB的Base64字符串拼到了GET请求的URL参数里请求还没发出去浏览器就先报错了。这种问题属于使用场景选错应该改用POST把数据放请求体。2.2 POST创建资源的“万能钥匙”POST的语义是“在服务器上创建资源”或者在更宽泛的语境里“向服务器提交数据并交给服务器处理”。比如注册账号、发帖子、上传文件、发起支付这些都是POST。POST的参数可以放在URL上但规范做法是放请求体里。请求体格式常见的有几种application/x-www-form-urlencoded表单格式键值对用连接、application/jsonJSON字符串、multipart/form-data文件上传。选哪种格式要和后端约定好设对Content-Type请求头否则后端解析不了。我调试时经常看到有人请求体写JSON但Content-Type没设置后端按表单解析结果字段全是空。POST有一个重要特性非幂等。连续提交两次相同内容的POST请求服务器会创建两条资源。正因为这个特性POST不适合做“重试”敏感的操作——除非你在后端做了幂等控制。常见的做法是客户端带上一个唯一的业务请求ID幂等键服务器记下这个键相同键的重复请求直接返回第一次的结果避免重复下单、重复支付。注意安全方面POST比GET稍微好一点——参数不走URL、不进浏览器历史但如果没有HTTPS加密抓包一样能看到请求体明文。不要因为用了POST就觉得数据安全了。2.3 PUT整体替换的确定性操作PUT的语义是“将指定URL上的资源整体替换为请求体中的数据”。如果资源不存在有些服务器也会创建它如果存在就完全覆盖。它和POST的核心区别在于PUT是幂等的而且语义是“替换整个资源”。怎么理解“整体替换”比如用户信息是一个对象{name: 张三, age: 18, email: zhangsanexample.com}。用PUT把age改成20请求体里如果只带了{age: 20}那服务器就可能把name和email替换成空值。因为PUT的语义是“以你给的这份数据为准整体覆盖”。所以用PUT的时候前端通常要先把资源完整数据取回来改完再整个提交。正因如此很多团队更倾向于用PATCH做局部修改想改哪个字段就传哪个字段其他不动。2.4 DELETE删除操作要谨慎设计DELETE的语义很直接把指定URL上的资源删掉。它也是幂等操作——第一次删除返回200或204再删同一条资源可能返回404但服务器的最终状态都是“资源不存在”。DELETE方法在实际开发中经常被绕过。有些团队因为浏览器兼容性、框架限制或者某些中间件过滤了DELETE请求就用POST加?_methodDELETE之类的参数来模拟。但我建议能用真DELETE就用真DELETE。因为HTTP方法语义本身就是一种接口文档把删除做成POST别人看接口时还得靠注释和约定去猜。DELETE还有一个设计问题要不要设计成“逻辑删除”很多系统不会真的从数据库里物理删掉数据而是打一个deleted标记。这属于业务设计范畴但HTTP层也有影响DELETE后返回什么状态码返回200但响应体里带删除结果对象还是返回204不返回响应体我推荐删除成功后返回204 No Content语义干净客户端也不用解析无意义的响应体。如果要返回被删除的对象用200同时带上对象数据也不是错但团队内要统一。2.5 容易被忽略的兄弟方法PATCH、HEAD、OPTIONSPATCH是局部更新。前面说PUT是整体替换PATCH就是只修改指定字段。它的请求体不需要完整资源传什么改什么。需要留意的是PATCH不是幂等的——虽然它在语义上确实更灵活但选它时要注意重复提交的潜在影响。有些后端框架对PATCH支持不完善接第三方接口时先确认对方是否实现了PATCH语义。HEAD和GET几乎一样区别是服务器只返回响应头不返回响应体。它常被用来“探测资源是否存在”、检查链接有效性、获取文件大小等不用下载整个文件。响应头里的Content-Length字段就能告诉你资源有多大。OPTIONS用于“询问服务器支持哪些方法”它也是跨域请求预检的核心。浏览器在发起跨域的PUT、DELETE、PATCH请求前会先发一个OPTIONS请求问服务器允不允许这个域名的来源、允许哪些方法、允许哪些请求头。服务器必须正确回应Access-Control-Allow-Methods等字段否则浏览器会在控制台报CORS错误实际请求根本不会发出。3. 实操过程与核心环节实现3.1 Python requests库完整CRUD示例Python的requests库是测试HTTP接口最顺手的工具简洁直观适合演示四种方法的完整写法。先安装依赖pip install requests下面是一段完整的CRUD示例模拟一个“用户管理”接口import requests BASE_URL https://api.example.com/api/users HEADERS {Content-Type: application/json, Authorization: Bearer your-token} # 1. GET查询用户列表 resp requests.get(BASE_URL, headersHEADERS, params{page: 1, size: 20}) print(GET状态码:, resp.status_code) print(GET响应:, resp.json()) # 2. GET查询单个用户 resp requests.get(f{BASE_URL}/1001, headersHEADERS) print(GET详情状态码:, resp.status_code) print(GET详情响应:, resp.json()) # 3. POST创建新用户 new_user {name: 张三, age: 25, email: zhangsanexample.com} resp requests.post(BASE_URL, headersHEADERS, jsonnew_user) print(POST状态码:, resp.status_code) created_user resp.json() print(创建成功用户ID:, created_user.get(id)) # 4. PUT完整替换用户信息 updated_user {name: 张三丰, age: 30, email: zhangsfexample.com} resp requests.put(f{BASE_URL}/1001, headersHEADERS, jsonupdated_user) print(PUT状态码:, resp.status_code) # 5. PATCH局部修改用户信息 patch_data {age: 31} resp requests.patch(f{BASE_URL}/1001, headersHEADERS, jsonpatch_data) print(PATCH状态码:, resp.status_code) # 6. DELETE删除用户 resp requests.delete(f{BASE_URL}/1001, headersHEADERS) print(DELETE状态码:, resp.status_code) # 7. HEAD只获取响应头探测资源是否存在 resp requests.head(f{BASE_URL}/1001, headersHEADERS) print(HEAD状态码:, resp.status_code) print(HEAD响应体长度:, len(resp.content))几点经验params参数传字典requests会自动帮你编码并拼到URL上不需要手动拼?page1size20。json参数会自动设置Content-Type: application/json并序列化请求体比用datajson.dumps(payload)更省事。PUT请求一定要传完整资源数据我一开始演示时只改了age结果后端把其他字段清空了这就是PUT和PATCH使用方式混了。3.2 Node.js侧的写法fetch与axios前端用浏览器内置的fetch可以这样写// GET const listResp await fetch(https://api.example.com/api/users?page1size20, { headers: { Authorization: Bearer your-token } }); const users await listResp.json(); // POST const createResp await fetch(https://api.example.com/api/users, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ name: 李四, age: 22 }) }); const created await createResp.json(); // PUT const updateResp await fetch(https://api.example.com/api/users/1002, { method: PUT, headers: { Content-Type: application/json }, body: JSON.stringify({ name: 李四, age: 23, email: lisiexample.com }) }); // DELETE const delResp await fetch(https://api.example.com/api/users/1002, { method: DELETE });axios库写法也大同小异只是用axios.put、axios.delete会更直观。这里有一个前端常见的坑fetch在HTTP状态码为404或500时并不会抛出异常它照样返回一个Response对象只是ok字段变成false。你必须在代码里主动判断if (!resp.ok)再做错误处理否则接口挂了你的回调里拿到的还是一个空对象排查起来很费劲。3.3 curl命令行调试前的第一选择我习惯在写代码前先用curl确认接口行为这样可以跳过前端框架的干扰直接验证后端逻辑。几个常用示例# GET 查看用户列表带查询参数 curl -X GET https://api.example.com/api/users?page1size20 \ -H Authorization: Bearer your-token # POST 创建用户-d 指定请求体-H 设置 JSON 头 curl -X POST https://api.example.com/api/users \ -H Content-Type: application/json \ -d {name: 王五, age: 28} # PUT 完整替换 curl -X PUT https://api.example.com/api/users/1003 \ -H Content-Type: application/json \ -d {name: 王五, age: 29, email: wangwuexample.com} # DELETE 删除 curl -X DELETE https://api.example.com/api/users/1003 -i # HEAD 只看响应头-I 更方便 curl -I https://api.example.com/api/users/1003 # OPTIONS 探测服务器支持的方法 curl -X OPTIONS https://api.example.com/api/users -i-i参数会让curl把响应头也打印出来排查状态码和响应字段时很有用。-v参数能打印整个HTTP请求的明细包括请求行、请求头、响应头想知道你的请求到底发成了什么样子用curl -v是最直观的。3.4 抓包验证浏览器DevTools与Charles写代码时有一件事一定要养成习惯打开浏览器开发者工具的Network面板点开具体请求看三样东西——请求行的Method字段、请求头的Content-Type和Authorization、响应状态码。很多联调问题不用看后端日志光看Network面板就能真相大白。举个例子你明明调的是PUT接口但Network面板里显示Method是POST。这种情况通常有两种原因要么前端代码的method写错了要么请求经过了代理或后端框架的“方法改写”。还有更隐蔽的浏览器发出一个PUT请求时如果触发了CORS预检Network里会先看到一个OPTIONS请求然后才是真正的PUT请求。有人不明就里看到OPTIONS就以为后端接口路径配错了其实这是正常流程。Charles抓包工具有一个常用功能Compose标签里可以手动修改请求方法把GET改成POST试试后端会不会返回405或者把DELETE改成PUT看路由是否区分。这类改动用来测试后端方法的容忍度很方便。我排接口权限问题的时候经常这样快速验证同一路径用不同方法各发一次看服务器分别返回什么状态码。4. 常见问题与排查技巧实录4.1 405 Method Not Allowed服务器明确告诉你“这个方法不行”实战中最常见的一个报错是405 Method Not Allowed对应英文就是热词里那句the specified http method is not allowed for the requested resource.出现这个状态码说明URL路径存在但服务器不允许你用当前这个HTTP方法访问它。排查思路按顺序来第一确认路径是否正确。有些框架对路由末尾斜杠敏感/api/users/和/api/users可能被视为不同路由导致方法匹配不上。第二确认路由定义的方法。后端代码里只注册了GET /api/users你用POST去打自然得到405。这时去看后端代码的app.route(/api/users, methods[GET])就知道了把POST加进methods列表就行。第三确认是不是中间件过滤了某些方法。Nginx配置、网关层安全策略都可能只放行GET和POST而拦截PUT、DELETE。我在和第三方联调时遇到过的情况是对方服务器前面挂了一层WAF把DELETE请求全拦截了返回405业务方还以为路由没配好。注意405和404很容易混淆却不同。404是“路径不存在”405是“路径存在但方法不匹配”。用curl -I看响应头里有没有Allow字段这个字段会列出该路径支持的所有方法比如Allow: GET, POST直接照着改就行。4.2 CORS跨域与OPTIONS预检热词里那句access to xmlhttprequest at http://127.0.0.1:8000/myapp/center from origin就是典型的跨域报错。前端页面在A域接口在B域浏览器出于安全策略默认不允许跨域Ajax请求。如果是“简单请求”GET、POST配合特定请求头浏览器直接发如果是“非简单请求”用了PUT、DELETE、PATCH、自定义请求头如Authorization或者Content-Type不是简单值浏览器会先发OPTIONS预检。预检请求本身不是问题问题是后端必须正确回应。常见的报错是响应头里没有Access-Control-Allow-Methods或者没有Access-Control-Allow-Headers。解决方法是在服务器加响应头以Nginx为例add_header Access-Control-Allow-Methods GET, POST, PUT, DELETE, PATCH, OPTIONS; add_header Access-Control-Allow-Headers Content-Type, Authorization, X-Requested-With; add_header Access-Control-Allow-Origin *;注意Access-Control-Allow-Origin不能和Cookie机制随便混用。如果接口需要携带Session Cookie这个值不能设为*必须指定具体允许的来源域名并且前端请求要开启withCredentials否则浏览器会拒绝读取响应。4.3 状态码速查方法用对了状态码也得看得懂请求方法代表“你想做什么”状态码代表“服务器做到什么程度”。两者配合才能读懂一次HTTP交互。整理一张速查表方便你排查时对着看状态码含义和请求方法的关系200 OK请求成功GET查询成功、PUT/DELETE成功且返回数据201 Created创建成功POST成功创建资源时最标准204 No Content无内容返回DELETE、PUT成功后常用304 Not Modified未修改走缓存GET配合If-Modified-Since之类的条件请求400 Bad Request请求参数错误POST/PUT请求体格式不对、字段缺失401 Unauthorized未认证没带Token或Token过期403 Forbidden无权限已认证但无权访问也可能是WAF拦截404 Not Found路径不存在GET/PUT/DELETE访问不存在的资源405 Method Not Allowed方法不允许当前路径不支持你用的方法500 Internal Server Error服务器内部错误后端代码崩溃先查后端日志502 Bad Gateway网关错误Nginx后面服务没起来或超时503 Service Unavailable服务不可用服务过载、正在重启热词里那个unexpected status 502 bad gateway: unknown error, url: http://127.0.0.1:15721/v1/responses和error response from daemon: get https://registry-1.docker.io/v2/: net/http本质都是HTTP客户端curl或Go的net/http收到了502而无法建立完整连接。这类问题不在请求方法上而是后端服务或网络链路的问题排查时要先分清层次。4.4 连接复用与性能优化http连接复用是热词里出现频率很高的词。它的核心机制是Keep-AliveHTTP/1.1默认启用连接复用同一个TCP连接上可以连续发送多个HTTP请求避免每次请求都经历TCP三次握手和TLS握手明显降低延迟。实际项目中请求方法和连接复用也有关系。浏览器对同一个域名的并发连接数量有限制HTTP/1.1下通常6个如果你的页面一次性发出几十个GET请求多余的就要排队。这时有两个优化方向一是开启HTTP/2多路复用允许多个请求在同一个连接上并行传输不受“一次一个请求”的限制二是合并请求把多个查询合并成一个POST或一个批量GET接口。HTTP/1.1的Connection: keep-alive与HTTP/2的多路复用不同。HTTP/2把TCP连接拆成了多个流不同请求交错传输互不阻塞解决了HTTP/1.1队头阻塞问题。但HTTP/2对服务器和中间设备的兼容性要求也高升级前要做充分的灰度验证。4.5 HTTP和HTTPS、TCP的关系到底怎么分很多人会把HTTP、HTTPS、TCP混为一谈这里用一句话理清HTTP是应用层协议定义请求和响应格式TCP是传输层协议负责数据可靠传输HTTPS是在HTTP和TCP之间加了一层TLS加密让数据在传输中不被窃听或篡改。你可以这样理解HTTP是“信封上的书写格式”TCP是“邮局的运输网络”HTTPS是“带锁和防拆封的信封”。请求方法属于HTTP层它不关心下层是TCP还是其他传输协议。平时排查问题时会看到net/http: request canceled这样的Go语言报错它发生在HTTP客户端层面但根因往往是TCP连接超时、服务没起来、负载均衡把连接断掉了。http和https的区别这个热词的答案说白了就是多了一层SSL/TLS加密。实际效果是用HTTP传POST请求体里的密码抓包工具一眼就能看到明文用HTTPS抓包看到的全是密文。现在的线上接口凡涉及用户数据我建议一律HTTPS别省这点成本。4.6 POST和PUT到底选谁从重复提交事故说起我碰到过一个真实案例一个内部系统把“更新配置”的接口从PUT改成了POST因为前端框架对PUT支持不太好。结果运营同学在网络卡顿时连点了两次保存生成了两条配置记录线上规则匹配发生错乱最后花了一个多小时逐条核对清理。这个教训让我记住了选请求方法不能只看“能不能实现”一定要看业务的“重复提交容忍度”。更新类的操作能用PUT或PATCH就别用POSTPOST只留给创建和临时处理业务。如果后端接口已经定死了POST那一定要在前端做按钮防抖禁用并在后端加幂等校验双保险。4.7 排查HTTP方法问题的通用流程最后分享一套我自己用的排查流程。当你遇到和请求方法相关的接口报错时按下面这几步走第一步复现并保留现场。打开DevTools的Network面板不勾选“Disable cache”刷新页面找到请求失败的记录看请求行里的Method和URL。第二步用curl复现。把浏览器Request Headers里的关键字段Content-Type、Authorization抄到curl命令里脱离浏览器环境再发一次。如果curl成功而浏览器失败问题大概率在CORS或前端代码如果curl也失败问题就定位到后端。第三步看后端日志。特别是405和500类错误后端日志会告诉你请求有没有进入到业务处理逻辑。405通常连业务代码都没进在路由层就被拦了500则是进去了但代码崩了。第四步检查网关和中间层配置。Nginx的proxy_method、网关的请求改写、安全策略的HTTP方法白名单都可能改变请求方法的语义。测试环境里用curl -X OPTIONS -v打印一下响应头看看Allow字段和CORS字段是否正确。注意排查完一个问题建议顺手把“请求方法错误”这个类型记进团队的接口规范文档里。这个看似基础的问题实际在跨团队联调、新人入职时出现的频率特别高。5. 最后的几句实操心得HTTP请求方法这套规则说复杂不算复杂说简单也绝没那么简单。我个人的体会是真正容易出问题的地方往往不在方法本身而在“语义是否对齐”。前后端对“POST是创建还是也可以当更新用”的理解不一致就会出现各种奇奇怪怪的重复数据、覆盖丢失问题。所以我在每个项目设计接口的第一周就拉着前端同学过一遍方法语义清单把每个接口用什么方法、幂等不幂等、成功返回什么状态码都白纸黑字定下来。这个方法帮我少加了很多班。另外一个小技巧在本地调试时我习惯写完代码先抓一次包确认真实发出的Method、Headers、Body和自己预期一模一样再开始联调。很多人浪费了大量时间在“我以为请求是这样发的实际上不是”这种问题上抓包一看基本没有悬念。HTTP请求方法的本质就是用最规范的方式表达你的意图希望你也能在工作中把每一步都弄得清清楚楚。
返回列表