排程用curl下載API資料,伺服器明明回傳404,後面的工作卻照常執行。先別急著把錯誤歸到shell:curl預設把「完成HTTP交換」視為成功,資源不存在則是HTTP回應裡的另一層結果。兩者分開看,才能決定程式該停下來,還是保留錯誤內容供查閱。
先分清退出碼與HTTP狀態碼
shell讀到的退出碼是本次curl程序的結果;HTTP狀態碼由伺服器放在回應中。成功連線、送出請求並收到完整404回應,可以同時得到HTTP 404與程序退出碼0。這不表示檔案存在,也不表示API處理了你要做的事。
反過來,DNS解析失敗或連線中斷時,curl也可能根本沒拿到HTTP回應。把所有非零退出碼都寫成「API回傳404」,會把網路問題與應用程式錯誤混在一起。診斷時至少保留退出碼、HTTP狀態,以及適合公開的錯誤內容;不要把憑證或完整敏感回應直接印到共用紀錄。
同一個404,三種選項的結果不同
下面的對照使用curl 8.21.0與自有隔離Python HTTP服務。服務的/missing固定回傳404和一小段JSON,/ok則回傳200。服務沒有對外開放連接埠,也沒有操作真實API;這個案例只用來確認命令行的錯誤處理。
default HTTP 404 exit 0 保留錯誤正文
--fail HTTP 404 exit 22 不輸出錯誤正文
--fail-with-body HTTP 404 exit 22 保留錯誤正文
--fail-with-body HTTP 200 exit 0 保留成功正文
若後面的步驟只看退出碼,預設命令就會放行404。加入–fail後,這次404變成退出碼22;加入–fail-with-body也得到22,但仍可讀到伺服器傳回的錯誤說明。兩個選項解決的是不同需求,並不是正文越少就一定越安全。
–fail-with-body從curl 7.76.0加入,舊機器先跑curl –version核對。沒有這個選項時,不要直接照抄後宣稱相同效果;可以先用–fail,或另行保存HTTP狀態再依服務契約判斷。不同系統的curl可能帶不同版本,不能只靠作業系統名稱猜測。
需要錯誤JSON時,用fail-with-body並立即保存退出碼
若要在自己的開發環境重現,可先啟動下面這個Python測試服務,再從另一個終端執行curl。它只監聽本機位址;不要把示範服務放到正式網站,也不要改成對外監聽來方便測試。執行時保持服務終端開著,結束後用Ctrl+C停止。
from http.server import BaseHTTPRequestHandler, HTTPServer
class Demo(BaseHTTPRequestHandler):
def do_GET(self):
ok = self.path == "/ok"
body = (b'{"ok":true}\n' if ok
else b'{"error":"not found"}\n')
self.send_response(200 if ok else 404)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
HTTPServer(("127.0.0.1", 8080), Demo).serve_forever()
把程式存成demo.py,用python3 demo.py執行。若連接埠已被自己的其他工作占用,就同時修改服務與curl網址的連接埠,不要停止用途不明的程序。先請求/ok確認回應200,再改成/missing。這樣能分清服務未啟動的連線失敗,與服務正常回覆的404。
示範沒有驗證身分、資料庫或第三方服務,狀態由路徑直接決定。它適合檢查下載腳本對HTTP錯誤的分支,卻不能證明你的正式API一定使用相同錯誤格式。移到真實服務時,仍要查它的文件:有些API回傳JSON錯誤,有些回傳HTML,直接把後者交給JSON解析器只會產生另一個錯誤。
url="http://127.0.0.1:8080/missing"
curl --silent --show-error \
--fail-with-body \
--output response.json \
--write-out "http=%{http_code}\n" \
"$url"
rc=$?
printf "curl exit=%s\n" "$rc"
這段會把正文放進response.json,把HTTP狀態印在終端,並在下一行立即保存退出碼。–silent關掉進度表,–show-error仍顯示錯誤;兩個選項不會改變HTTP是否成功的判定。案例得到http=404、curl exit=22,檔案內容是{“error”:”not found”}。
不要在curl與rc=$?之間插入echo、cat或其他命令,否則你保存的可能是那個命令的退出碼。若現有腳本啟用了set -e,curl非零也可能讓腳本在賦值前離開。這時可以把curl放在if條件中,明確走成功與失敗兩條分支,而不是依賴未寫清楚的退出規則。
測三個選項時,請各自選一個新的輸出路徑。例如預設命令用default.json,–fail用fail.json,–fail-with-body用fail-body.json。測完再讀對應的退出碼與檔案,才能避免把先前成功下載的內容當成本次結果;測試紀錄也能直接對照命令與正文,不必猜測哪個選項覆蓋了哪份資料。
if curl --silent --show-error \
--fail-with-body \
--output response.json \
"$url"; then
printf "HTTP交換通過curl的錯誤檢查\n"
else
rc=$?
printf "curl失敗,退出碼=%s\n" "$rc" >&2
exit "$rc"
fi
失敗分支仍須先保存$?,再輸出訊息。保留下來的正文只能當成錯誤資料,不能在非零結果後直接當成功JSON繼續匯入。若工作流程希望記錄內容後再退出,可以在這個分支增加有範圍的讀取與遮蔽處理;不要因為「檔案有內容」就跳過判斷。
不需要錯誤正文時,fail比較直接
–fail適合下載失敗就中止,而且不需要保留回應正文的工作。它讓這次404變成非零結果,但不能當成所有HTTP情況的完整保證;官方手冊也指出驗證相關的401、407等情況可能有例外。對結果有嚴格要求的程式,仍應核對HTTP狀態與服務規則。
另外,錯誤時沒有新正文,不代表你指定的路徑一定不存在。若同名檔案早已留下上一輪資料,讀取舊檔就會造成誤判。本次對照每次在新的容器檔案系統執行,所以「沒有正文檔案」是這個乾淨案例的結果。實際排程可用每次獨立的暫存路徑,通過檢查後再交給下一步。
退出碼0也還沒完成業務驗證
若排程原本使用curl … | 某個解析器,把錯誤判斷直接加到管線裡還要再留意:shell通常報的是最後一個命令的結果,不一定是curl。最小且容易查閱的做法,是先分開下載與解析兩步,保存本次curl的結果,再決定是否交給解析器。等這條分支清楚之後,才評估是否需要管線以及額外的錯誤傳遞設定。
也別只搜尋輸出裡有沒有「error」這個字。正常正文可能包含這個欄位名稱,HTML錯誤頁卻未必有它。退出碼與HTTP狀態提供的是結構化訊號,正文則補充原因;三者一起留下,下一次排查才能還原真正發生了什麼。
–fail-with-body主要把HTTP 400以上視為錯誤,不會替你保證拿到特定的200,也不會判定JSON內容符合業務需求。重新導向、空回應,以及200正文裡的業務錯誤,都可能需要另外處理。API若要求一定取得200,就依它的契約明確比較狀態,再檢查內容類型與必要欄位。
排查時先保留一筆可對照的結果:本次請求網址的安全版本、curl版本、退出碼、HTTP狀態與正文存放位置。能重現404與200兩條路徑之後,再把命令放回排程。不要為了讓測試變成成功而關閉TLS驗證,也不要在尚未確定請求是否可重複執行時,加上無限制重試。
最後把後續步驟的入口收緊:只有通過你實際需要的HTTP與內容檢查,才解析或匯入資料。這樣退出碼0、狀態200與業務完成各有清楚的位置,下載腳本才不會把錯誤JSON當成新資料送進網站。

評論0