DB 接入塊 0-5 上主幹後的收尾工作,讓 DB-on 模式可真人使用 + 補齊功能與測試。 OIDC / pairing FK 修復(接 DB 上線必要): - 新建 internal/user package(User + Store + InMemory + Postgres);OIDC callback 驗證 id_token 成功後 fail-closed upsert users(sub 直接當 users.id,MC sub 為 UUID) - pairing exchange 雲端自建 device(不動 local-tool)+ 同 tx 綁 session token; 自建 device 空 serial 寫 NULL(避免撞 partial unique) - device.SaveTx / session.CreateTx 新增 tx-aware 版本 B4 model metadata: - 轉檔 result 的 analysis_info(input_shape/classes/framework)串進 model: converter_client → flow → adapter → model.Model → PG → ModelResponse DTO - input_shape 優先用陣列、後備四維組 NCHW、缺一不亂組;全 optional 防禦性 - 前端詳細頁顯示(另 repo);轉檔端串接交接檔 b4-converter-handoff.md nginx healthz(部署層): - 新增 /healthz/deep 轉發 backend(ping PG+Redis、down 回 503)給 LB - 修掉 default_server return 444 短路 bug(docker healthcheck 長期 unhealthy 真因) storage error 統一映射(不洩漏 storage 後端細節)。 測試:補 internal/api(storage/errors handler)、cmd/api-server(seed/adapter)、 internal/db(redis)、relay/session 弱處,含 testcontainers integration。 DB 接入相關 package 真環境覆蓋達 88-94%。全程 Reviewer 審查 + 130 真 PG/Redis dbtest 綠。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
127 lines
5.9 KiB
Go
127 lines
5.9 KiB
Go
// errors_db.go — 統一把 DB(pgx)錯誤映射成對外 API 錯誤(DB 接入塊 5.4)。
|
||
//
|
||
// 兩個目的:
|
||
// 1. **fail-fast 落地**:PG 連線失敗 / context 逾時 → 503 SERVICE_UNAVAILABLE(不回假資料、
|
||
// 不誤報 500),讓 load balancer 把這台拉出。對齊使用者拍板的「PG 都 fail-fast、不自動降級」。
|
||
// 2. **收掉塊 3 M1 技術債**:handler 過去把 raw DB error 字串串進 response
|
||
// (`"... failed: "+err.Error()`)會洩漏 schema / SQL / 連線細節。本檔統一映射,
|
||
// 對外只給穩定的 error code + 通用 message,raw error 只進 server log(含 request_id)。
|
||
//
|
||
// 映射規則(依優先序):
|
||
//
|
||
// | 來源 | HTTP | code |
|
||
// |----------------------------------------|------|-----------------------|
|
||
// | context 逾時 / 取消(DeadlineExceeded / Canceled)| 503 | SERVICE_UNAVAILABLE |
|
||
// | 連線層失敗(pgconn 無 SQLSTATE:refused/reset/EOF)| 503 | SERVICE_UNAVAILABLE |
|
||
// | unique violation(SQLSTATE 23505) | 409 | CONFLICT |
|
||
// | 其餘有 SQLSTATE 的 PG 錯誤(語法/約束等)| 500 | INTERNAL_ERROR |
|
||
// | 非 DB 錯誤 / 未知 | 500 | INTERNAL_ERROR |
|
||
//
|
||
// ⚠️ not found 不在此映射:domain 層(device.ErrNotFound / auth.ErrInvalidToken 等)已把
|
||
// pgx.ErrNoRows 轉成自己的 sentinel,handler 應先用 errors.Is 比對那些 sentinel 回 404,
|
||
// **再**把剩下的「真 DB 錯誤」交給 WriteDBError。這樣「正常的 not found」絕不會被誤判成 503。
|
||
package api
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"log/slog"
|
||
"net/http"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"github.com/jackc/pgx/v5/pgconn"
|
||
)
|
||
|
||
// dbErrorClass 是 DB 錯誤分類結果。
|
||
type dbErrorClass struct {
|
||
status int
|
||
code string
|
||
message string // 對外通用訊息(不含 raw DB 細節)
|
||
}
|
||
|
||
// classifyDBError 把一個 error 分類為對外的 (HTTP status, code, message)。
|
||
//
|
||
// 不洩漏 raw DB error:回傳的 message 是固定通用字串,raw error 由 WriteDBError 寫進 server log。
|
||
func classifyDBError(err error) dbErrorClass {
|
||
// 1) context 逾時 / 取消 → 503(依賴慢/不可用,fail-fast)。
|
||
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
|
||
return dbErrorClass{http.StatusServiceUnavailable, ErrCodeServiceUnavailable,
|
||
"service temporarily unavailable"}
|
||
}
|
||
|
||
// 2) pgconn.PgError:PG server 端回的錯誤(有 SQLSTATE)。
|
||
var pgErr *pgconn.PgError
|
||
if errors.As(err, &pgErr) {
|
||
switch pgErr.Code {
|
||
case "23505": // unique_violation
|
||
return dbErrorClass{http.StatusConflict, ErrCodeConflict, "resource already exists"}
|
||
default:
|
||
// 其餘 PG 錯誤(語法、約束、權限…)對外一律 500,不洩漏 SQLSTATE / 欄位名。
|
||
return dbErrorClass{http.StatusInternalServerError, ErrCodeInternalError,
|
||
"internal error"}
|
||
}
|
||
}
|
||
|
||
// 3) pgconn.ConnectError 等「連不上 / 連線中斷」(無 SQLSTATE)→ 503。
|
||
// pgxpool 連線層失敗(connection refused / reset / EOF)多半包成 *pgconn.ConnectError,
|
||
// 或為底層 net error。用 errors.As 抓 ConnectError;抓不到再保守視為連線問題前先看下一步。
|
||
var connErr *pgconn.ConnectError
|
||
if errors.As(err, &connErr) {
|
||
return dbErrorClass{http.StatusServiceUnavailable, ErrCodeServiceUnavailable,
|
||
"service temporarily unavailable"}
|
||
}
|
||
|
||
// 4) 其餘未知錯誤 → 500(保守,不假設是 DB down 以免把程式 bug 也報成 503)。
|
||
return dbErrorClass{http.StatusInternalServerError, ErrCodeInternalError, "internal error"}
|
||
}
|
||
|
||
// WriteDBError 把一個 DB 操作錯誤映射成對外 API 錯誤並寫回 response,同時把 raw error 進 log。
|
||
//
|
||
// op 是操作描述(如 "get device" / "list models"),只進 log、不對外。
|
||
// 對外只給 classifyDBError 算出的穩定 code + 通用 message(不洩漏 raw DB error)。
|
||
//
|
||
// 用法(handler 內,已先處理過 domain sentinel 如 ErrNotFound 後):
|
||
//
|
||
// if err != nil {
|
||
// WriteDBError(c, deps.Logger, "list devices", err)
|
||
// return
|
||
// }
|
||
func WriteDBError(c *gin.Context, log *slog.Logger, op string, err error) {
|
||
cls := classifyDBError(err)
|
||
|
||
// raw error 只進 server log(附 request_id + 對外 code,方便對照),不進 response。
|
||
logOrDefault(log).Error("db operation failed",
|
||
"op", op,
|
||
"error", err,
|
||
"http_status", cls.status,
|
||
"code", cls.code,
|
||
"request_id", RequestIDFrom(c))
|
||
|
||
WriteError(c, cls.status, cls.code, cls.message, nil)
|
||
}
|
||
|
||
// WriteStorageError 把一個 storage(物件儲存)操作錯誤映射成對外 API 錯誤並寫回 response,
|
||
// 同時把 raw error 進 server log(含 request_id)。
|
||
//
|
||
// 動機(塊 5 Minor-1):presigned URL / Stat 等 storage 操作失敗時,handler 過去把
|
||
// err.Error() 直接串進 response message,可能洩漏 storage 後端細節(bucket / endpoint /
|
||
// 內部路徑 / 簽章參數)給前端。本函式比照 WriteDBError,對外只給穩定 code + 通用 message,
|
||
// raw error 只進 server log。
|
||
//
|
||
// 對外一律 500 INTERNAL_ERROR:storage 後端不可用 / 設定錯誤對前端而言都是「伺服器端問題」,
|
||
// 不細分(與 WriteDBError 的 503 區隔開——DB 是 fail-fast 拉機制的依賴、storage 不是)。
|
||
// 呼叫端應先處理 storage sentinel(如 storage.ErrNotFound)回對應的 4xx,再把剩下的
|
||
// 「真 storage 錯誤」交給本函式。
|
||
//
|
||
// op 是操作描述(如 "presigned put url" / "stat object"),只進 log、不對外。
|
||
func WriteStorageError(c *gin.Context, log *slog.Logger, op string, err error) {
|
||
logOrDefault(log).Error("storage operation failed",
|
||
"op", op,
|
||
"error", err,
|
||
"http_status", http.StatusInternalServerError,
|
||
"code", ErrCodeInternalError,
|
||
"request_id", RequestIDFrom(c))
|
||
|
||
WriteError(c, http.StatusInternalServerError, ErrCodeInternalError, "internal error", nil)
|
||
}
|