一次粗心导致的bug定位

一次神奇的Bug定位之旅:从"Remote host terminated the handshake"到URL拼写错误

背景

最近在维护一个PubMed文献下载系统时,遇到了一个非常有趣的Bug。系统在下载PDF文件时频繁出现"Remote host terminated the handshake"错误,但通过curl测试却完全正常。这个问题的定位过程充满了意外和惊喜,值得记录下来分享给大家。

问题现象

错误日志

复制代码
2025-07-24 10:57:26 [downloadExecutorService-pool-0] ERROR o.p.x.s.b.DailyArticleDownloadService  下载失败: PMC12282765 - Remote host terminated the handshake

2025-07-24 10:57:26 [downloadExecutorService-pool-0] WARN  o.p.x.s.b.DailyArticleDownloadService  🔄 下载失败: PMC12282765 (Remote host terminated the handshake), 第1次重试

但curl测试却成功

bash 复制代码
curl -x http://127.0.0.1:8118 -H "User-Agent: Mozilla/5.0..." -H "Accept: application/pdf" -H "cookie: cloudpmc-viewer-pow=TOKEN" -L -o /tmp/test.pdf "https://pmc.ncbi.nlm.nih.gov/articles/PMC12282765/pdf"

# 下载成功,文件大小2.5MB

问题定位过程

第一步:网络连通性测试

首先确认基础网络连通性:

bash 复制代码
# 测试代理连通性

curl -x http://127.0.0.1:8118 -I https://www.baidu.com

# ✅ 成功



# 测试PubMed API

curl -x http://127.0.0.1:8118 -I "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?db=pmc&mindate=2025/07/23&maxdate=2025/07/23&datetype=pdat&retmax=10&retmode=json"

# ✅ 成功,返回405(正常,因为用了HEAD请求)



# 测试PDF下载链接

curl -x http://127.0.0.1:8118 -I "https://pmc.ncbi.nlm.nih.gov/articles/PMC12282765/pdf"

# ❌ 返回403 Forbidden(需要token)

第二步:Token认证测试

发现需要token认证,测试带token的请求:

bash 复制代码
# 测试带token的PDF下载

curl -x http://127.0.0.1:8118 -I -H "User-Agent: Mozilla/5.0..." -H "Accept: application/pdf" -H "cookie: cloudpmc-viewer-pow=TOKEN" "https://pmc.ncbi.nlm.nih.gov/articles/PMC12282765/pdf"

# ✅ 返回301重定向



# 跟随重定向

curl -x http://127.0.0.1:8118 -I -H "User-Agent: Mozilla/5.0..." -H "Accept: application/pdf" -H "cookie: cloudpmc-viewer-pow=TOKEN" -L "https://pmc.ncbi.nlm.nih.gov/articles/PMC12282765/pdf"

# ✅ 返回200,Content-Type: application/pdf

第三步:Java代码诊断

既然curl成功,问题可能在Java代码。创建诊断接口:

java 复制代码
@RestController

@RequestMapping("/diagnostic")

public class DiagnosticController {

   

    @GetMapping("/comprehensive-test")

    public Map<String, Object> comprehensiveTest() {

        // 测试代理连通性、PubMed API、PDF SSL连接等

    }

   

    @GetMapping("/concurrent-test")

    public Map<String, Object> concurrentTest() {

        // 测试并发下载

    }

}

诊断结果:所有测试都成功!

json 复制代码
{

  "proxyTest": {"success": true, "statusCode": 200},

  "pubmedApiTest": {"success": true, "statusCode": 200},

  "pdfSslTest": {"success": true, "statusCode": 200},

  "pdfTokenTest": {"success": true, "statusCode": 200},

  "concurrentTest": {"success": true, "successCount": 2, "failureCount": 0}

}

第四步:深入分析差异

这很奇怪!诊断接口成功,但实际下载任务失败。让我们对比:

  1. curl成功 :使用token VwR3AGZmZwD4AwVhZwHjBGN0AvV:xDK5ppGzDRT8-HIxpqIeRiEKzy4pSs5CmywvDYwEPMf%2C3044

  2. Java诊断接口成功:并发测试也成功

  3. Java下载任务失败 :使用token VwR3AGZmZwH4ZmDhZQp1Zwx0AlV:qf1SIS80cScsJoCpiL6WbY

