Postman接口调试实战指南-从HTTP请求到环境变量-Collection与自动化测试

目录

  • [Postman 接口调试实战指南:从 HTTP 请求到环境变量、Collection 与自动化测试](#Postman 接口调试实战指南:从 HTTP 请求到环境变量、Collection 与自动化测试)
    • 阅读路线:先定位问题,再完成一条链路
    • [1. Postman 到底解决什么问题?先把 API 调用拆出来](#1. Postman 到底解决什么问题?先把 API 调用拆出来)
    • [2. 一个 HTTP 请求到底由什么组成?](#2. 一个 HTTP 请求到底由什么组成?)
    • [3. GET、POST、PUT、PATCH、DELETE 到底怎么选?](#3. GET、POST、PUT、PATCH、DELETE 到底怎么选?)
    • [4. 第一个 GET 请求:先拿到一份可检查的响应](#4. 第一个 GET 请求:先拿到一份可检查的响应)
    • [5. Query Params 详解:Params 改的是最终 URL](#5. Query Params 详解:Params 改的是最终 URL)
      • [5.1 URL Encoding 为什么会改变参数含义?](#5.1 URL Encoding 为什么会改变参数含义?)
    • [6. Postman 怎么设置 Header?先分清 Content-Type 与 Accept](#6. Postman 怎么设置 Header?先分清 Content-Type 与 Accept)
    • [7. Postman 怎么发送 POST 请求和 JSON?](#7. Postman 怎么发送 POST 请求和 JSON?)
    • [8. form-data、x-www-form-urlencoded、raw、binary 有什么区别?](#8. form-data、x-www-form-urlencoded、raw、binary 有什么区别?)
    • [9. HTTP 状态码如何判断接口结果?200 还不够](#9. HTTP 状态码如何判断接口结果?200 还不够)
    • [10. Postman 怎么添加 Token?Authorization 面板和 Header 的关系](#10. Postman 怎么添加 Token?Authorization 面板和 Header 的关系)
    • [11. Postman 变量怎么用?先选范围,再处理同名覆盖](#11. Postman 变量怎么用?先选范围,再处理同名覆盖)
      • [11.1 新版 local value 不等于 Local scope](#11.1 新版 local value 不等于 Local scope)
      • [11.2 值类型与变量替换](#11.2 值类型与变量替换)
    • [12. Postman Environment 怎么用?只切环境,不改请求](#12. Postman Environment 怎么用?只切环境,不改请求)
    • [13. Collection 是什么?它保存的是可维护的 API 工作流](#13. Collection 是什么?它保存的是可维护的 API 工作流)
    • [14. Postman 如何保存登录 Token?失败时也要清掉旧值](#14. Postman 如何保存登录 Token?失败时也要清掉旧值)
    • [15. Postman 脚本基础:只学调试需要的几个入口](#15. Postman 脚本基础:只学调试需要的几个入口)
    • [16. Postman Tests 怎么写?把判断标准变成可重复的断言](#16. Postman Tests 怎么写?把判断标准变成可重复的断言)
      • [16.1 响应时间断言应来自接口要求](#16.1 响应时间断言应来自接口要求)
      • [16.2 错误测试也可以通过](#16.2 错误测试也可以通过)
    • [17. Postman 接口之间怎么传参数?把资源 ID 串进链路](#17. Postman 接口之间怎么传参数?把资源 ID 串进链路)
    • [18. Collection Runner 怎么用?把顺序、环境和结果一起检查](#18. Collection Runner 怎么用?把顺序、环境和结果一起检查)
      • [18.1 数据驱动运行的最小理解](#18.1 数据驱动运行的最小理解)
    • [19. Pre-request Script 有什么实际用途?准备值,不把逻辑堆成系统](#19. Pre-request Script 有什么实际用途?准备值,不把逻辑堆成系统)
    • [20. Cookies 与 Session:为什么登录后其他请求还能保持状态?](#20. Cookies 与 Session:为什么登录后其他请求还能保持状态?)
    • [21. Postman Console 怎么用?看实际请求,不只看编辑器](#21. Postman Console 怎么用?看实际请求,不只看编辑器)
    • [22. 完整案例:登录、用户 CRUD 与错误验证](#22. 完整案例:登录、用户 CRUD 与错误验证)
      • [22.1 文件结构与运行前提](#22.1 文件结构与运行前提)
      • [22.2 API 完整源码](#22.2 API 完整源码)
      • [22.3 创建虚拟环境并启动](#22.3 创建虚拟环境并启动)
      • [22.4 导入 Collection 与 Environment](#22.4 导入 Collection 与 Environment)
      • [22.5 接口契约与执行顺序](#22.5 接口契约与执行顺序)
      • [22.6 更新、错误、删除脚本](#22.6 更新、错误、删除脚本)
      • [22.7 Runner 执行与实际验证记录](#22.7 Runner 执行与实际验证记录)
      • [22.8 Test 环境与停止、清理](#22.8 Test 环境与停止、清理)
    • [23. Postman 请求失败怎么排查?先判断错误属于哪一层](#23. Postman 请求失败怎么排查?先判断错误属于哪一层)
      • [23.1 HTTP 错误:现象、原因、证据、处理](#23.1 HTTP 错误:现象、原因、证据、处理)
      • [23.2 Could not send request:没有响应时检查什么?](#23.2 Could not send request:没有响应时检查什么?)
      • [23.3 Environment 变量没有替换](#23.3 Environment 变量没有替换)
      • [23.4 Postman 成功,但程序请求失败](#23.4 Postman 成功,但程序请求失败)
    • [24. Postman 与 curl 如何配合?从交互调试走向脚本](#24. Postman 与 curl 如何配合?从交互调试走向脚本)
      • [24.1 同一个请求的 curl 表达](#24.1 同一个请求的 curl 表达)
      • [24.2 用 Newman 运行本文集合](#24.2 用 Newman 运行本文集合)
    • [25. Postman 不适合解决什么问题?](#25. Postman 不适合解决什么问题?)
    • [26. 建立一套可以继续使用的 API 调试工作流](#26. 建立一套可以继续使用的 API 调试工作流)
    • 官方资料与引用
    • 配图来源说明

Postman 接口调试实战指南:从 HTTP 请求到环境变量、Collection 与自动化测试

专栏 :工具|状态 :待发布稿|核对日期:2026-10-02。

适合已经接触 Web 开发、希望把 API 调用拆出来调试的学生和开发者。本文从请求本身开始,完成「登录 → 保存 Token → 创建用户 → 查询 → 更新 → 删除 → 验证错误处理」的完整闭环。

当前官方文档为 Postman v12,旧版截图和菜单名称可能不同,请以当前版本为准。文中的 Tests 指响应后的测试脚本,新界面主要入口为 Scripts → Post-response 。配套集合使用可导入的 Collection 2.1 JSON;它与当前 Native Git 的 Collection 3.0 YAML 格式有区别。169

阅读路线:先定位问题,再完成一条链路

先按问题定位,再按链路练习:

当前问题 对应章节
请求怎么配置?Params 和 Body 有什么区别? 2--8
收到 200、401、415,到底意味着什么? 9--10、23
换环境为什么还在请求旧服务器? 11--12、21
Postman 如何保存 Token、传递资源 ID? 14、17、22
Tests 怎么写、Collection 怎么批量执行? 15--19
要一个能真正运行、能清理数据的案例 22
Postman 成功,前端或脚本失败 20--21、23--24

本文中的公网演示只用于第一次 GET;完整案例由配套的本地 Flask + SQLite API 提供。公网接口发生变化时,后面的本地案例仍可独立运行。源码、环境文件、Collection 和验证记录见配套资源。

💡 第一次跟着做:先完成第 4 章的公网 GET,再到第 22 章启动本地 API、导入集合并跑通一轮。遇到不理解的配置,回到对应章节查原因。

✅ 这篇文章的完成标准:不只是收到一次 200,而是请求可保存、环境可切换、Token 和 ID 可传递、断言可重复、测试数据可清理。


1. Postman 到底解决什么问题?先把 API 调用拆出来

「本地正常」和「接口报错」可能说的是两个不同请求。先把请求对齐,再找原因。

前端说「接口返回 500」,后端说「我本地正常」。这两句话还不足以判断谁的问题,因为它们可能描述的是两个不同请求。

真正需要对齐的是:请求访问哪台服务器、用哪个 Method、Query 是否一致、是否携带 Token、Content-Type 是什么、Body 实际发送了什么,以及服务端在同一时刻记录了什么。

Postman 的核心作用是把程序中的 API 调用拆出来单独验证。 你可以先消除界面、组件状态和业务代码的干扰,再逐项改变请求,观察响应。确认接口行为后,把请求保存成 Collection;下一次联调时复用请求和断言,而不是靠聊天记录找参数。

一个可用的调试结果应包括四件事:可复现的请求、明确的预期、实际响应、足以定位问题的证据。例如「POST /api/users,JSON 中 username 为数字,预期 422,实际 500,并附同一时间的后端异常」,比「接口坏了」有用得多。

它适合前后端联调、认证排查、接口链路验证与基础回归。它不会替你确认数据库事务、浏览器页面状态或所有业务分支。本文会把工具用到可以继续维护的位置,边界放在第 25 章。

2. 一个 HTTP 请求到底由什么组成?

用一个结构示例 说明请求;这里的 /users?id=1001 不是让你直接请求的公网地址:

http 复制代码
GET /users?id=1001 HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer <token>

这是便于阅读的 HTTP/1.1 表达形式。Postman 实际协商的协议版本可能不同,但需要理解的信息仍然存在。

部分 例子 调试时要确认什么
URL https://api.example.com/users?id=1001 协议、主机、端口、路径是否指向目标服务
Method GET 当前路由是否接受这一方法
Query Parameters id=1001 参数名称、值、编码和重复参数的处理规则
Headers Accept: application/json 元信息、内容协商、认证等是否符合接口契约
Body JSON、表单、文件字节 发送了什么内容,用什么媒体类型表达
Authorization Bearer Token 等 请求的凭据如何进入 Header 或 Query

URL 的路径通常用于定位资源,Query 常用于筛选、分页和排序,Body 用于携带内容。这是常见 API 设计方式,不是 HTTP 强制规定的业务模型。

例如 GET /users?id=1001 与 POST /users 的 JSON {"id":1001},即使字段名相同,也不是同一请求:路由、方法、参数解析入口都可能不同。把 Body 中的字段填到 Params,不会自动成为后端读取的 JSON。

响应也要分开看:

http 复制代码
HTTP/1.1 200 OK
Content-Type: application/json

{"id":1001,"username":"xiaosu"}

Status Code 是 HTTP 结果;Response Headers 说明媒体类型、缓存、Cookie 等;Response Body 是返回内容。响应中的 JSON 不一定沿用请求 Body 的结构。

一次完整判断应从「有没有收到 HTTP 响应」开始,再看状态码、响应格式与业务内容。DNS 失败时没有 404;TCP 连接失败时没有 500。把网络错误误当业务错误,会让排查方向完全偏离。

3. GET、POST、PUT、PATCH、DELETE 到底怎么选?

方法的协议语义与具体接口契约要一起看。1011

Method 常见接口设计 示例 对调试的影响
GET 读取资源 查询用户、分页列表 通常用路径与 Query;不要为了传参随意给 GET 加 Body
POST 对目标资源执行处理,常用于创建 创建用户、登录、执行某操作 不能假定重复发送不会重复创建
PUT 用请求中的表示替换目标资源的表示 完整更新指定用户 必填字段、缺省字段行为要看契约
PATCH 应用部分修改 只修改 email 修改格式、是否支持某字段要看契约
DELETE 删除目标资源与当前功能之间的关联 删除指定用户 不代表所有历史数据都被物理删除

GET 是安全方法;PUT、DELETE 等具有幂等语义。幂等讨论的是重复同样请求的预期效果,不要求每次响应都一样。 同一 DELETE 第一次返回 204、第二次返回 404,可以仍然符合删除后的最终效果。

PATCH 不天然保证幂等。例如「把 email 改为固定值」与「余额增加 10」的重复效果不同。POST 也不一定用于新增,登录和触发计算都可能用 POST。服务器实现若违背方法语义,工具不会自动纠正它。

调试时先看接口说明,再检查后端路由;不要仅凭参数像「新增」就自行换成 POST。收到 405 时,优先检查 Method,并查看 Allow 响应头。

4. 第一个 GET 请求:先拿到一份可检查的响应

从官方入口下载 Postman,安装适合系统的桌面应用。本文的本地练习推荐桌面应用;如果使用 Web 应用,要选择已安装并运行的 Desktop Agent 。Cloud Agent 在 Postman 的云端发送请求,访问不到你电脑的 127.0.0.1;Browser Agent 还可能受到浏览器 CORS 限制。2

第一个公网请求使用 JSONPlaceholder,它是用于教学的公开模拟 REST API,无需在此例中添加认证。12

text 复制代码
GET https://jsonplaceholder.typicode.com/users/1

操作步骤:

  1. 创建一个 HTTP 请求,Method 选择 GET。
  2. 在地址栏粘贴完整地址。这个请求没有 Query,Params 保持空,Body 选择 none。
  3. Authorization 使用 No Auth,避免继承其他集合的 Token。
  4. 点击 Send,先看 Status,再看响应 Body 的 JSON。
  5. 保存请求为 Public Demo / Get User,使下一次能够直接复现。

创作时通过实际 HTTP 请求验证,此地址返回 200 ;返回内容中的 id 为 1、username 为 Bret。下面只摘出有解释价值的字段,不是完整响应:

json 复制代码
{
  "id": 1,
  "name": "Leanne Graham",
  "username": "Bret"
}

响应包含更多字段;不要把省略的字段当作接口不存在,也不要把公网服务可用性当作永久保证。

Postman 响应区信息 应怎样理解
Status 此请求的 HTTP 状态码与状态信息
Time 客户端记录的这次请求耗时,可能受网络、连接与服务处理影响;不是服务端 CPU 执行时间
Size 工具记录的响应大小;悬停或展开后看当前版本对 Body、Header 的划分,别把格式化后的屏幕字符数当传输字节数
Body 实际返回内容,可用 JSON 视图阅读,也应保留查看原始内容的能力
Headers 判断响应媒体类型、缓存、重定向、Cookie 等行为的依据

JSONPlaceholder 的创建、修改和删除响应是模拟结果,不会真正持久化资源。12 因此它适合练习发送请求,本文的创建后查询、删除后 404 验证使用第 22 章本地服务。

5. Query Params 详解:Params 改的是最终 URL

图 1|Postman 官方 v10 文档示例。重点看上方 URL 与 Params 的对应关系,以及下方响应区域;图中 Postman Echo 请求、耗时、大小和 Header 均为原图演示内容,不是本文 JSONPlaceholder 的实测结果。

💡 只看这一点 :勾选的 id 和 type 出现在 ?id=123&type=VIP 中。Params 是 URL 的编辑视图,不是 JSON Body。

结构示例:

text 复制代码
/users?page=2&limit=10

? 后是 Query;& 分隔参数;每项通常以 key=value 表达。在 Params 表中填入两行:

Key Value 本地案例含义
page 2 查询第 2 页
limit 10 每页最多 10 条

Postman 的 Params 与地址栏是同一个请求的两种编辑视图,修改一处会反映到另一处。3 取消参数行的勾选,表示不发送该参数;留下空 Value,可能仍发送 key=。缺省参数与空字符串参数不等价。 本地案例缺省 page 使用 1,page= 无法转成整数,会返回 400。

重复参数例如 tag=python&tag=api 可以有意义,但服务器可能读取全部值、第一项、最后一项,或者拒绝重复。不要把它擅自改为 tag=python,api,后者只是一个字符串,除非契约明确这样定义。

5.1 URL Encoding 为什么会改变参数含义?

如果参数值是 a+b@example.com,其中的 + 在一些表单式解码规则中会被当作空格,应使用 %2B 表示字面加号。值里含 & 时,未经正确编码可能被解析成另一个参数;中文通常以 UTF-8 字节对应的百分号编码表达。

不要编码整个 URL,否则 /、? 等结构字符也可能被处理。只编码需要编码的参数值,并避免把已经编码的 %2B 再编码成 %252B。

不要假定 Params 表替你完成了所有编码。 官方参数文档提醒需要时可选中文本使用 EncodeURIComponent;通用设置和版本也会影响请求处理。最终以 Console 中实际发出的 URL、服务器解析结果为准。3 这个检查比争论地址栏看起来像什么更可靠。

Query 不适合放密码或 Token:URL 可能进入代理、访问日志和历史记录。API Key 如果契约要求放 Query,也要评估相应暴露面。

6. Postman 怎么设置 Header?先分清 Content-Type 与 Accept

在 Headers 中增加 Key / Value,启用对应行,再检查是否与自动生成项冲突。

Header 描述什么 常见值与注意点
Content-Type 当前消息内容的媒体类型;在请求中描述请求体 application/json、application/x-www-form-urlencoded
Accept 客户端能够接受的响应媒体类型 application/json,不保证所有 API 都实现内容协商
Authorization 请求携带的认证凭据 Bearer <token>;通常交给 Authorization 面板生成
User-Agent 客户端信息 工具默认值通常够用;少数服务有特殊检查,不要把它当身份认证
http 复制代码
Content-Type: application/json
Accept: application/json

第一行表示「这份请求 Body 按 JSON 解释」;第二行表达「我接受 JSON 响应」。仅添加第一行,不会把任意文本变成合法 JSON;仅添加第二行,也不会把 HTML 错误页面自动变成 JSON。

Postman 会依据 Body、Authorization、Cookie 等设置生成某些 Header。发现重复或不一致时,要回到生成它的配置位置修改;手工添加 Content-Type 会优先于按 Body 自动生成的值。34

例如 raw 里选 JSON,却遗留一个启用的 Content-Type: text/plain,后端可能返回 415。把 Body 重新排版不会解决问题,因为错误在媒体类型。

请求中的 Content-Type 与响应中的 Content-Type 也要分开看。API 声称返回 JSON,但网关返回 text/html,这时 pm.response.json() 报错很可能是响应本身不符合预期。

7. Postman 怎么发送 POST 请求和 JSON?

第 22 章服务启动并登录后,用下面这个请求练习创建用户:

text 复制代码
POST {{base_url}}/api/users

图 2|Postman 官方文档旧版界面示例,原图用图书字段演示 JSON 输入。下面的 username、email 才是本文本地案例要提交的字段。

在 Body 选择 raw → JSON,填入:

json 复制代码
{
  "username": "xiaosu-demo",
  "email": "xiaosu@example.com"
}

Authorization 继承 Collection 的 Bearer 配置,Headers 检查 Content-Type: application/json。第一次成功会返回 201 和实际用户 ID;同名用户再次创建会返回 409,因此后面的自动链路会生成唯一用户名。

JSON 模式提供语法高亮及适当的 Content-Type,并支持相关编辑体验。3 它不会替你完成业务字段校验,也不会保证服务器接受你的结构。

错误 实际问题 本地案例结果
两个字段之间少逗号 JSON 无法解析 400
使用单引号或中文弯引号 JSON 字符串语法不合法 400
顶层写成数组,接口要求对象 JSON 合法,但结构不满足契约 422
"username": 123 值类型不符合接口定义 422
JSON 正确,Header 为 text/plain 请求媒体类型不被该接口支持 415
同名用户名重复提交 资源状态发生冲突 409

这些是配套 API 的明确行为;其他 API 可能采用不同错误码,不能把本表当所有框架的固定规则。

不要为了修复类型错误,把每个变量都套上引号。若接口要求数值,"age": "18" 与 "age": 18 不同。变量替换是文本替换,包含引号、换行的值需要正确 JSON 转义;第 19 章给出用 JSON.stringify() 构造 Body 的做法。

8. form-data、x-www-form-urlencoded、raw、binary 有什么区别?

Body 类型 HTTP 内容形态 适合场景 关键检查
raw JSON 一个 JSON 文档 结构化业务对象、嵌套字段和数组 JSON 语法、字段类型、application/json
x-www-form-urlencoded 编码后的键值串 表单提交、某些认证接口 字段名、编码规则、application/x-www-form-urlencoded
form-data 用 boundary 分隔的多个部分 同时提交文字字段和文件 每个 part 的字段名、文件类型与 boundary
binary 直接发送选中文件的字节 专门接收原始文件流的上传接口 服务端要求的媒体类型和文件内容

URL-encoded 内容可能类似:

text 复制代码
username=xiaosu&note=hello+world

它虽然使用类似 Query 的编码规则,但仍然放在请求 Body 中,不会自动出现在 URL。通常每个值是文本;嵌套对象的表达方式需要服务端另行约定。

multipart/form-data 内容包含多个 part,每部分可携带自己的 Content-Disposition、名称、文件名和媒体类型。boundary 用来区分这些部分,文件字节无需把整个文件塞进 JSON 字符串,因此适合同一请求传「描述 + 文件」。

图 3|Postman 官方 v9 文档示例。图中 profile 是原图的文件字段名,真实请求必须使用服务端约定的名称。每一行可以有不同类型,不是整个 Body 都切成文件。

在 Postman 的 form-data 中,把字段 file 的类型从 Text 切为 File,再选择本地文件;其他文字字段仍用 Text。让 Postman 自动生成包含 boundary 的 Content-Type,不要手写一个只有 multipart/form-data 的 Header。 Header 中的 boundary 必须与 Body 实际分隔符一致。

binary 只发文件内容,不替你附带 file=... 这样的字段包装;Postman 不自动为 binary 设置媒体类型,需要按契约补充。3 所以 form-data 和 binary 上传不能只靠切换一个标签互相替代。

本文本地 API 只接收 JSON,不包含文件上传路由。这里说明的是协议与工具设置;不要向 /api/users 上传文件并期待成功。对真实上传接口,还要检查文件大小限制、字段名,以及 Runner 所在机器是否能读取对应文件路径。

9. HTTP 状态码如何判断接口结果?200 还不够

状态码 常见含义 应继续检查什么
200 请求成功 返回结构、字段值、业务状态是否正确
201 已创建资源 新资源 ID、Location、能否随后读取
204 成功且没有响应内容 不要对它执行 JSON 解析
400 请求存在客户端方面的问题 Query、语法、路由要求的参数
401 缺少有效认证凭据 Token、挑战头、有效期、目标环境
403 服务理解请求但拒绝执行 权限或策略;不一定单纯是 Token 过期
404 找不到目标资源或服务不愿透露其存在 路径、资源 ID、环境、权限策略
409 与资源当前状态冲突 重复唯一字段、版本冲突、重复操作
422 内容类型与语法可理解,但内容无法按要求处理 字段类型、约束、对象结构
500 服务端遇到意外情况 后端异常、日志、触发请求
502 网关收到上游的无效响应 反向代理到后端的链路
503 当前无法处理请求,常见于过载或维护 服务状态、Retry-After、依赖可用性

这些含义以 HTTP 规范为依据;每个 API 如何映射具体业务错误,还要看契约。10 401 的标准语义涉及认证挑战,配套 API 会返回 WWW-Authenticate: Bearer realm="postman-demo"。

某些系统始终返回 HTTP 200,却把失败放在业务字段:

json 复制代码
{
  "code": 5001,
  "message": "invalid token"
}

这是业务响应示例 ,不是本文服务的响应。遇到这类 API,要同时断言 HTTP 状态和契约规定的业务成功码,不能对所有系统统一写 code === 0。

还有一种误判:Postman 自动跟随重定向,最终拿到登录页面的 200。检查响应 Content-Type、Body 和 Console 中的跳转记录,才能知道是否真的调用到了 API。

10. Postman 怎么添加 Token?Authorization 面板和 Header 的关系

Bearer Token 的请求形式:

http 复制代码
Authorization: Bearer <token>

在 Authorization 选择 Bearer Token,Token 栏填 {``{access_token}}。只填 Token 值,不要再输入 Bearer 前缀;工具会生成对应 Header。5

将 Bearer 配置放在 Collection 层,其他受保护请求选择 Inherit auth from parent。登录与健康检查显式设置 No Auth,避免无关的旧 Token 被发送出去。若同时在 Headers 手工填写 Authorization,容易造成重复值或覆盖,优先检查实际发送的 Header。

认证方式 Postman 的配置 需要理解的边界
Bearer Token Token 栏填写值或变量 持有 Token 即可使用它;不代表 Token 一定是 JWT
Basic Auth Username / Password 通常以 Base64 形式进入 Authorization;Base64 不是加密,应通过 HTTPS 传输
API Key Key、Value 和添加位置 可放 Header 或 Query,名称由 API 契约决定;常用来标识调用方

本文 Token 是服务端签发并验证的带时间戳签名凭据,不是 JWT,也不是完整 OAuth2 方案。客户端应把它当字符串使用,不依赖某个解码后的字段来判断有效性。

生产服务使用 HTTPS,真实 Token、密码、Cookie 不放进博客截图、GitHub、共享环境文件或测试报告。本文 xiaosu / demo-pass 是专门写在本地教学服务中的演示账号,不是任何真实系统的凭据。

11. Postman 变量怎么用?先选范围,再处理同名覆盖

💡 变量失效时的第一步 :先检查选中的 Environment,再查同名变量是否被更窄的作用域覆盖。不要先把所有 {``{base_url}} 改回固定地址。

把几十个请求写成 http://127.0.0.1:8000/api/...,换端口时就要逐个修改。改用:

text 复制代码
{{base_url}}/api/users

只需在选中的环境里调整 base_url。变量不仅用于地址,也可用于 Header、Body、Token 和资源 ID。

Scope 可见范围与适用场景 脚本入口
Global 当前工作区的多个集合可访问,适合临时公共值 pm.globals
Collection 同一 Collection 内复用,不随切换环境自动改变 pm.collectionVariables
Environment 当前选中的环境,适合地址和该环境的认证信息 pm.environment
Data Runner 使用 CSV / JSON 数据时的当前迭代数据 pm.iterationData
Local 当前请求或集合运行中的临时值,运行结束后不持久保留 pm.variables

同名变量的优先级,从高到低为 Local → Data → Environment → Collection → Global。7 常见陷阱是环境中 base_url 已修改,却被脚本中的 Local 同名值覆盖。

javascript 复制代码
// 按优先级读取当前真正会被采用的值
const effective = pm.variables.get("base_url");
// 只读取当前环境中的值,用于比较是否发生覆盖
const environmentValue = pm.environment.get("base_url");
console.log("base_url", { effective, environmentValue });

这两次读取目的不同:前者模拟变量解析,后者定位环境配置。如果它们不一致,继续检查 Data、Local 等来源,不要反复修改环境表。

11.1 新版 local value 不等于 Local scope

需要区分两个维度:

  • Environment / Collection / Global scope 描述变量在哪些请求中可见。
  • local value / shared value 描述值是否只在本机实例使用,或已明确共享给协作者、云端运行。

一个 Environment 变量也可以拥有本地值;它不会因此变成 Local scope。

当前官方文档说明:变量值默认在本地,主动 Share 才共享;旧版可独立维护的 Initial / Current 双值方式已发生变化,VS Code 扩展等入口仍可能不同。8 因此看到旧截图,不要机械要求自己必须填两列。

普通本地值也不是天然的保密保险:导出、日志和分享动作仍要检查。当前版本提供 Secure variables / Postman Vault 等敏感值管理能力,团队使用时按对应运行端的支持情况配置;本文可导入文件中只放演示账号和空 Token。

11.2 值类型与变量替换

变量用于存储和替换时,应按字符串处理;资源 ID 可显式 String(id) 保存,断言时再 Number(...)。对象先 JSON.stringify(),读取后 JSON.parse(),不要期待对象直接插入 Body 后自动成为合法 JSON。7

请求中没有解析的 {``{user_id}},不会变成「请自动查询一个用户」。优先检查变量名称、范围和是否有值。名字区分大小写,access_token 与 accessToken 是两个不同变量。

12. Postman Environment 怎么用?只切环境,不改请求

为完整案例创建两个环境,或直接导入配套文件:

变量 Development Test 为什么这样安排
base_url http://127.0.0.1:8000 http://127.0.0.1:8001 同一组请求访问两套本地进程
login_username xiaosu xiaosu 仅为本地教学账号
login_password demo-pass demo-pass 明确可公开的演示值
access_token 空 空 登录后写入,环境间不混用
user_id 空 空 本轮创建成功后写入
demo_username / demo_email 空 空 自动生成本轮资源数据

操作方法:进入 Environments,新建 Development,添加上述键值;再建立 Test。返回请求编辑区,用环境选择器明确选择 Development,悬停 {``{base_url}} 或打开变量面板检查解析后的值。8

保存环境与选中环境是两件事。创建了 Development 但仍处于 No environment,不能期待它自动生效。

真实项目中 Test 可以是团队提供的测试地址,但 https://test.example.com 只是文档占位,不是可用接口。本篇选择真实可启动的 8001 本地实例,避免让读者向不存在的域名发送请求。配套 Collection 还会检查目标只允许本地 8000 或 8001,防止教学删除请求被误指向其他服务。

切环境后要重新登录:不同服务的 Token、Cookie、数据 ID 不一定通用。只有改 base_url,而保留来自另一台服务的 access_token,是联调中常见的 401 原因。

13. Collection 是什么?它保存的是可维护的 API 工作流

Collection 不只是请求文件夹。它能保存请求配置、说明、认证继承、变量与脚本,Runner 再按顺序把它们执行成一个流程。

实际项目可以按 Auth、Users、Orders、Files 等模块分组,但本文没有订单、上传和刷新 Token 接口,不会为了目录好看虚构这些请求。

配套集合的结构为:

text 复制代码
Postman 实战指南 - Local Demo API
├── 00 Setup
│   ├── 01 Health
│   └── 02 Login
├── 01 Identity
│   └── 03 Me
├── 02 Users
│   ├── 04 Create User
│   ├── 05 Get User
│   ├── 06 List Users
│   ├── 07 Patch User
│   └── 08 Put User
├── 03 Negative
│   └── 09--14:重复、缺少认证、权限、JSON、媒体类型、字段类型
└── 04 Cleanup
    ├── 15 Delete User
    └── 16 Confirm Deleted

模块分组回答「请求属于哪里」;编号回答「本案例应按什么顺序执行」。几十个平铺请求容易混淆登录前提、数据来源与清理步骤。命名不要只写 GET 1、POST 2,要让别人知道请求用途和预期。

在 Collection 的 Authorization 设置 Bearer Token;请求继承它。公共配置集中维护,但状态断言留在具体请求中:创建预期 201、删除预期 204、错误测试预期 4xx,不适合在集合层对所有请求断言 200。

Collection 导出后可以进入 Git 版本管理。当前文档的 Collection 3.0 使用多文件 YAML,Native Git 工作流采用这个格式;本文的 2.1 JSON 是为可导入及 Newman 复现做出的明确选择。9 不要把格式版本当成软件版本,也不要让 Newman 直接运行 3.0 文件。

14. Postman 如何保存登录 Token?失败时也要清掉旧值

⚠️ Token 接力最容易遗漏的一步:登录失败要清掉旧 Token。否则后续请求可能拿着上一次的凭据成功,造成「这次登录通过了」的错觉。

登录请求:

text 复制代码
POST {{base_url}}/api/login

Authorization 选择 No Auth,Body 用 raw JSON:

json 复制代码
{
  "username": "{{login_username}}",
  "password": "{{login_password}}"
}

成功响应结构如下。<本次签发的值> 是脱敏占位,不能拿它调用接口:

json 复制代码
{
  "access_token": "<本次签发的值>",
  "token_type": "Bearer",
  "expires_in": 3600
}

先在 Login 的 Scripts → Pre-request 清理本次链路可能误用的旧数据:

javascript 复制代码
["access_token", "user_id", "demo_username", "demo_email"].forEach(function (key) {
    pm.environment.unset(key);
});

这里逐个移除旧 Token 和资源变量。重新登录失败时,后续请求不应悄悄使用上一轮的认证信息。

在 Scripts → Post-response 放入完整脚本:

javascript 复制代码
let body;
pm.test("登录返回 200 和有效 token", function () {
    pm.response.to.have.status(200);
    body = pm.response.json();
    pm.expect(body.access_token).to.be.a("string").and.not.empty;
    pm.expect(body.token_type).to.equal("Bearer");
    pm.expect(body.expires_in).to.equal(3600);
});
if (pm.response.code === 200 && body && typeof body.access_token === "string" && body.access_token.length > 0 && body.token_type === "Bearer" && body.expires_in === 3600) {
    pm.environment.set("access_token", body.access_token);
} else {
    pm.environment.unset("access_token");
    pm.execution.setNextRequest(null);
    throw new Error("登录失败,停止链路并清除旧 token");
}

逐步理解这段脚本:

  1. let body 声明变量,使解析结果可在断言与保存逻辑中共用。
  2. pm.test(...) 注册有名字的测试,失败会显示在 Test Results,而不是只在 Console 打印一句错误。
  3. pm.response.to.have.status(200) 先验证状态,避免把失败响应当登录结果。
  4. pm.response.json() 把响应文本解析为对象;如果服务器返回 HTML,此处会明确暴露问题。
  5. pm.expect(...) 校验 Token 非空、类型为 Bearer、有效期符合本例约定。它不是对所有认证 API 的统一假设。
  6. if 再检查保存条件。断言失败不会自动阻止后面的普通 JavaScript,所以不能在失败后无条件 set。
  7. pm.environment.set(...) 写入当前环境,其他请求用 {``{access_token}} 复用。
  8. 失败分支清除旧值,pm.execution.setNextRequest(null) 在集合运行中结束当前链路,再抛出错误让原因可见。

setNextRequest 用于 Runner 或命令行集合执行;单击 Send 不会自动帮你发送下一个请求。6 手工调试时,依旧要确认登录测试通过,再打开其他请求。

这比单独写 pm.environment.set("access_token", pm.response.json().access_token) 稍长,但解决了真正危险的边缘情况:失败响应没有字段,旧 Token 却继续让其他接口看起来「正常」。

15. Postman 脚本基础:只学调试需要的几个入口

脚本运行在 Postman Sandbox,不等同于浏览器页面或任意系统终端。不要假定可以直接访问 DOM、运行 Python 或随意读取本机文件。1

任务 示例 放在哪里
读状态码 pm.response.code Post-response
解析 JSON pm.response.json() Post-response,且响应确实有 JSON
读取响应原文 pm.response.text() Post-response,用于空响应或格式排查
读实际采用的变量 pm.variables.get("base_url") 两类脚本均可
读环境变量 pm.environment.get("user_id") 两类脚本均可
保存环境变量 pm.environment.set("user_id", "1") 获取结果后的 Post-response
删除旧值 pm.environment.unset("user_id") 登录或创建前、失败时
定义断言 pm.test("名称", function () { ... }) 通常 Post-response

console.log 是定位问题的日志,pm.test 是可记录的验证结果。打印了「status=200」不能替代断言。

脚本可放在 Collection、Folder、Request 层。公共准备动作放上层,接口特有字段检查放请求层;执行顺序通常是 Collection → Folder → Request。不要在集合公共脚本里解析所有响应为 JSON:204 无内容,下载接口可能是二进制,错误网关可能是 HTML。1

当 pm.response.json() 报错时,先看响应原文和 Content-Type,再判断是不是脚本写错。把 JSON 解析套进空 catch,虽能让红色错误消失,却可能让测试漏报。

16. Postman Tests 怎么写?把判断标准变成可重复的断言

图 4|Postman 官方 v9 文档示例。旧版入口叫 Tests,当前版本通常在 Scripts → Post-response。原图故意对同一 200 响应分别断言 404 和 200,因此一个失败、一个通过;这里用来解释断言结果,不是本文案例的运行截图。

✅ 看结果的方法:HTTP 200 与「测试通过」是两件事。先读失败断言的名称和 expected / actual,再判断是接口行为错了还是预期写错了。

查询 /api/me 的成功响应应包含:

json 复制代码
{
  "id": 1001,
  "username": "xiaosu",
  "role": "developer"
}

最小断言组合:

javascript 复制代码
pm.test("HTTP 200", function () {
    pm.response.to.have.status(200);
});

pm.test("当前账号与角色正确", function () {
    const body = pm.response.json();
    pm.expect(body).to.have.property("id", 1001);
    pm.expect(body.username).to.equal("xiaosu");
    pm.expect(body.role).to.equal("developer");
});

第一条检查 HTTP 层结果;第二条检查返回了需要的账号,而不只是「某个 JSON」。字段存在、字段类型、字段值是不同标准;创建接口的 ID 会变化,应该断言是正整数,并与后续请求使用的变量一致。

断言写在 pm.test 回调里,失败会以测试名称汇总。不要只返回 false 或 console.log("失败"),用 pm.expect 或响应断言表达条件。

16.1 响应时间断言应来自接口要求

若项目有明确响应预算,可设置环境变量 response_budget_ms,再添加可选脚本:

javascript 复制代码
const rawBudget = pm.environment.get("response_budget_ms");
if (rawBudget !== undefined && rawBudget !== "") {
    const budget = Number(rawBudget);
    if (!Number.isFinite(budget) || budget <= 0) {
        throw new Error("response_budget_ms 必须是正数");
    }
    pm.test("满足本环境配置的响应预算", function () {
        pm.expect(pm.response.responseTime).to.be.below(budget);
    });
}

读取配置、转为数值、验证配置、比较耗时,四步各有目的。本文不设置一个无依据的「200 ms 合格线」,配套集合也没有这个断言。单次客户端耗时受网络和运行端影响,不能由此推出压测容量或生产可用性。

16.2 错误测试也可以通过

测试缺少 Token 的请求,应设 No Auth,预期 401:

javascript 复制代码
pm.test("缺少认证时返回 401", function () {
    pm.response.to.have.status(401);
    pm.expect(pm.response.json().code).to.equal("unauthorized");
});

收到 401 是这条负向测试的成功结果。反过来,如果它意外返回 200,才应失败,因为认证约束失效了。

17. Postman 接口之间怎么传参数?把资源 ID 串进链路

创建用户返回真实 ID 后,后续请求应使用该 ID,而不是每次写死 1。

步骤 请求 变量如何流动
创建 POST /api/users 校验成功后把 id 保存为 user_id
查询 GET /api/users/{``{user_id}} 校验返回 id 与本轮变量相同
修改 PATCH /api/users/{``{user_id}} 断言 email 改变而 username 保留
完整更新 PUT /api/users/{``{user_id}} 提交完整 username 与 email
删除 DELETE /api/users/{``{user_id}} 断言 204 和空内容
再查询 GET 同一地址 断言 404,确认删除效果

创建请求的 Pre-request 准备唯一数据:

javascript 复制代码
pm.environment.unset("user_id");
const unique = pm.variables.replaceIn("{{$guid}}");
pm.environment.set("demo_username", "postman-" + unique);
pm.environment.set("demo_email", "postman-" + unique + "@example.com");

先清理旧 ID,再求值动态 GUID,最后写入本轮用户名和 email。replaceIn 会把动态变量变成实际字符串;只把 {``{$guid}} 原样保存在其他变量中,不应假定会按你期待的次数展开。

创建 Body:

json 复制代码
{
  "username": "{{demo_username}}",
  "email": "{{demo_email}}"
}

创建请求的 Post-response:

javascript 复制代码
let body;
pm.test("创建返回 201 和资源字段", function () {
    pm.response.to.have.status(201);
    body = pm.response.json();
    pm.expect(body.id).to.be.a("number");
    pm.expect(Number.isInteger(body.id) && body.id > 0).to.equal(true);
    pm.expect(body.username).to.equal(pm.environment.get("demo_username"));
    pm.expect(body.email).to.equal(pm.environment.get("demo_email"));
    pm.expect(pm.response.headers.get("Location")).to.equal("/api/users/" + body.id);
});
if (pm.response.code === 201 && body && Number.isInteger(body.id) && body.id > 0 && body.username === pm.environment.get("demo_username") && body.email === pm.environment.get("demo_email") && pm.response.headers.get("Location") === "/api/users/" + body.id) {
    pm.environment.set("user_id", String(body.id));
} else {
    pm.environment.unset("user_id");
    pm.execution.setNextRequest(null);
    throw new Error("创建失败,停止链路,避免操作旧资源");
}

它除状态与类型外,还检查回传 username、email、Location,避免把一个无关成功响应里的 ID 当成本轮资源。只有满足本例条件才写入 user_id;失败时清除 ID 并停止链路。

查询请求的 Post-response:

javascript 复制代码
pm.test("HTTP 200", function () { pm.response.to.have.status(200); });
pm.test("读取本轮创建的用户", function () {
    const body = pm.response.json();
    pm.expect(body.id).to.equal(Number(pm.environment.get("user_id")));
    pm.expect(body.username).to.equal(pm.environment.get("demo_username"));
    pm.expect(body.email).to.equal(pm.environment.get("demo_email"));
});

断言会把环境中的字符串 ID 转成数值后比较。PATCH 只发送 {"email":"changed@example.com"},然后验证 username 保持原值;这比只断言 200 更能证明部分更新的行为正确。

请求链路更接近实际调用依赖,但仍不是完整业务覆盖。它只证明当前数据、当前环境和当前断言下的行为;并发竞争、不同用户权限、数据库异常还需要额外案例。

清理也应可审查:只删除当前创建得到的 ID,不写「删除所有用户」请求。如果中途失败没走到 Cleanup,记录本轮 ID,用同一个 DELETE 清理;不要为了让下一轮通过而清空真实业务库。

18. Collection Runner 怎么用?把顺序、环境和结果一起检查

打开已保存的 Collection,使用 Run 进入 Runner,选择 Functional 执行;界面随版本可能变化。6

第一次运行:

  1. 选择 Development 环境,确认本地服务已启动。
  2. 检查 01--16 全部请求勾选,保持目录与编号顺序。
  3. Iterations 设为 1;先不引入数据文件和自定义跳转。
  4. 检查错误停止、变量保存与 Cookie 选项,再开始运行。
  5. 在结果中逐个查看请求状态、Test Results、失败名称和具体响应。

图 5|Postman 官方 v10 文档示例。点击单个请求可以查看实际 URL、Header、Body 和响应,排查时不要只看顶部通过数。图中 example.com 是原图占位内容,本文要运行第 22 章的本地集合。

Keep variable values 控制运行中更新的变量是否在结束后保留,更新的是本地值,而不是自动共享值。6 不启用它,并不妨碍同一轮内部 Token / ID 传递,只是结束后不一定写回编辑器环境。

一个请求显示红色 4xx,并不必然表示回归失败;本例 Negative 文件夹正是断言这些错误响应。判断依据是失败断言、脚本异常、发送错误及接口契约,不能只数「多少个 200」。

断言失败本身也不保证停止后面的请求。Runner 的错误停止选项与脚本异常、网络发送错误有关,本文在登录和创建失败时显式结束链路。6 中途退出不保证 Cleanup 会执行,所以必须保留按 ID 清理的方法。

18.1 数据驱动运行的最小理解

Runner 可使用 JSON / CSV 为每个迭代提供数据。例如在另一个仅用于数据校验的集合中,用两组输入验证成功与 422:

json 复制代码
[
  {"username": "case-a", "email": "a@example.com", "expected_status": 201},
  {"username": 123, "email": "b@example.com", "expected_status": 422}
]

通过 pm.iterationData.get("username") 读取当前行,构建 JSON Body;用 Number(pm.iterationData.get("expected_status")) 做断言。Data 范围比 Environment 更窄,要注意同名覆盖。

这份数据仅说明 Runner 的迭代机制,不是给本文 16 请求链路直接导入的文件。完整链路会自行生成数据,并包含清理步骤;要改成数据驱动,应同时设计不同预期、唯一字段和失败后清理,不能只给 CSV 增加几行就称为完整测试框架。

19. Pre-request Script 有什么实际用途?准备值,不把逻辑堆成系统

它在请求发送前执行,适合生成时间戳、唯一标识、准备 Body 或签名需要的输入。13

javascript 复制代码
pm.variables.set("request_timestamp", String(Date.now()));

这行写入毫秒时间戳;随后可以在接口明确支持的 Header 或 Body 中引用 {``{request_timestamp}}。若后端要求秒,应该使用 Math.floor(Date.now() / 1000)。把秒、毫秒混用,常让签名验证或有效期判断失败。

本文自动创建用户使用 GUID,解决重复运行时唯一字段冲突。对于字符串可能含引号、换行的 Body,可以用完整 JSON 序列化:

javascript 复制代码
const input = {
    username: pm.environment.get("demo_username"),
    email: pm.environment.get("demo_email")
};
pm.variables.set("request_body", JSON.stringify(input));

然后把 raw JSON 的整个内容写为 {``{request_body}},不要再给整个变量套引号。对象构造明确字段,stringify 处理 JSON 转义,Local 变量只用于当前运行。

签名请求要严格按服务端约定:参与签名的方法、路径、Query 排序、Body 字节、时间戳单位和算法都可能影响结果。没有协议说明,不能写一个「通用 MD5」脚本并声称适用所有 API。

签名逻辑变长、有多个版本或要跨客户端复用时,应把算法与测试放进可版本管理的代码中,Postman 只调用受支持的准备方式。本文不包含签名 API,也不向配套服务添加无意义签名参数。

20. Cookies 与 Session:为什么登录后其他请求还能保持状态?

Session 认证常见流程是:服务端登录后返回 Set-Cookie,客户端保存 Cookie,后续匹配域名、路径等规则的请求携带 Cookie;服务端据此查找会话。Session 数据可以在服务器上,Cookie 只是会话标识,不能把两者混成一个对象。

Postman 有 Cookie 管理器,可从请求附近的 Cookies 入口查看对应域名的值;收到 Set-Cookie 后,后续请求可以自动使用匹配的 Cookie。14

容易被忽略的规则 对测试的影响
Domain / 主机 localhost 与 127.0.0.1 的 Cookie 不应假定通用
Path 为 /admin 设置的 Cookie 不一定发送到 /api/users
Secure 通常要求 HTTPS;使用 HTTP 时要检查是否实际发送
到期时间 历史 Cookie 可能已失效,也可能让你意外保持登录
HttpOnly 主要限制浏览器脚本读取,不代表 HTTP 客户端永远无法携带

旧 Cookie 可能掩盖认证问题:你删除 Authorization 后仍返回 200,并不能马上证明接口无需认证,它可能通过 Session 识别了你。做未登录测试时,应检查并清理目标域名 Cookie,或使用 Runner 的不读取已存 Cookie 选项。

Postman 的 Cookie 行为不完整复制浏览器:官方文档列出了对 SameSite、Cookie 前缀等的支持边界。14 所以 Postman 登录成功不能替代浏览器的跨站 Cookie 验证,更不能证明 CSRF 防护已有效。

本文本地案例采用 Bearer Token,不设置 Session Cookie。 这让 Token 断言更清楚;Session 项目可以沿用查看、隔离 Cookie 的方法,但应以它自己的登录接口契约为准。

21. Postman Console 怎么用?看实际请求,不只看编辑器

使用应用中的 Console 入口打开 Postman Console;若位置改变,可在当前应用命令或菜单中查找。它能展示请求与响应、网络信息,以及脚本输出。15

具体错误:Development 表里 base_url 为 8000,你却一直命中 8001。不要先怀疑缓存。

  1. 打开 Console,清空无关日志,然后只发送一次有问题的请求。
  2. 展开这一条请求,查看实际 URL 是 8000 还是 8001。
  3. 检查启用的环境,并比较第 11 章的 pm.variables.get 与 pm.environment.get。
  4. 查找 Pre-request 脚本是否设置 Local 同名 base_url,或数据文件是否有同名字段。
  5. 移除覆盖后重新发送,确认实际 URL 改变,再检查服务端访问日志。

另一个高频错误是 Authorization: Bearer Bearer ...:Token 栏误填了前缀。Console 中实际 Header 能直接说明原因,返回 401 本身只能提示认证失败。

还可以用 Console 追踪重定向、检查自动 Header、看到未解析变量或脚本 JSONError。若服务器返回 HTML,先确认是不是代理或登录页面,再决定改 API 还是脚本。

日志中可能有 Token、密码和用户数据;排查时先缩小到一个请求,对外分享前脱敏。诊断脚本尽量只打印地址、字段类型、状态或是否有 Token,不要打印完整凭据。

22. 完整案例:登录、用户 CRUD 与错误验证

🧪 本节的操作顺序:启动 API → 导入两个 JSON 文件 → 选择 Development → 运行完整 Collection → 检查断言和清理结果。环境与集合要一起检查,只有导入成功还不算完成。

这一章把前面的概念连成能执行的项目。它不是企业生产案例,而是为了学习接口联调专门编写、实际验证的本地教学 API。

22.1 文件结构与运行前提

下载配套目录后,主要文件如下:

text 复制代码
postman-guide-94/
├── app.py
├── requirements.txt
├── Demo-API.postman_collection.json
├── Development.postman_environment.json
├── Test.postman_environment.json
├── README.md
├── VERIFICATION.md
└── 发布说明.md

需要 Python 环境;本文验证使用 Python 3.12 与 Flask 3.1.2。SQLite 来自 Python 标准库,不需要独立数据库服务。Postman 桌面应用用于交互;Node.js 与 Newman 仅在选择命令行验证时需要。

requirements.txt:

text 复制代码
Flask==3.1.2

固定直接依赖便于复现实验;这不是包含全部传递依赖的锁文件。团队项目还应管理完整依赖和更新流程。

22.2 API 完整源码

app.py:

python 复制代码
"""Local teaching API. Do not expose this development server to the Internet."""
import os
import secrets
import sqlite3
from functools import wraps
from pathlib import Path

from flask import Flask, g, jsonify, request
from itsdangerous import BadSignature, SignatureExpired, URLSafeTimedSerializer
from werkzeug.exceptions import BadRequest, HTTPException

app = Flask(__name__)
# A fresh key invalidates old tokens after a server restart.
signer = URLSafeTimedSerializer(secrets.token_hex(32), salt="postman-demo")
DB_PATH = Path(os.environ.get("DEMO_DB", Path(__file__).with_name("demo.sqlite3")))
with sqlite3.connect(DB_PATH) as db:
    db.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, email TEXT NOT NULL)")


def get_db():
    if "db" not in g:
        g.db = sqlite3.connect(DB_PATH)
        g.db.row_factory = sqlite3.Row
    return g.db


@app.teardown_appcontext
def close_db(_error):
    db = g.pop("db", None)
    if db is not None:
        db.close()


def error(code, message, status):
    response = jsonify(code=code, message=message)
    response.status_code = status
    if status == 401:
        response.headers["WWW-Authenticate"] = 'Bearer realm="postman-demo"'
    return response


def read_json():
    if not request.is_json:
        return None, error("unsupported_media_type", "Use application/json", 415)
    try:
        data = request.get_json()
    except BadRequest:
        return None, error("invalid_json", "Malformed JSON", 400)
    if not isinstance(data, dict):
        return None, error("invalid_fields", "JSON body must be an object", 422)
    return data, None


def require_token(func):
    @wraps(func)
    def wrapped(*args, **kwargs):
        parts = request.headers.get("Authorization", "").split()
        if len(parts) != 2 or parts[0].lower() != "bearer":
            return error("unauthorized", "Missing Bearer token", 401)
        try:
            payload = signer.loads(parts[1], max_age=3600)
        except (BadSignature, SignatureExpired):
            return error("unauthorized", "Invalid or expired token", 401)
        if payload != {"sub": "xiaosu", "role": "developer"}:
            return error("unauthorized", "Invalid token subject", 401)
        g.identity = payload
        return func(*args, **kwargs)
    return wrapped


@app.get("/health")
def health():
    return jsonify(status="ok")


@app.post("/api/login")
def login():
    data, failure = read_json()
    if failure is not None:
        return failure
    if data.get("username") != "xiaosu" or data.get("password") != "demo-pass":
        return error("unauthorized", "Invalid demo credentials", 401)
    token = signer.dumps({"sub": "xiaosu", "role": "developer"})
    return jsonify(access_token=token, token_type="Bearer", expires_in=3600)


@app.get("/api/me")
@require_token
def me():
    return jsonify(id=1001, username=g.identity["sub"], role=g.identity["role"])


@app.get("/api/admin")
@require_token
def admin():
    return error("forbidden", "This demo account has no admin permission", 403)


def validate_user(data, partial=False):
    allowed = {"username", "email"}
    if set(data) - allowed or not data or (not partial and set(data) != allowed):
        return "Provide username and email; PATCH permits a subset"
    for key, value in data.items():
        if not isinstance(value, str) or not value.strip() or len(value) > 200:
            return f"{key} must be a nonempty string of at most 200 characters"
    if "email" in data and "@" not in data["email"]:
        return "email must contain @ (demo validation only)"
    return None


@app.route("/api/users", methods=["GET", "POST"])
@require_token
def users():
    db = get_db()
    if request.method == "GET":
        try:
            page = int(request.args.get("page", "1"))
            limit = int(request.args.get("limit", "10"))
            if page < 1 or not 1 <= limit <= 100:
                raise ValueError
        except ValueError:
            return error("invalid_query", "page >= 1; 1 <= limit <= 100", 400)
        rows = db.execute("SELECT * FROM users ORDER BY id LIMIT ? OFFSET ?", (limit, (page - 1) * limit)).fetchall()
        return jsonify(items=[dict(row) for row in rows], page=page, limit=limit)
    data, failure = read_json()
    if failure is not None:
        return failure
    message = validate_user(data)
    if message:
        return error("invalid_fields", message, 422)
    try:
        with db:
            cursor = db.execute("INSERT INTO users (username, email) VALUES (?, ?)", (data["username"], data["email"]))
    except sqlite3.IntegrityError:
        return error("username_exists", "username already exists", 409)
    response = jsonify(id=cursor.lastrowid, **data)
    response.status_code = 201
    response.headers["Location"] = f"/api/users/{cursor.lastrowid}"
    return response


@app.route("/api/users/<int:user_id>", methods=["GET", "PUT", "PATCH", "DELETE"])
@require_token
def user(user_id):
    db = get_db()
    row = db.execute("SELECT * FROM users WHERE id = ?", (user_id,)).fetchone()
    if row is None:
        return error("not_found", "User does not exist", 404)
    if request.method == "GET":
        return jsonify(dict(row))
    if request.method == "DELETE":
        with db:
            db.execute("DELETE FROM users WHERE id = ?", (user_id,))
        return "", 204
    data, failure = read_json()
    if failure is not None:
        return failure
    message = validate_user(data, partial=request.method == "PATCH")
    if message:
        return error("invalid_fields", message, 422)
    updated = {**dict(row), **data}
    try:
        with db:
            db.execute("UPDATE users SET username = ?, email = ? WHERE id = ?", (updated["username"], updated["email"], user_id))
    except sqlite3.IntegrityError:
        return error("username_exists", "username already exists", 409)
    return jsonify(updated)


@app.errorhandler(HTTPException)
def http_error(exc):
    response = exc.get_response()  # Preserve headers such as Allow on 405.
    response.data = app.json.dumps({"code": "http_error", "message": exc.description})
    response.content_type = "application/json"
    return response


if __name__ == "__main__":
    app.run(host="127.0.0.1", port=int(os.environ.get("DEMO_PORT", "8000")), debug=False)

配置与实现中的关键取舍:

  • 127.0.0.1 只接受本机连接,适合本文联调。它不是生产发布配置,开发服务器也不用于生产。
  • debug=False 避免把调试器当作接口启动的前提。排查错误看终端日志,不开启面向公网的调试器。
  • SQLite 的 users 表保存资源,username UNIQUE 让重复测试能稳定得到 409;SQL 值使用参数绑定,不通过拼接用户输入构造语句。
  • Token 有签名和时间戳,服务端验证 3600 秒有效期。签名密钥在进程启动时随机生成,重启后旧 Token 失效,需重新登录。
  • 固定演示账号、简单 email 检查、不区分多用户资源权限,都是教学范围;不能改一下监听地址就拿去生产。
  • JSON 解析分别处理媒体类型、语法与字段约束,让 415、400、422 各有可复现触发条件。
  • DELETE 返回空内容与 204;不能为了「统一格式」再给它塞一份 JSON。
  • HTTP 错误处理保留原有响应头,例如 405 的 Allow,再把 Body 改为 JSON;正常端点与错误端点更便于统一观察。

22.3 创建虚拟环境并启动

在 postman-guide-94 目录执行。Windows PowerShell:

powershell 复制代码
py -3 -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r requirements.txt
.\.venv\Scripts\python.exe app.py

第一行创建隔离的 Python 环境;第二行通过这个环境的解释器 安装指定依赖;第三行启动本地服务。若未安装 Windows 的 py 启动器,可用已确认指向目标 Python 的 python 替换第一行的 py -3。

这些命令不依赖激活脚本,因此无需为了执行 Activate.ps1 改全局执行策略。启动后终端保持打开,服务地址为 http://127.0.0.1:8000。

macOS / Linux / WSL:

bash 复制代码
python3 -m venv .venv
.venv/bin/python -m pip install -r requirements.txt
.venv/bin/python app.py

三行作用相同,解释器路径不同。初次练习若 Postman 在 Windows,优先把服务也启动在 Windows,先减少跨 WSL 网络的变量;跨环境访问再按实际监听与网络模式排查。

检查健康接口,Windows 使用 curl.exe,避免旧版 Windows PowerShell 把 curl 当成其他命令别名:

powershell 复制代码
curl.exe -i http://127.0.0.1:8000/health

-i 同时显示状态行和响应头,URL 指向本地健康路由。预期 200,Body 为 {"status":"ok"}。macOS / Linux 可用相同参数的 curl -i ...。

22.4 导入 Collection 与 Environment

在 Postman 的 Import 入口选择:

  • Demo-API.postman_collection.json
  • Development.postman_environment.json
  • Test.postman_environment.json

确认导入集合包含 16 个请求,选择 Development。打开 02 Login,检查 No Auth 与 JSON Body,再按第 14 章手工发送;Token 写入成功后发送 03 Me。

集合的公共 Pre-request 还会检查实际解析后的目标地址:

javascript 复制代码
const base = pm.variables.get("base_url");
const allowed = /^http:\/\/127\.0\.0\.1:(8000|8001)$/.test(base || "");
if (!allowed) {
    pm.test("目标为允许的本地服务", function () { pm.expect(allowed).to.equal(true); });
    pm.execution.setNextRequest(null);
    pm.execution.skipRequest();
}

正则只接受两套指定的本地地址。目标不符时先记录失败断言,再结束集合链路,skipRequest() 阻止当前请求发送;不能仅靠 throw 就假定当前 HTTP 请求一定被跳过。该方法放在 Pre-request,不能移到 Post-response。19 这是教学集合的目标限制,迁移到团队项目时应按明确的测试环境修改,而不是直接删除检查。

导入的是 Collection 2.1 JSON,不是要求你把 Postman 降到某个版本。当前 Native Git 的 3.0 文件体系与 Newman 兼容性见第 13、24 章。9

22.5 接口契约与执行顺序

全部路径都相对于选中环境的 base_url。登录和健康检查 No Auth;其余请求默认继承集合 Bearer,Missing Token 请求再显式设 No Auth。

序号 Method / 路径 Body 或 Params 预期 必须验证的内容
01 GET /health 无 200 status=ok
02 POST /api/login 演示 username / password 200 Token、Bearer、3600;保存 access_token
03 GET /api/me 无 200 id=1001、username=xiaosu、role=developer
04 POST /api/users 本轮 demo_username / demo_email 201 正整数 ID、回传值、Location;保存 user_id
05 GET /api/users/{``{user_id}} 无 200 ID 与本轮 username / email
06 GET /api/users page=1、limit=10 200 items 数组和分页字段
07 PATCH /api/users/{``{user_id}} email=changed@example.com 200 email 改变、username 未变
08 PUT /api/users/{``{user_id}} 完整本轮 username / email 200 完整更新结果
09 POST /api/users 再提交相同 username 409 code=username_exists
10 GET /api/me No Auth 401 code=unauthorized
11 GET /api/admin 有效普通账号 Token 403 code=forbidden
12 POST /api/users 故意破坏 JSON 400 code=invalid_json
13 POST /api/users Header 为 text/plain 415 code=unsupported_media_type
14 POST /api/users username 为数字 422 code=invalid_fields
15 DELETE /api/users/{``{user_id}} 无 204 响应原文为空
16 GET /api/users/{``{user_id}} 查询已删除 ID 404 code=not_found

分页接口只保证返回本页数组,数据库可能有手工创建的用户,不把 items.length === 1 当不变条件。本例不会为了测试方便删除他人记录。

响应关系示例,用 N 表示服务端此次分配的正整数:

请求 响应 Body 示例 下一步
创建成功 {"id":N,"username":"postman-<guid>","email":"postman-<guid>@example.com"} 保存 N 到 user_id
PATCH 成功 {"id":N,"username":"postman-<guid>","email":"changed@example.com"} 校验字段并继续 PUT
重复创建 {"code":"username_exists","message":"username already exists"} 该负向测试通过后继续
删除成功 空内容 对同一个 N 再发 GET
删除后查询 {"code":"not_found","message":"User does not exist"} 断言 404 与业务错误码

表中 <guid> 和 N 是说明变量,不是可以直接粘贴的 JSON。真实返回以每次运行的 ID、GUID 为准。

22.6 更新、错误、删除脚本

PATCH Body:

json 复制代码
{
  "email": "changed@example.com"
}

Post-response:

javascript 复制代码
pm.test("HTTP 200", function () { pm.response.to.have.status(200); });
pm.test("PATCH 只更新 email", function () {
    const body = pm.response.json();
    pm.expect(body.id).to.equal(Number(pm.environment.get("user_id")));
    pm.expect(body.username).to.equal(pm.environment.get("demo_username"));
    pm.expect(body.email).to.equal("changed@example.com");
});

先检查 200,再验证 ID、保留的 username 和变化的 email。PUT Body 则重新提交完整的 demo_username、demo_email,脚本断言完整返回值。

以 Wrong Content Type 的脚本为例:

javascript 复制代码
pm.test("HTTP 415", function () {
    pm.response.to.have.status(415);
});
pm.test("错误码 unsupported_media_type", function () {
    pm.expect(pm.response.json().code).to.equal("unsupported_media_type");
});

请求 Body 是 JSON 形状的文本,但 Header 明确设为 text/plain,用它证明「内容看着像 JSON」与「媒体类型被服务端接受」不是一回事。

Delete User:

javascript 复制代码
pm.test("HTTP 204", function () {
    pm.response.to.have.status(204);
});
pm.test("204 没有响应体", function () {
    pm.expect(pm.response.text()).to.equal("");
});

Confirm Deleted:

javascript 复制代码
pm.test("HTTP 404", function () {
    pm.response.to.have.status(404);
});
pm.test("资源确实不存在", function () {
    pm.expect(pm.response.json().code).to.equal("not_found");
});

这里保留 user_id,才能在删除后验证同一个资源;不要在 DELETE 成功后立刻清除它,使下一条 GET 失去目标。下一轮 Login 会清理旧值。

全部请求、Header 与脚本已保存在配套 Collection,不需要从正文猜测省略的部分。

22.7 Runner 执行与实际验证记录

先执行一轮,检查 16 个请求、30 条断言。通过后再设 Iterations=3,检查重复运行不会因同名用户名冲突而失败。

本次实际执行的是 Newman 6.2.1 的 2.1 Collection,在 Linux 环境连接同机 Flask API;并非伪造 Postman GUI 截图或桌面 Runner 操作记录。

检查项 实际结果
公开 GET /users/1 HTTP 200,id=1、username=Bret
官方 Collection 2.1 Schema 校验 通过
三轮集合执行 48 个 HTTP 请求,90 条断言,0 失败
各错误场景 分别收到预期 409 / 401 / 403 / 400 / 415 / 422 / 404
执行后的教学数据库 users 表剩余记录数为 0
错误密码 + 预设旧 Token 非零退出,仅执行 Health 和 Login,没有继续用户操作
base_url 指向未允许的本地端口 非零退出,接收端收到 0 次 HTTP 请求

这些结果只证明配套案例和指定断言通过;Windows 安装、桌面应用 UI、Web Agent、Cookie 与证书场景未在本环境中实测。请按本节步骤在自己的目标机器验收,详细边界见验证记录。

22.8 Test 环境与停止、清理

要验证切环境,另开一个终端,进入同一项目目录,给第二个进程指定不同端口与数据库。

PowerShell:

powershell 复制代码
$env:DEMO_PORT = "8001"
$env:DEMO_DB = "test.sqlite3"
.\.venv\Scripts\python.exe app.py

前两行只配置当前终端进程环境;第三行启动 8001 的独立服务。Postman 选择 Test,再从 Login 开始运行。它与 Development 使用不同进程签名密钥和数据库,旧 Token / ID 不应跨环境使用。

Bash:

bash 复制代码
DEMO_PORT=8001 DEMO_DB=test.sqlite3 .venv/bin/python app.py

两个变量只对这一条启动命令生效。本文实际自动验证使用 Development,第二实例的目标机器操作步骤供读者验收。

停止服务:在各自运行终端按 Ctrl+C。正常集合执行已删除其创建资源;如果手工创建或中途失败,先查看本轮 ID,用已登录的 DELETE 请求清理。

仅在确认需要重置本文教学数据库、且相关进程已经停止时,在项目目录删除对应文件:

powershell 复制代码
Remove-Item -LiteralPath .\demo.sqlite3

该命令永久删除本地教学用户数据;不要替换成业务数据库路径。Test 数据库若需要重置,单独确认 test.sqlite3 后再处理。下次启动会重建空表。不是每次运行都需要重置,也不要把清库当作修复接口问题的常规步骤。

23. Postman 请求失败怎么排查?先判断错误属于哪一层

「刚才还好好的」不是排查证据。保留这次请求和响应,从发生变化的地方开始查。

💡 先确认收到没有:没有 HTTP 响应,查连接、DNS、代理和证书;已经收到响应,再查状态码、接口契约与后端日志。

排查顺序通常是:发送端与网络 → 目标地址与路由 → 媒体类型与语法 → 认证权限 → 字段与业务约束 → 服务端异常 → 断言脚本。
#mermaid-svg-FmrSQVNP0qu04WJq{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-FmrSQVNP0qu04WJq .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-FmrSQVNP0qu04WJq .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-FmrSQVNP0qu04WJq .error-icon{fill:#552222;}#mermaid-svg-FmrSQVNP0qu04WJq .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-FmrSQVNP0qu04WJq .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-FmrSQVNP0qu04WJq .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-FmrSQVNP0qu04WJq .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-FmrSQVNP0qu04WJq .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-FmrSQVNP0qu04WJq .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-FmrSQVNP0qu04WJq .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-FmrSQVNP0qu04WJq .marker{fill:#333333;stroke:#333333;}#mermaid-svg-FmrSQVNP0qu04WJq .marker.cross{stroke:#333333;}#mermaid-svg-FmrSQVNP0qu04WJq svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-FmrSQVNP0qu04WJq p{margin:0;}#mermaid-svg-FmrSQVNP0qu04WJq .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-FmrSQVNP0qu04WJq .cluster-label text{fill:#333;}#mermaid-svg-FmrSQVNP0qu04WJq .cluster-label span{color:#333;}#mermaid-svg-FmrSQVNP0qu04WJq .cluster-label span p{background-color:transparent;}#mermaid-svg-FmrSQVNP0qu04WJq .label text,#mermaid-svg-FmrSQVNP0qu04WJq span{fill:#333;color:#333;}#mermaid-svg-FmrSQVNP0qu04WJq .node rect,#mermaid-svg-FmrSQVNP0qu04WJq .node circle,#mermaid-svg-FmrSQVNP0qu04WJq .node ellipse,#mermaid-svg-FmrSQVNP0qu04WJq .node polygon,#mermaid-svg-FmrSQVNP0qu04WJq .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-FmrSQVNP0qu04WJq .rough-node .label text,#mermaid-svg-FmrSQVNP0qu04WJq .node .label text,#mermaid-svg-FmrSQVNP0qu04WJq .image-shape .label,#mermaid-svg-FmrSQVNP0qu04WJq .icon-shape .label{text-anchor:middle;}#mermaid-svg-FmrSQVNP0qu04WJq .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-FmrSQVNP0qu04WJq .rough-node .label,#mermaid-svg-FmrSQVNP0qu04WJq .node .label,#mermaid-svg-FmrSQVNP0qu04WJq .image-shape .label,#mermaid-svg-FmrSQVNP0qu04WJq .icon-shape .label{text-align:center;}#mermaid-svg-FmrSQVNP0qu04WJq .node.clickable{cursor:pointer;}#mermaid-svg-FmrSQVNP0qu04WJq .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-FmrSQVNP0qu04WJq .arrowheadPath{fill:#333333;}#mermaid-svg-FmrSQVNP0qu04WJq .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-FmrSQVNP0qu04WJq .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-FmrSQVNP0qu04WJq .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FmrSQVNP0qu04WJq .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-FmrSQVNP0qu04WJq .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FmrSQVNP0qu04WJq .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-FmrSQVNP0qu04WJq .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-FmrSQVNP0qu04WJq .cluster text{fill:#333;}#mermaid-svg-FmrSQVNP0qu04WJq .cluster span{color:#333;}#mermaid-svg-FmrSQVNP0qu04WJq div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-FmrSQVNP0qu04WJq .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-FmrSQVNP0qu04WJq rect.text{fill:none;stroke-width:0;}#mermaid-svg-FmrSQVNP0qu04WJq .icon-shape,#mermaid-svg-FmrSQVNP0qu04WJq .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FmrSQVNP0qu04WJq .icon-shape p,#mermaid-svg-FmrSQVNP0qu04WJq .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-FmrSQVNP0qu04WJq .icon-shape .label rect,#mermaid-svg-FmrSQVNP0qu04WJq .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FmrSQVNP0qu04WJq .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-FmrSQVNP0qu04WJq .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-FmrSQVNP0qu04WJq :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
否
是
发送失败或断言失败
收到 HTTP 响应?
检查 Agent、连接、DNS、代理、TLS
响应符合接口契约?
对齐 URL、参数、认证与后端日志
检查断言、变量类型与执行顺序

图中先分「没有响应」与「响应不符」,可避免收到 ECONNREFUSED 却去改 JSON,或者 415 时反复刷新 Token。

23.1 HTTP 错误:现象、原因、证据、处理

现象 可能原因 要检查的证据 对应处理
404 Not Found base_url 指错;路径大小写、前缀错误;ID 不存在 Console 最终 URL、环境解析值、服务端路由和访问日志 对齐实际服务前缀;确认资源确实创建,使用本轮 ID
400 Bad Request Query 非法、JSON 语法错误、缺少契约要求的参数 响应错误信息、raw Body、Query 实际值 修复语法或参数;不要随意删除服务端必须字段
401 Unauthorized 未发送 Token、双 Bearer、过期、环境不匹配 实际 Authorization 是否存在,登录响应、有效期、WWW-Authenticate 重新登录当前环境,清理旧变量,修正前缀与认证继承
403 Forbidden 角色不足、IP 或其他策略拒绝 有效身份、权限策略、响应 code 使用允许操作的账号或调整授权;不能只靠反复登录
415 Unsupported Media Type JSON 路由收到 text/plain、表单类型不符、multipart boundary 有误 实际 Content-Type 与 Body 类型 去除冲突手工 Header,按接口契约选择 Body
409 Conflict 唯一字段重复、资源版本或当前状态冲突 当前资源、返回业务码、重复请求历史 用唯一测试数据,或按契约处理状态冲突
422 Unprocessable Content JSON 可解析,但结构、类型、字段约束错误 字段验证详情、请求中的实际类型 改正类型与字段;不能只格式化 JSON
500 Internal Server Error 后端异常、依赖异常或未处理的输入 同一请求的 Body、响应、后端堆栈、时间或 request ID 复现最小请求并定位服务端异常;不能直接认定是 Postman 问题
502 / 503 网关到上游异常、维护或依赖暂不可用 代理日志、上游健康、Retry-After 检查服务链路;对有副作用操作不要无条件自动重试

401 表示没有有效认证凭据;403 表示理解请求但拒绝执行。常见项目把它们分别用于未登录与无权限,但实际权限策略也可能用 404 隐藏资源,不能仅凭状态码推断所有内部细节。

对于 500,客户端发送异常输入可能触发缺陷,但服务端也应按契约处理它。保存最小复现、对齐日志后再修改实现,不要把所有 500 都归因于客户端,也不要把它当作无需排查的后端专属问题。

23.2 Could not send request:没有响应时检查什么?

错误或场景 检查方法 解决方向
ECONNREFUSED / 连接被拒绝 看启动终端;用 curl.exe 请求健康路由 启动服务、修正地址和端口、检查监听位置
ETIMEDOUT / 超时 判断连接阶段还是等待响应;对照服务端日志 排查防火墙、网络、代理与后端阻塞,再调整有依据的超时
ENOTFOUND / DNS 失败 检查域名是否拼错;用系统 DNS 工具解析 修正域名或网络 DNS,变量中的协议和主机也要检查
Web 应用访问不到 localhost 查看所选 Agent 改用桌面应用或运行 Desktop Agent;云端 localhost 不是你的电脑
HTTPS 证书验证失败 检查主机名、证书链、有效期、本机时间 配置可信 CA;需要 mTLS 的接口添加客户端证书
代理导致本地请求失败 检查 Postman 代理设置与环境代理 根据实际组织策略排除本地地址,或使用正确代理配置
8000 被其他进程占用 看启动异常、系统端口占用与进程 关闭确认无用的进程,或协调端口;别随意结束未知系统进程

Windows 可用以下命令检查端口:

powershell 复制代码
Get-NetTCPConnection -LocalPort 8000 -State Listen

它查询本机 8000 的监听连接;输出中的 OwningProcess 帮助找到对应进程。这个检查只能说明端口监听,不能证明监听者就是本文 API,所以还要请求 /health。

本地服务启动但访问失败时,先让 curl 与 Postman 使用相同地址。不要把 URL 的 http 写成 https,也不要把 Docker 中的 localhost、WSL 中的 localhost、远端云 Agent 的 localhost 一概视为同一机器。

证书排查不以「长期关闭 SSL verification」为解决方案。若为了隔离问题临时比较验证开关,确认原因后恢复验证,修复信任链或证书配置;生产请求仍应正确验证身份。16

23.3 Environment 变量没有替换

按以下顺序检查:选中环境 → 名字完全一致 → 变量启用且有本地值 → 同名覆盖 → 脚本是否真正运行 → Console 最终 URL。

登录保存变量之前若 pm.response.json() 已抛错,后面的 set 不会正常完成。若 Runner 内部成功、结束后看不到值,检查 Keep variable values;这不等于链路内部没有传参。

user_id="" 与变量完全不存在都无法定位有效资源。不要通过写死 ID 掩盖创建失败;重新从 Login / Create 执行并检查各自断言。

23.4 Postman 成功,但程序请求失败

最有效的对比不是「两个 URL 看起来一样」,而是逐项比较实际请求:

对比项 典型差异
Method / 路径 / Query 程序使用另一方法、多了前缀、重复编码
Headers Content-Type 不同、漏 Token、默认 Header 覆盖
Body 字节和类型 对象被转成表单;数字被转为字符串;空值不同
Cookie Postman 保留了历史会话,程序是未登录状态
Token 与运行环境 使用不同账号、已过期 Token、请求了另一服务
重定向、代理、TLS 两个客户端默认策略不同
浏览器约束 CORS、预检、SameSite、credentials 等与桌面请求不同

先把 Postman 生成的 curl 在程序所在机器执行,再用服务器日志或受控抓包比较两次请求。分享前删除凭据。

如果只有浏览器失败,检查浏览器 Network 中的 OPTIONS 预检、响应允许来源、允许 Header 与凭据策略。桌面 Postman 不受浏览器 CORS 规则限制,Postman 成功不证明前端跨域配置正确。2

24. Postman 与 curl 如何配合?从交互调试走向脚本

Postman 适合边改边看,curl 适合文档、终端、服务器和脚本。工具之间的价值在于交叉验证:确认失败来自请求配置,还是某个客户端运行环境。

24.1 同一个请求的 curl 表达

健康检查,Windows PowerShell:

powershell 复制代码
curl.exe -i http://127.0.0.1:8000/health

登录时,避免 shell 对内嵌 JSON 的引号处理差异,先创建一个 UTF-8 的 login.json:

json 复制代码
{
  "username": "xiaosu",
  "password": "demo-pass"
}

再执行一行命令:

powershell 复制代码
curl.exe -i -X POST http://127.0.0.1:8000/api/login -H "Content-Type: application/json" --data-binary "@login.json"

-X POST 指定方法;-H 设置请求媒体类型;--data-binary @文件 从文件发送请求内容,避免手工拼接引号;-i 展示 Header。macOS / Linux 使用 curl 替换 curl.exe,其余这组参数含义相同。

查询用户信息需要 Token。下面是语法示例,必须把占位值替换为自己本地本次登录的返回值:

powershell 复制代码
curl.exe -i http://127.0.0.1:8000/api/me -H "Authorization: Bearer <本次登录返回的Token>"

真实环境不要把凭据写入可分享的命令历史或日志,使用团队允许的秘密注入方式。Postman 的代码生成入口也能生成 curl,但复制后仍需核对变量是否展开、认证是否包含,以及当前终端的引号和换行语法。17

24.2 用 Newman 运行本文集合

已安装 Node.js / npm 的读者,在配套项目目录执行:

bash 复制代码
npm install --save-dev newman@6.2.1
npx newman run Demo-API.postman_collection.json -e Development.postman_environment.json -n 3

第一行安装本篇实测的 Newman 版本作为当前项目开发依赖;会产生 npm 项目依赖文件。第二行执行已保存集合,-e 指定环境 JSON,-n 3 运行三轮。本地 API 必须保持启动。其用途是实际 HTTP 与脚本验证,不是重新安装 Postman。

验证失败时关注退出码和具体断言,CI 不要忽略非零退出。不要用 --suppress-exit-code 把失败包装成成功,也不要生成含真实 Token 的报告后直接上传。

版本边界:Newman 支持这里的 2.1 JSON,不支持 Postman v12 Native Git 的 3.0 集合。使用 3.0 时按官方文档采用 Postman CLI,不要把重命名扩展名当成格式转换。918

本文没有把命令行执行宣传成完整 CI/CD;要进入团队流水线,还需要隔离环境、秘密管理、测试数据生命周期和报告策略。官方 Postman CLI 也是后续选择,不要求初学者在本篇同时配置两套命令行工具。

25. Postman 不适合解决什么问题?

问题 为什么本文工作流不足以验证
完整性能容量测试 三轮功能请求无法说明并发负载、吞吐或容量;即使工具提供性能功能,也需独立设计负载和指标
完整端到端测试 API 响应不覆盖浏览器页面、操作系统交互和所有外部系统状态
后端单元测试 不能替代对函数、异常分支、领域逻辑的隔离测试
浏览器自动化 不会证明点击、渲染、CORS 和真实浏览器 Cookie 行为都正常
生产监控系统 一次 Runner 成功不能证明持续可用,也不能替代日志、指标和告警体系

这些边界不意味着工具完全没有相关能力,而是不能把本篇的接口调试和基础回归直接等同于所有测试层级。

如果只需偶尔验证一个无认证 GET,curl 或已有客户端可能已经够用。需要反复联调、管理多个环境、传递 Token 和 ID、保存断言时,Collection 才体现长期价值。

26. 建立一套可以继续使用的 API 调试工作流

先明确 HTTP 请求与接口契约,建立可复现请求;再用 Environment 管理目标地址与认证信息,用 Collection 保存结构与公共配置,用 Post-response 把判断条件写成断言。

最重要的实践闭环是:登录取得本环境 Token,创建得到本轮 ID,使用同一个 ID 查询与修改,最后删除并确认不存在。遇到错误时,从真实发送内容和服务器证据出发,而不是只看按钮或状态码颜色。

完成本篇后,再按项目需要学习 OpenAPI 契约、CI 中集合执行、测试数据隔离与更完整的后端测试。先把这套小工作流跑稳定,比一次配置所有工具更有价值。

官方资料与引用

以下资料在 2026-10-02 核对。Postman 菜单、变量机制、格式与使用额度可能变化;本文没有承诺固定套餐额度。引用用于核对功能和协议,源码与案例为本文配套教学实现。

  1. Postman:Post-response 与测试脚本
  2. Postman:Agent 与本地访问
  3. Postman:参数与 Body
  4. Postman:请求 Header
  5. Postman:Authorization 类型
  6. Postman:Collection Runner
  7. Postman:变量范围与优先级
  8. Postman:变量定义与新版值模型
  9. Postman:Collection Schema 与 3.0 / Newman 边界
  10. RFC 9110:HTTP Semantics
  11. RFC 5789:PATCH Method
  12. JSONPlaceholder:公开 API 与模拟写入说明
  13. Postman:Pre-request Script
  14. Postman:Cookie 管理
  15. Postman:Console 与请求排查
  16. Postman:证书配置
  17. Postman:生成客户端代码
  18. Postman:Newman 命令行与格式兼容性
  19. Postman:请求跳过与集合执行控制

配套入口:源码与导入文件|实际验证与目标机器验收

配图来源说明

本文使用 5 张 Postman 官方文档操作截图、1 张官方学习插画和 2 张 Twemoji 表情。截图保留原始内容,并在各图旁注明旧版界面及演示边界;

相关推荐
小此方1 小时前
Linux网络(十九):TCP流量控制与滑动窗口详解,超时重传和快重传到底有什么区别
linux·运维·服务器·网络·网络协议·tcp/ip
吴声子夜歌2 小时前
Nginx应用与运维——Nginx HTTP模块详解(访问控制功能模块)
运维·nginx·http
网络小江2 小时前
组网第二课:X.25 与帧中继——第一次让带宽“拼车“
网络协议
通信瓦工3 小时前
利用浊度和电导率测量确定乙二醇基流体的质量
网络·数据库·ai
阿钱真强道3 小时前
33 嵌入式操作系统 | 模块七验收:端到端一键验收 + 项目答辩
网络·网络协议·粘包·回归测试·uloop
ggaofeng3 小时前
Ctrl+C结束进程是如何实现的
服务器·c语言·网络
和裕3 小时前
年度框架直供 vs 零散按需采购:定制纸箱采购成本、交付与服务核心区别全对比
大数据·运维·网络·人工智能·算法
Galeoto4 小时前
如何判断路由器的网络情况
网络
尹人入圣5 小时前
市面上IP驱动产业新场景新工具
运维·网络·python·tcp/ip