ARTICLE DETAIL

资讯详情

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

调试时如何优雅取消HTTP请求:断点、AbortController与超时链路

调试时如何优雅取消HTTP请求:断点、AbortController与超时链路 1. 断点停住那一刻请求到底卡在哪一环群里隔三差五就有人问同一个问题我在 IDE 里给接口打了断点Postman 那边一直转圈我直接把 Postman 关掉、或者把浏览器标签页叉掉这算不算把这次 http请求中断了每次看到这个问题我都想先说一句你得先搞清楚你要中断的是哪一层的东西。因为 debug 这件事一次请求会被拆成好几段彼此独立的悬空状态你只在其中一段上按了退出键另外几段很可能还在原地挂着甚至已经把活儿干完了。这也是为什么很多人觉得我明明取消了怎么数据库里还是多了一条记录。这一篇我不打算讲什么高大上的理论就把我自己这几年在排查接口、调前端、调嵌入式设备时踩过的坑摊开来讲。核心就一件事当一次 HTTP 请求正停在断点上、或者正被你的调试器捏在手里的时候你要用什么手段、在哪个层面把它干脆利落地放弃掉并且保证这个放弃不会给你留下后遗症。适合后端、前端、测试以及要跟硬件设备打交道的同学哪怕你只是偶尔用 Postman 调一下接口看完也能少踩几个坑。1.1 一次请求在调试期会被拆成三段彼此不知情的状态很多人脑子里的请求是一个整体发出去、等回来就这么简单。但在调试场景里它其实是三条平行线客户端的一侧、网络中间层的一侧、服务端的一侧。你在服务端打断点只有第三条线被按了暂停键第一条和第二条线根本不知道发生了什么。客户端那侧的状态是TCP 连接已经建立ESTABLISHED请求字节已经全部写进了内核发送缓冲区并被对端确认应用层正在阻塞等待响应。这个时候它唯一在做的事就是等等一个可能永远不会来的响应。中间层那侧的状态要看你的部署形态。如果前面挂了反向代理那代理此时也开着一个等待上游响应的计时器。以常见的 Nginx 为例proxy_read_timeout默认是 60 秒意思是如果上游 60 秒没吐数据代理就会主动断掉这条连接并给客户端返回 504。也就是说代理是有自己的脾气的它不会无限等你。服务端那侧的状态最复杂一个工作线程停在断点上这个线程可能还持有数据库连接、可能还持有一个没提交的事务、可能还握着一把分布式锁。它占用的一切资源在断点释放之前都不会归还。所在层卡住时的具体状态常见默认超时谁最容易先放弃客户端连接已建立阻塞读等待响应多数框架无超时或很长手动取消才会断反向代理等待上游首字节proxy_read_timeout60s代理自己超时应用服务端线程停在断点持有事务与连接取决于线程池/连接池得靠你手动放行数据库/下游事务未提交行锁未释放innodb_lock_wait_timeout50s锁等待超时看清这张表你就明白了你关掉 Postman 只影响了第一行后面三行该怎么挂着还怎么挂着。真正需要你处理的往往是第三行。1.2 关掉浏览器和关掉调试器差别比想象中大先说关掉客户端会发生什么。浏览器叉掉标签页时内核会替你把这条 TCP 连接关掉通常是发一个 FIN 走正常四次挥手情况紧急时可能是 RST。这个动作对服务端来说是对端已断开的信号。但如果你的线程正停在断点上服务端根本没在监听这个信号——它没在读 socket信号在内核里躺着等线程恢复执行、尝试写响应的时候才会发现哦写不出去了然后抛一个 broken pipe 或者 connection reset by peer。关键在于业务逻辑可能已经在你单步的过程中执行完了。你断在orderService.create()的第一行按了十几下 F8走到最后一行才意识到这单不该下然后你关掉 Postman。这时候数据库里那条记录已经在事务里了等线程一恢复、事务一提交它就是真实存在的。客户端取消对服务端来说只是响应写不回去不是这件事没发生。再说关闭调试器。这里要分清两种动作一种是 Continue / Resume把断点放行让代码继续跑完另一种是 Terminate / Stop直接杀掉被调试进程。前者是我允许这次请求正常结束后者是这次请求连同整个进程一起没了。杀进程是最彻底的取消因为它连事务和连接一锅端了但代价是进程里其他正在处理的请求也一起陪葬线上绝对不能这么干本地开发环境倒是很常见——我本地调试时经常就是断点看够了直接点红色方块。还有一种是 IDE 里的 Disconnect仅仅断开调试器与进程之间的连接进程本身还在跑。这个动作对业务代码几乎是透明的请求会继续执行完。很多人以为点了 Disconnect 就等于放弃了这次请求其实只是我不看了。1.3 中断这个词在这些场景里至少有三层含义热词里冒出来一堆串口中断DMA 空闲中断CAN 总线中断STM32 进不去中断跟 HTTP 请求的中断搅在一起看着乱其实是同一个词干了三件不同的事我把它们区分一下后面就不容易混淆了。第一层是中止、取消abort / cancel。指的是主动放弃一个正在进行中的异步操作让它不再等待结果。HTTP 请求中断、Promise 取消、任务取消都属这一类。它的特点是协作式——需要参与者配合客户端愿意停服务端也得愿意检查。第二层是硬件中断interrupt。CPU 收到外设的电平信号后暂停当前指令流跳去执行中断服务函数干完再回来。串口收到一个字节触发接收中断、DMA 搬完一帧数据触发传输完成中断、看门狗溢出触发复位都是这一类。它是抢占式的说打断就打断不跟你商量。第三层是调试暂停break / suspend。调试器让目标线程停在断点上本质上是调试器在指令流里插入了一个陷阱指令。IDEA 工具栏上那个暂停按钮、gdb 里的 CtrlC都是让程序停下来跟前面两层没有半点关系。搞清楚这三层之后你会发现一个有意思的类比硬件中断是外部事件抢占 CPUHTTP 请求取消是调用方通知执行方别干了。前者是硬件层面的强制后者是软件层面的协商。很多人调嵌入式设备的时候用 HTTP 上报数据一按调试暂停整个上报链路就乱了本质就是第二层和第三层打架。2. 客户端主动放弃从 AbortController 到各语言 SDK 的取消入口如果你想要我点了取消这次请求就真的不再占用资源正确的做法是在客户端就把它掐掉而不是等它自己超时。现代浏览器和绝大多数主流语言的 HTTP 客户端都提供了显式的取消入口只是散落在不同的 API 名字下面。这一章把这些入口统一拎出来对比一下。2.1 浏览器与 Node 场景AbortController 是通用钥匙从 fetch 进入标准库开始AbortController 就成了前端取消请求的事实标准。它的用法是创建一个控制器把它的 signal 交给请求想取消的时候调用 abort()。const controller new AbortController(); // 把 signal 传给请求 const p fetch(/api/order/create, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify(payload), signal: controller.signal, }); // 用户切走了页面、或者点了取消按钮 window.addEventListener(beforeunload, () controller.abort()); try { const res await p; return await res.json(); } catch (err) { // 必须区分主动取消和真的出错 if (err.name AbortError) { console.log(请求已被主动放弃不计入错误监控); return null; } throw err; }这段代码里有两个细节值得强调是我踩过坑才记住的。第一个细节AbortError一定要和真实错误分开处理。因为很多团队的前端错误上报是全局捕获的如果不加判断用户每切一次页面就会往监控平台塞一条请求失败指标直接失真。我见过有团队因为这个接口错误率常年虚高 3% 左右排查半天才发现是取消请求没过滤。第二个细节beforeunload里取消请求这种行为要谨慎。它确实能让你在离开页面时不再等待那些没意义的响应但如果你在页面卸载时还想上报埋点或者做数据保存贸然 abort 会把这类请求也一起掐掉。稳妥的做法是给要保命的请求单独用一个不会被取消的 fetcher或者走navigator.sendBeacon。超时类的取消可以更简洁现在主流环境支持直接组合// 5 秒没结果就自动放弃不用手写 setTimeout const res await fetch(/api/slow, { signal: AbortSignal.timeout(5000) });这行的好处是它把超时和取消统一到了同一个信号机制下你不用再自己维护setTimeout和clearTimeout也就不会出现定时器泄漏。2.2 各语言和库的取消入口横向对比跨语言调试的时候最容易忘的就是这个客户端到底怎么取消。我整理了一张表都是我实际项目里用过的。客户端取消入口取消后的表现备注浏览器 fetchAbortController.abort()抛AbortError标准做法推荐XMLHttpRequestxhr.abort()触发abort事件老项目常见Axios v1signal传 AbortSignal抛CanceledError老版本用CancelToken已废弃OkHttpcall.cancel()抛 IOException消息含 Canceled底层直接断 socketJava 11 HttpClientsendAsync返回的CompletableFuture.cancel(true)抛 CancellationException配合HttpRequest.timeout更稳Go net/httpreq.WithContext(ctx)cancel()返回 context.Canceled生态最统一Python requests无原生取消只能靠 timeout 或杀线程/进程同步模型天生不好取消curlCtrlC立即断开连接调试常用最干脆Postman关闭标签页或取消按钮客户端不再等待不影响服务端逻辑关于 Python 这一行我要多说一句因为太多人在这个问题上困惑。requests是同步阻塞模型一旦send()调下去主线程就卡在 socket 读上了没有任何标准的从外面取消它的接口。你只能在请求前设timeout(3.05, 30)这种连接超时和读超时的元组把最坏情况限制住。如果确实需要可取消的请求就上aiohttp或httpx用 asyncio 的 Task 取消机制那个是真的能中断。Go 的 context 是我个人认为设计得最舒服的取消模型因为它是显式参数传递的你不可能忘记它可以被取消ctx, cancel : context.WithTimeout(context.Background(), 3*time.Second) defer cancel() req, _ : http.NewRequestWithContext(ctx, POST, url, body) resp, err : http.DefaultClient.Do(req) if errors.Is(err, context.DeadlineExceeded) { log.Println(上游超时已放弃本次调用) }2.3 取消只是我不等了不等于服务端没做这是我最想强调的一点也是无数脏数据的源头。客户端取消的语义是放弃等待结果而服务端是否执行、执行到哪一步完全是另一件事。取消和回滚之间没有任何自动的因果关系。所以只要你的接口有副作用就得在服务端做幂等。最常用的手段是幂等键客户端在发起请求时生成一个唯一 ID比如requestId服务端用它做唯一索引或者分布式锁重复请求直接返回第一次的结果。更根本的办法是把不确定的部分放在最后。比如一个下单流程需要扣库存、写订单、发消息三步如果前两步在事务里第三步在事务外那你调试时停在第三步断点上即便客户端取消了前面的数据也已经落库了。反过来如果你把发消息这类不可回滚的操作尽量前移或者做成异步补偿取消带来的不一致就会少很多。还有一个很实用的小技巧在调试环境给写操作加一个环境开关。比如配置app.debug.dryRuntrue的时候所有写库操作走内存而不落库。这样你打断点随便怎么按都不会污染数据。这个开关的成本极低但能省掉大量手动删脏数据的时间我们团队后来是默认开着的只有需要完整验证链路时才关掉。3. 调试器里怎么体面地放弃这次请求前面讲的都是在客户端动手。但更常见的场景是你已经断在代码里了单步走了一半发现这个请求不该继续。这时候你要在调试器这一侧做决定。这一章讲的就是这些具体操作以及它们各自到底干了什么。3.1 断点命中之后你其实有四种退出姿势大多数人只知道两种继续和停止。实际上主流调试器都提供了四种语义差别很大用错了后果完全不同。第一种是Resume / ContinueIDE 里通常是 F9或者绿色三角。让线程从断点处继续往下跑代码逻辑全部执行响应正常返回。适合我看完了没问题放行。第二种是Force ReturnIDEA 里叫 Force ReturnEclipse 里类似。不执行剩余代码直接构造一个返回值让当前方法返回。这个动作非常有用它能让请求链路假装成功地走完但你跳过了所有危险的写操作。比如你断在orderService.create()里面发现参数不对直接 Force Return 一个 null请求继续往外走但数据库一条记录都不会多。第三种是Drop Frame / Pop Frame弹出栈帧。把当前栈帧弹掉执行点回到调用者的那一行可以重新走一遍。这个是我想换个参数重来一次的场景非常适合反复调试同一段逻辑。注意它只改变执行位置已经被执行过的副作用比如已经写了的日志、已经改了的对象字段不会自动回退。第四种是Terminate / Stop。直接杀掉整个被调试进程。最彻底也最粗暴。本地用完全没问题线上千万别用。有些 IDE 还提供一个Stop按钮其实是 Detach只是断开了调试连接进程还在跑——这个一定要看清楚图标提示我就干过一次以为停了进程结果接口照常执行完的蠢事。动作对业务逻辑的影响对连接的影响典型场景Resume完整执行正常返回响应排查完毕放行Force Return跳过剩余逻辑正常返回可能返回空值发现参数不对不想写库Drop Frame回退到调用者重来保持不变想换输入重跑Terminate进程终止连接被强制断开本地环境无所谓Detach无影响代码继续跑正常返回只是想不看了3.2 主流调试器的具体操作位置不同工具的按钮叫法五花八门我把常用的几款列一下省得你到处翻菜单。IntelliJ IDEA 和 PyCharm调试窗口左侧那排图标里Force Return 是一个带返回箭头的小图标鼠标悬停能看到说明。Drop Frame 是一个向下的箭头。还有个非常实用的功能叫做 Mute Breakpoints一键把所有断点静音但保留配置调试到一半想让请求跑通时特别顺手比一个个取消勾选快多了。VS Code终止按钮就是那个红色方块。它有个特点是 Terminate 和 Disconnect 语义取决于你用的调试适配器Node 场景下重启进程会比较干脆。如果你调的是附加到已运行进程的模式红色方块可能只是断开连接。建议每次用之前先确认一下launch.json里的配置类型。Chrome DevTools这个工具比较特殊它没有取消当前请求的按钮。你能做的是在 Network 面板里把某个请求右键 Block request URL这样后续同 URL 的请求会被直接拦截并发起失败但已经发出的那个你只能等它或者关标签页。所以调前端的时候如果你的请求打到后端停在断点上了最快的方式其实是去后端放行而不是在前端折腾。GDB 和 LLDBdetach是断开连接让程序继续跑kill是杀掉进程interruptCtrlC是让程序暂停下来。命令行调试要小心kill在没有确认对象的时候可能误伤。3.3 用代理把请求掐在门外比在代码里拦更省事除了调试器还有一个思路是在请求到达应用之前就把它拦下来。这个方式我觉得被严重低估了。抓包代理工具基本都支持断点功能可以设置规则匹配某个 URL请求发到代理时先停下来你可以在代理界面里选择放行、修改或者直接丢弃。丢弃意味着这个请求根本到不了你的后端服务也就不会有任何副作用。调一些危险接口比如支付、删除的时候我习惯性地在代理上挂一条规则先拦住确认参数没问题再放行。另一个思路是本地反向代理做 mock。用 Nginx 或者简单的 mock 服务把某些路径直接返回固定 JSON请求压根不转发到真实后端。这招在联调期特别好用因为你既能看到前端完整流程又不用担心污染数据。还有一招是网关层的接口下线。有些公司的网关支持把某个路由临时置为拒绝返回 503。这个动作是全局的适合这个接口现在谁也别调的场景但影响面大调试环境用之前最好跟同事打个招呼。4. 踩坑记录断点期间那些看起来没事的连锁反应前面讲的是怎么取消这一章讲取消不当会怎样。这几个坑都是我或者我身边的人实打实踩过的排查过程我尽量写完整因为排查链路本身比结论更有价值。4.1 一次超时引发的双写排查链路复盘事情是这样的测试同学反馈用户在页面上点了一次提交后台出现了两条一模一样的记录间隔正好 30 秒。拿到这个问题排查顺序是这样的。第一步看数据库里两条记录的时间戳和字段。两条记录除了主键和创建时间其他字段完全一致创建时间相差 30.02 秒。这个间隔太规整了明显不是用户手抖点了两次而是某个环节的重试。第二步看网关的访问日志。同一时刻有两条请求记录第一条的upstream_response_time是 30.001 秒状态码 504第二条upstream_response_time是 0.02 秒状态码 200。这就锁定方向了第一条请求在 30 秒时被网关判定超时返回 504 给前端前端有自动重试逻辑发起了第二条而第一条其实在服务端还在跑最终也成功了。第三步为什么第一条会卡 30 秒应用日志显示那条请求进入接口后在某个Transactional方法里停留了 30 秒才继续。再看线程栈发现当时有人在开发环境用调试器断在这个方法上单步了差不多半分钟才放行。结论就很清楚了断点停留时间超过了网关的上游读超时触发了超时重试而接口本身没有幂等保护。对应的修复有三处网关的超时从 30 秒提到 60 秒但这只是延后问题前端对写操作的重试必须带幂等键服务端对幂等键做唯一约束。三处都做了之后同样的操作再也不会出现双写。这个案例给我的教训是调试断点这件事不只是我本地看看它会影响整个超时链路。如果你在的服务是多人共用的开发环境长时间挂断点相当于给所有人都埋了个雷。4.2 连接池被慢慢榨干的那种窒息感另一个更隐蔽的坑是资源耗尽。有一次开发环境突然整体变慢所有人的接口响应时间从几十毫秒涨到几秒日志里开始刷Connection is not available, request timed out after 30000ms。第一反应是数据库出问题了去看show processlist发现有二十多个连接处于Sleep状态但每个都带着未提交的事务。这就奇怪了事务没提交又不干活是在等什么接着去应用侧jstack抓线程栈答案立刻出来了十几个工作线程的栈顶都停在同一个断点所在的方法上下面挂着数据库连接持有的调用链。也就是说有十几个人或者同一个人开了十几个请求在同一个断点上暂停了十几个线程每个线程都攥着一个数据库连接不放。连接池最大值就那么点被占满之后所有新请求都在排队等连接整个环境就瘫了。这个问题的排查思路值得记一下先看数据库侧有没有异常再看应用侧线程栈最后对照连接池配置算一笔账。连接池大小 20、线程池大小 200这个比例本身就不合理——200 个线程抢 20 个连接只要有 3、4 个人同时挂断点立刻就会排队。处理办法也很直接开发环境把连接池调大一点、加一个事务超时的兜底配置比如 Spring 的Transactional(timeout 30)超过时间自动回滚释放连接。同时约定一条纪律不要在共享的开发环境里长期挂断点。想长时间单步就用自己本地的服务或者独立的调试实例。有几个数据库侧的配置也值得留意它们决定了资源被占住之后多久能自动释放参数常见默认值作用innodb_lock_wait_timeout50 秒行锁等待多久后报错wait_timeout28800 秒空闲连接多久被服务端断开HikariCPconnectionTimeout30000 毫秒从池里拿连接最多等多久HikariCPmaxLifetime1800000 毫秒连接最长存活时间Spring 事务 timeout默认 -1不限制事务最长执行时间提示wait_timeout默认 8 小时意味着一个被断点挂住的空闲事务理论上能占着连接耗掉一整个工作日。把应用侧的事务超时配上比指望数据库自己清理靠谱得多。4.3 当你调试的是一台嵌入式设备中断是另一个故事热词里混进来一堆 STM32、串口中断、DMA 空闲中断、CAN 总线收发的关键词我猜不少人是想在设备上跑 HTTP 上报然后发现调试模式下链路全乱。这块我也踩过说说经验。嵌入式设备的 HTTP 请求通常是模组里的 TCP socket和你在 PC 上理解的请求取消完全是两码事。PC 上你可以调一个 API 就放弃等待设备上你能做的通常是三件事之一发一条ATCIPCLOSE关掉 socket、让模组复位、或者直接断电。其中关 socket 是相对温和的方式但要注意有些模组的 socket 关闭是异步的你发完指令得等CLOSE OK的确认不等就继续发下一条指令很容易撞上busy。更麻烦的是调试模式和硬件中断会互相干扰。你在 STM32 上打断点CPU 停下来的时候外设并不会停串口还在往里收字节、DMA 还在搬数据、看门狗还在计数。如果你断点停的时间超过看门狗溢出时间回来的时候设备已经复位了你会看到一堆莫名其妙的进不去中断现象。这不是中断配置错了是你的断点把时序破坏了。还有个典型现象用 DMA 加空闲中断接收串口数据正常跑没问题一开调试就收不全。原因往往是断点期间又来了新数据DMA 缓冲区被覆盖或者溢出标志置位没清等你恢复执行时读到的是残缺帧。我后来的做法是调试串口收发时不用断点改用日志和计数器把关键节点的状态打到缓冲区里跑完再看比断点靠谱得多。这里有个概念上的类比我觉得挺有意思硬件中断是外设抢 CPU是强制性的、有优先级的HTTP 请求取消是调用方通知执行方是协作式的、没有强制力的。你没法用抢占的思路去理解请求取消也别指望在设备上按个按钮就能中断一个已经发出去的 TCP 包——网络层的东西一旦出了网卡就不受你控制了。4.4 一份可以贴在显示器边上的自检清单零散说了这么多我把它压缩成一份清单每次打断点之前扫一眼能省不少事。这次调试的接口有没有写操作有的话先确认幂等键、先确认是否有 dryRun 开关。断点会停多久如果超过网关的上游读超时一般是 30 到 60 秒就要警惕重试。现在用的是哪个环境共享开发环境挂断点等于给同事埋雷。我准备用什么方式退出Resume、Force Return、Drop Frame、Terminate先想清楚再点。客户端那边要不要同步取消如果是前端页面记得处理AbortError不污染错误监控。设备端的场景要额外确认看门狗时间、串口溢出标志、DMA 缓冲区的覆盖问题。5. 让请求天生可中断几处值得做的工程化改造与其每次调试时手忙脚乱地取消不如在设计阶段就让请求具备可以被干净地放弃的能力。这一章讲几个投入不大但收益明显的改造点。5.1 服务端要有感知客户端断开的能力大多数框架默认不会主动告诉你客户端已经走了你得显式去监听。Spring MVC 里可以用异步请求拿到AsyncContext注册一个监听器在onError或onComplete之外判断连接是否还活着。Go 的写法更自然直接把r.Context()一路透传下去客户端断开时这个 ctx 会自动被取消func handler(w http.ResponseWriter, r *http.Request) { ctx : r.Context() select { case -time.After(5 * time.Second): w.Write([]byte(done)) case -ctx.Done(): // 客户端已经断开主动放弃后续耗时计算 log.Println(客户端已断开放弃处理:, ctx.Err()) return } }这个模式的价值在于它让服务端可以提前放弃。一个本来要跑 10 秒的聚合查询客户端在第 2 秒就断开了你可以在每一步开始前检查一下 ctx省下后面 8 秒的计算和数据库压力。高频接口上这个优化非常值钱。Nginx 那一侧也有一处要注意proxy_ignore_client_abort这个参数默认是关的意思是客户端断开时 Nginx 会跟着断开上游连接如果被打开成onNginx 会无视客户端断开继续把上游的响应读完。调试期把它保持默认关闭比较好能让取消传导得更快。5.2 超时链路要分层对齐从外到内依次收紧我在前面反复提到超时这里把它系统化一下。多层架构里超时配置必须从最外层往最内层依次减小否则会出现外层还在等内层已经放弃或者反过来的混乱。层级建议超时理由客户端10 秒用户能接受的等待上限反向代理8 秒比客户端略短先于用户看到错误应用接口5 秒留出响应组装和网络传输的余量下游 HTTP 调用2 秒快速失败避免线程堆积数据库查询1 秒慢查询直接掐掉保护连接池这个递减关系背后的逻辑很简单内层先超时外层就能拿到一个明确的错误响应并返回给用户如果外层先超时内层还在跑你既浪费了资源用户又只看到一个没有信息量的 504。还有一点要提醒超时值一定要配置化不要硬编码。调试期你可能想把它临时调大好从容地单步线上则要保持严格。有个配置中心或者环境变量能改比每次改代码重启快得多。5.3 把副作用隔离出去让取消变得无所谓最彻底的方案是让取消这件事变得不重要。核心思路是把不可逆的副作用集中到流程的最后一步前面的步骤全部设计成可重复执行且无副作用的。具体做法上我比较推荐三种。第一种是幂等键加唯一索引。客户端生成 UUID服务端在业务表上建唯一索引重复插入直接抛约束冲突捕获之后返回第一次的结果。这个方案的好处是它不依赖任何分布式组件单库就能扛。第二种是状态机加前置校验。任何写操作之前先检查目标状态是否允许这次变更。比如订单只有在待支付状态才允许支付已经支付成功的再进来就直接返回成功不再走支付逻辑。这个思路能天然消化掉重试和重复请求。第三种是调试期的 dryRun 开关。前面提过这里再强调一次它的价值它是唯一能让你随便打断点、随便单步而不担心数据的手段。实现成本可能就是一个 if 判断。if (debugProperties.isDryRun()) { log.info([dryRun] 跳过真实写库参数{}, payload); return mockResult(payload); } // 真实写库逻辑6. 一些我个人的操作习惯写了这么多最后说几个我这两年固定下来的小习惯都是被坑出来的。第一个习惯调试写接口之前先在抓包代理上挂一条拦截规则。请求先停在代理上我检查一遍参数确认没问题再放行。这样如果我在代码里打断点代理这一侧就已经有了一个保险丝随时可以丢弃。成本是每次多配一条规则收益是从此不用再手动删脏数据。第二个习惯把长断点换成条件断点加日志。很多人挂断点是因为想看当时的变量值其实条件断点或者带条件的日志输出完全能满足还不会让线程停下来。IDEA 的 Evaluate and Log 功能特别适合这个场景可以让断点命中时打印变量但不暂停请求照常跑完。只有当确实需要单步跟踪逻辑分支时我才挂真断点。第三个习惯需要长时间单步的时候一定切到本地服务或者专属调试实例。共享环境是公共资源你在上面挂三分钟断点可能就有别人的请求在连接池里排队。这个道理跟不要在公共 WiFi 上看高清视频是一样的。第四个习惯给调试期的关键动作留个标记。比如 dryRun 模式下所有日志前缀都加[dryRun]这样事后翻日志一眼就能分辨哪些操作是调试期的哪些是真实的。排查问题的时候这个区分能省很多时间。第五个习惯AbortController这种取消机制在项目模板里就预置好。别等到需要取消的时候再临时加因为那时候通常已经写了好几个 fetch 调用一个个改很容易漏。前端项目在封装请求库的时候就把 signal 参数留出来后面谁想取消谁传成本最低。关于中断正在 debug 的请求这件事说到底就是一句话先分清你要中断的是哪一层再选对应的工具最后想清楚中断之后会不会留下后果。客户端取消用 signal服务端放弃用调试器的 Force Return真要彻底断开就杀进程至于数据一致性那是在写代码的时候就该解决的问题不是调试时能补救的。
返回列表