第五步:检查实际下载代码

仔细检查DailyArticleDownloadService.java中的下载代码:

java 复制代码
private SingleDownloadResult downloadSingleArticle(String pmcId, String token) {

    try {

        String baseUrl = "https://pmc.ncgibi.nlm.nih.gov";  // 等等...

        String pdfUrl = baseUrl + "/articles/PMC" + pmcId + "/pdf";

        // ...

    }

}

发现问题了!

URL中的域名拼写错误:pmc.ncgibi.nlm.nih.gov 应该是 pmc.ncbi.nlm.nih.gov

问题根源

为什么会出现这种错误?

  1. 错误的域名解析ncgibi 可能解析到了某个服务器

  2. SSL握手失败:该服务器在SSL握手阶段就断开了连接

  3. 错误包装:Java的SSL库将连接中断包装成了"Remote host terminated the handshake"

为什么诊断接口成功?

诊断接口使用的是正确的URL,而实际下载任务使用的是错误的URL。

解决方案

修复URL拼写错误:

java 复制代码
// 修复前

String baseUrl = "https://pmc.ncgibi.nlm.nih.gov";



// 修复后  

String baseUrl = "https://pmc.ncbi.nlm.nih.gov";

经验总结

1. 不要被错误信息误导

"Remote host terminated the handshake"听起来像是SSL配置或网络问题,但实际上可能是URL错误。

2. 对比测试很重要

通过curl测试确认了网络和认证都正常,这帮助我们排除了很多可能的原因。

3. 代码审查要仔细

即使是简单的URL,也要仔细检查拼写。这种错误很容易被忽略。

4. 诊断工具的价值

创建诊断接口帮助我们快速定位问题,是很好的调试实践。

5. 日志分析要全面

不仅要看错误信息,还要对比不同场景下的日志,找出差异。

技术细节

为什么错误的域名会导致SSL握手失败?

  1. DNS解析:错误的域名可能解析到不存在的服务器或错误的IP

  2. 连接建立:TCP连接可能建立成功,但目标服务器不是预期的

  3. SSL握手:目标服务器可能不支持SSL或SSL配置不匹配

  4. 连接中断:服务器在握手过程中主动断开连接

为什么错误被包装成"Remote host terminated the handshake"?

这是Java SSL库的标准错误处理机制。当SSL握手过程中连接被远程服务器中断时,就会抛出这个错误。

预防措施

  1. 代码审查:建立代码审查流程,特别是URL和配置相关的代码

  2. 单元测试:为关键的网络请求编写单元测试

  3. 集成测试:定期运行完整的下载流程测试

  4. 监控告警:设置下载失败率的监控告警

  5. 文档记录:记录常见问题和解决方案

结语

这次Bug定位过程虽然最终发现是一个简单的拼写错误,但整个过程展示了系统化调试的重要性。通过逐步排除、对比测试、深入分析,我们最终找到了问题的根源。

这种"简单"的Bug往往最难发现,因为它们隐藏在复杂的系统架构和错误信息背后。但只要我们保持耐心,运用正确的调试方法,就一定能找到问题的真相。


这次调试经历让我深刻体会到:在软件开发中,最复杂的问题往往有最简单的解决方案,而最简单的错误往往最难发现。

相关推荐
90后的晨仔3 分钟前
👂《侦听器(watch)》— 监听数据变化执行副作用逻辑
前端·vue.js
曾经的三心草3 分钟前
微服务的编程测评系统6-管理员登录前端-前端路由优化
前端·微服务·状态模式
Point15 分钟前
[LeetCode] 最长连续序列
前端·javascript·算法
rookiesx19 分钟前
安装本地python文件到site-packages
开发语言·前端·python
支撑前端荣耀20 分钟前
九、把异常当回事,代码才靠谱
前端
LotteChar28 分钟前
HTML:从 “小白” 到 “标签侠” 的修炼手册
前端·html
趣多多代言人30 分钟前
20分钟学会TypeScript
前端·javascript·typescript
90后的晨仔30 分钟前
⚙️ 《响应式原理》— Vue 是怎么做到自动更新的?
前端·vue.js
结城30 分钟前
深入掌握CSS Grid布局:每个属性详解与实战示例
前端·css
寒..36 分钟前
网络安全第三次作业
前端·css·html