从 cJSON_Parse 崩溃说起:libcurl-multi 并发取 IP 定位时,我把 Codex 的 Base URL 切到了 TaoToken
这篇文章要解决的是一个很具体的排障场景:你用 libcurl 的 multi 接口并发请求ip-api.com,拿到 JSON 后交给 cJSON 解析countryCode、regionName、city、lat、lon,结果程序在cJSON_Parse之后直接段错误,或者parse_ip_json_str返回了 -1 却没人处理,最后打印出一堆空字符串。更隐蔽的是chunk->memory这种写法——chunk本身是栈上的结构体,不是指针,写成chunk->memory编译器可能只给个警告,运行时却读到野地址。这类问题靠肉眼盯代码很容易漏,我现在的做法是先把 Codex 的 Base URL 改到 TaoToken 通道(官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end ),拿到 Key 后让 Codex 对照 cJSON 的返回值语义,把判空、释放、返回值检查逐条补上。TaoToken 在这里只负责提供 Key 和 Base URL,不参与 libcurl 的请求过程,也不碰 JSON 解析逻辑,它解决的是"我能不能稳定地把这段代码贴给模型、让它帮我查"的问题。
一、原问题与场景:multi 并发 + cJSON 解析,错在哪几处
先把场景还原清楚。原始代码的结构大致是:
mulit_get_ip_info(const char *curl_url)负责初始化CURLM、创建 easy handle、挂到 multi handle 上,然后curl_multi_perform循环直到still_running == 0;WriteMemoryCallback把响应体追加到MemoryStruct chunk的memory缓冲区;- 请求结束后调用
cJSON_Parse(chunk->memory),再走parse_ip_json_str(root)提取字段; parse_ip_json_str内部用cJSON_GetObjectItemCaseSensitive取status,判断是不是"success",再逐个取countryCode、regionName、city、lat、lon。
痛点集中在四个地方,而且它们经常同时出现:
第一,cJSON_Parse的返回值没有判空。cJSON_Parse在输入不是合法 JSON、或者缓冲区里混入了非 JSON 字节时,会返回NULL。如果直接把这个NULL传给parse_ip_json_str,函数内部第一句cJSON_GetObjectItemCaseSensitive(current_json_str, "status")就会对空指针解引用,段错误就是这么来的。
第二,parse_ip_json_str的返回值没有被处理。这个函数设计上返回 0 表示解析成功、-1 表示失败,但调用方往往写成parse_ip_json_str(root);就完事,既不判断也不打印。结果是解析失败时current_ip_info里全是初始化的 0 和空串,print_ip_json_parse照样输出,看起来"跑通了",实际数据是错的。
第三,chunk->memory这类指针访问写错。MemoryStruct chunk = {0};声明的是结构体变量,访问成员应该用chunk.memory。写成chunk->memory时,编译器把chunk当成指针去解引用,取到的地址完全不可控。这个错误在cJSON_Parse(chunk->memory)这一行尤其致命,因为紧接着就是解析。
第四,WriteMemoryCallback里的realloc失败没有区分处理。char *ptr = realloc(mem->memory, mem->size + realsize + 1);如果返回NULL,原缓冲区还在,但代码直接return 0,curl 会认为写回调失败并中止传输。此时mem->memory仍然指向旧地址,后续如果还去cJSON_Parse,解析的就是不完整的 JSON。
这四个问题里,前两个是逻辑判空,后两个是指针和内存管理。它们单独出现时可能只是数据不对,叠在一起就是崩溃。我一开始想自己逐行改,但parse_ip_json_str里goto end的跳转和cJSON_Delete的释放时机交织在一起,改一处容易漏另一处,于是决定把整段代码连同报错一起交给 Codex,让它按 cJSON 的返回值约定来补。
二、TaoToken 前置:把 Codex 的 Base URL 指到通道上
在让 Codex 看代码之前,先要把它的请求出口配好。我用的方式是走 TaoToken 的兼容通道,Base URL 填https://taotoken.net/api,Key 从官网创建。
具体步骤:
- 打开 https://taotoken.net/?utm_source=taotoken_aicg_blog_end ,注册后进入控制台;
- 在 API Keys 页面创建一个 Key,复制出来(下面用
YOUR_API_KEY代替); - 找到 Codex 的配置文件。Codex 用的是
config.toml,通常在~/.codex/config.toml或者项目根目录下; - 在
config.toml里把base_url指向https://taotoken.net/api,并把api_key设成你创建的 Key; - 保存后重启 Codex,或者重新加载配置。
这里要强调一点:TaoToken 只提供 Key 和 Base URL,它不参与 libcurl 的 multi 请求,也不参与 cJSON 的解析。你的程序该请求ip-api.com还是请求ip-api.com,JSON 该怎么解析还是怎么解析。TaoToken 的作用是让你有一个稳定的通道,把mulit_get_ip_info、WriteMemoryCallback、parse_ip_json_str和cJSON_Parse的报错一起贴给 Codex,让它帮你查。
如果你用的是 Claude Code,配置方式不同,要改的是settings.json里的ANTHROPIC_*环境变量,把ANTHROPIC_BASE_URL指向https://taotoken.net/api,ANTHROPIC_API_KEY填你的 Key。Codex 和 Claude Code 的配置文件不要混用,一个走config.toml,一个走settings.json。
三、可复制配置:Codex 的 config.toml 与请求侧参数
先给 Codex 侧的配置。config.toml里至少要有这几项:
model = "你的模型ID" base_url = "https://taotoken.net/api" api_key = "YOUR_API_KEY"如果你习惯用环境变量,也可以把 Key 放到OPENAI_API_KEY或对应的变量里,config.toml里只留base_url。模型 ID 按你实际选用的填,不要照抄别人的。
再给 libcurl 请求侧的参数。原始代码里InitCurlRequest没有展开,这里补一个最小可用的版本,重点是CURLOPT_WRITEFUNCTION和CURLOPT_WRITEDATA要配对:
CURL *InitCurlRequest(const char *url, MemoryStruct *chunk) { CURL *handle = curl_easy_init(); if (!handle) return NULL; curl_easy_setopt(handle, CURLOPT_URL, url); curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, WriteMemoryCallback); curl_easy_setopt(handle, CURLOPT_WRITEDATA, (void *)chunk); curl_easy_setopt(handle, CURLOPT_TIMEOUT, 10L); curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION, 1L); return handle; }CURLOPT_WRITEDATA传的是chunk的地址,回调里userp收到的就是它,然后MemoryStruct *mem = (MemoryStruct *)userp;再访问mem->memory。注意这里mem是指针,用->是对的;而chunk是结构体变量,在mulit_get_ip_info里访问它必须用chunk.memory。
WriteMemoryCallback里realloc失败的处理建议改成:
char *ptr = realloc(mem->memory, mem->size + realsize + 1); if (!ptr) { fprintf(stderr, "realloc failed\n"); return 0; }返回 0 会让 curl 中止,但至少你能在 stderr 看到原因,而不是静默拿到半截 JSON。
四、验证请求与成功结果:让 Codex 补判空和释放
配置好之后,把下面这段贴给 Codex,让它对照 cJSON 的返回值语义来改:
mulit_get_ip_info的完整实现;WriteMemoryCallback;parse_ip_json_str;- 报错信息,比如
Segmentation fault或者parse_ip_json_str returned -1; - 你期望的行为:解析成功打印五个字段,失败时打印原因并返回非 0。
Codex 通常会给出几处关键修改。第一处是cJSON_Parse之后必须判空:
cJSON *root = cJSON_Parse(chunk.memory); if (!root) { fprintf(stderr, "cJSON_Parse failed, raw: %s\n", chunk.memory ? chunk.memory : "(null)"); goto end; } if (parse_ip_json_str(root) == 0) { print_ip_json_parse(); } else { fprintf(stderr, "parse_ip_json_str failed\n"); } cJSON_Delete(root);注意这里把chunk->memory改成了chunk.memory,并且cJSON_Delete(root)只在root非空时执行。goto end跳到清理段,curl_multi_remove_handle、curl_easy_cleanup、curl_multi_cleanup、curl_global_cleanup和free(chunk.memory)都在那里,顺序不能乱。
第二处是parse_ip_json_str内部对status的判断。原始代码用cJSON_GetObjectItemCaseSensitive取status,然后cJSON_IsString(status)和strcmp(status->valuestring, "success")。这里status本身可能是NULL,cJSON_IsString(NULL)返回 false,短路后不会走到strcmp,所以是安全的。但countryCode、regionName、city这几个字段,原始代码用strncpy拷贝,没有保证目标缓冲区以\0结尾。strncpy(dst, src, sizeof(dst)-1)之后应该手动补一个dst[sizeof(dst)-1] = '\0';,否则strlen可能越界。
第三处是lat和lon的判断。原始代码用current_ip_info.lat != 0作为成功条件之一,但纬度 0 是合法值(赤道),经度 0 也是合法值(本初子午线)。用!= 0判断会把合法数据当成失败。更稳妥的做法是单独用一个int has_lat = 0;标志,取到cJSON_IsNumber就置 1。
改完之后重新编译运行,成功的输出应该类似:
current ip info: countryCode ID: CN regionName: Beijing city: Beijing lat: 39.9042 lon: 116.4074如果cJSON_Parse失败,你会看到cJSON_Parse failed, raw: ...并把原始响应打出来,这时就能判断是ip-api.com返回了非 JSON(比如限流页面),还是chunk.memory里混入了多余字节。
五、本篇常见错排查
错误一:chunk->memory编译通过但运行崩溃。这是最典型的。MemoryStruct chunk = {0};是结构体变量,chunk.memory才是成员访问。写成chunk->memory时,编译器把chunk的值当作地址,而chunk在栈上,它的"值"是栈上的字节内容,解引用后读到的是随机地址。修复方式就是全文搜索chunk->,改成chunk.。如果你确实用了MemoryStruct *chunk = malloc(...),那才用->,但此时chunk本身也要判空。
错误二:cJSON_Parse返回NULL没判,直接传给parse_ip_json_str。表现是段错误,栈回溯会指向cJSON_GetObjectItemCaseSensitive。修复就是在cJSON_Parse之后立刻if (!root) { ... goto end; },并且在goto end之前不要调用cJSON_Delete。
错误三:parse_ip_json_str返回 -1 但调用方不处理。表现是程序正常退出,但打印的countryCode、regionName、city都是空串,lat、lon都是 0。修复是判断返回值,失败时打印status字段的内容,ip-api.com在失败时会返回{"status":"fail","message":"..."},把message打出来能直接看到原因。
错误四:WriteMemoryCallback里realloc失败返回 0,但mem->memory没置空。表现是后续cJSON_Parse解析到不完整 JSON,或者free时重复释放。修复是realloc失败时保留原指针,不要free,也不要覆盖mem->memory,直接返回 0 让 curl 中止,然后在mulit_get_ip_info的清理段统一free。
错误五:curl_multi_perform循环里没有处理CURLM_OK以外的返回值。原始代码在res != CURLM_OK时break,但break之后直接走到解析段,此时请求可能没完成,chunk.memory里是半截数据。修复是break之后设置一个错误标志,跳过解析,直接进清理段。
错误六:curl_global_init和curl_global_cleanup的配对。如果你在多个线程里调用mulit_get_ip_info,curl_global_init应该只在程序启动时调用一次,而不是每次进函数都调。原始代码在函数内调用curl_global_init,多线程下会有竞态。修复是把curl_global_init提到main里,mulit_get_ip_info里只做curl_multi_init。
六、语义一致的 CTA
这段 libcurl-multi 加 cJSON 的代码,排查到最后其实就两类问题:指针访问写错,和返回值没判空。前者靠编译器警告和全文搜索能抓一部分,后者必须对着 cJSON 的文档逐个函数确认返回值语义。我现在的流程是先把 Codex 的 Base URL 配到https://taotoken.net/api,Key 从 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 创建,然后把mulit_get_ip_info、WriteMemoryCallback、parse_ip_json_str和报错一起贴过去,让 Codex 按 cJSON 的返回值约定补判空和释放逻辑。
如果你也在做类似的接入和排障,可以按这个顺序走:先到 API Keys 页面(https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite )创建 Key,再对照接入文档(https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite )把 Codex 的config.toml或 Claude Code 的settings.json配好。配好之后,像cJSON_Parse判空、parse_ip_json_str返回值处理、chunk.memory指针访问这类问题,都可以直接贴给模型让它逐条核对。如果你需要长期在编码和 Agent 场景里用,可以看一下 Coding Plan(https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite );如果只是想先验证模型对话能不能通,用模型对话页面(https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite )发一条请求即可。