Files
easyai-ai-gateway/apps/api/internal/httpapi/task_idempotency.go
T
wangbo 0f0998cbcf feat(storage): 统一二进制对象存储与公开错误
新增 Aliyun OSS 与 S3 协议、通道内重试和按优先级跨通道切换,保留 server-main 兼容与环境 OSS 内存通道。

将请求及结果中的 Base64、Data URI、Buffer、multipart 和内联二进制统一对象化,生产路径不再写入本机静态目录,历史本地资源仅保留只读兼容。

引入 PublicErrorV1 并统一 API、异步查询、兼容协议和失败回调的安全错误输出,同时补充迁移、管理端、指标、OpenAPI 与本地模拟验收。

验证:go test ./... -count=1;go vet ./...;pnpm lint;pnpm test;pnpm build;pnpm openapi;tests/ci/migrations-test.sh。
2026-08-04 08:14:39 +08:00

90 lines
2.7 KiB
Go

package httpapi
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"net/http"
"strings"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)
var errInvalidTaskIdempotencyKey = errors.New("invalid Idempotency-Key header")
func optionalTaskIdempotencyKey(r *http.Request) (string, bool, error) {
values := r.Header.Values("Idempotency-Key")
if len(values) == 0 {
return "", false, nil
}
if len(values) != 1 {
return "", false, errInvalidTaskIdempotencyKey
}
value := strings.TrimSpace(values[0])
if value == "" || len(value) > 255 || strings.Contains(value, ",") {
return "", false, errInvalidTaskIdempotencyKey
}
return value, true, nil
}
func taskIdempotencyKeyHash(key string) string {
digest := sha256.Sum256([]byte(key))
return hex.EncodeToString(digest[:])
}
func taskIdempotencyRequestHash(kind string, async bool, stream bool, body map[string]any) string {
payload, _ := json.Marshal(map[string]any{
"kind": kind, "async": async, "stream": stream, "request": body,
})
digest := sha256.Sum256(payload)
return hex.EncodeToString(digest[:])
}
func writeIdempotentTaskReplay(w http.ResponseWriter, task store.GatewayTask, compatible bool) {
w.Header().Set("Idempotent-Replayed", "true")
w.Header().Set("X-Gateway-Task-Id", task.ID)
if !compatible || task.AsyncMode || (task.Status != "succeeded" && task.Status != "failed" && task.Status != "cancelled") {
writeTaskAccepted(w, task)
return
}
if task.Status == "succeeded" {
writeJSON(w, http.StatusOK, task.Result)
return
}
status := storedTaskErrorStatus(task.ErrorCode)
message := strings.TrimSpace(task.ErrorMessage)
if message == "" {
message = strings.TrimSpace(task.Error)
}
if message == "" {
message = "task failed"
}
code := strings.TrimSpace(task.ErrorCode)
if code == "" {
code = "task_failed"
}
writeError(w, status, message, code)
}
func storedTaskErrorStatus(code string) int {
switch strings.TrimSpace(code) {
case "storage_write_failed", "storage_read_failed", "storage_config_invalid", "storage_auth_failed":
return http.StatusServiceUnavailable
case "binary_result_expired", "result_expired", "result_unavailable":
return http.StatusGone
case "pricing_unavailable", "response_chain_unavailable", "billing_hold":
return http.StatusServiceUnavailable
case "insufficient_balance":
return http.StatusPaymentRequired
case "bad_request", "invalid_parameter", "invalid_previous_response_id", "unsupported_operation":
return http.StatusBadRequest
case "no_model_candidate", "cloned_voice_not_found":
return http.StatusNotFound
case "rate_limit", "platform_cooling_down", "model_cooling_down":
return http.StatusTooManyRequests
default:
return http.StatusBadGateway
}
}