Files
easyai-ai-gateway/apps/api/internal/httpapi/response.go
T
wangbo b13392ef50 fix(media): 统一图片结果 URL 化并限制同步 Base64
将上游 URL 直接持久化,内联媒体经对象存储后仅保留 URL 与内部定位元数据;异步轮询、任务详情和幂等重放统一使用零对象读取的 URL 投影,并增加 64KiB 响应门禁。

OpenAI 图片接口接受 url 与 b64_json,同步 Base64 限制为 20MiB 和每 Pod 2 并发;新增历史结果迁移清零门禁、结果指标和 API GOMEMLIMIT。

验证:API go test ./...、go vet、聚焦 race、pnpm openapi、pnpm lint/test/build、迁移安全检查与 docker compose config 均通过。
2026-08-05 18:15:06 +08:00

94 lines
3.1 KiB
Go

package httpapi
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/publicerror"
)
const maxAsyncMediaResultResponseBytes = 64 << 10
func writeJSON(w http.ResponseWriter, status int, value any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(value)
}
func writeAsyncMediaResultJSON(w http.ResponseWriter, value any, observer interface {
ObserveResultDelivery(string, int64)
}) {
payload, err := json.Marshal(value)
if err != nil {
writeError(w, http.StatusInternalServerError, "encode task result failed", "internal_error")
return
}
if len(payload) > maxAsyncMediaResultResponseBytes {
if observer != nil {
observer.ObserveResultDelivery("poll_oversized", int64(len(payload)))
}
writeErrorWithDetails(w, http.StatusInternalServerError, "task result response exceeds the URL-only size limit", map[string]any{
"limit_bytes": maxAsyncMediaResultResponseBytes,
"actual_bytes": len(payload),
}, "result_response_too_large")
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("X-Gateway-Response-Format", "url")
w.Header().Set("Deprecation", "true")
w.Header().Set("X-Gateway-Deprecated-Fields", "output_content,result")
w.WriteHeader(http.StatusOK)
_, _ = w.Write(append(payload, '\n'))
if observer != nil {
observer.ObserveResultDelivery("poll_success", int64(len(payload)+1))
}
}
func writeError(w http.ResponseWriter, status int, message string, codes ...string) {
writeErrorWithDetails(w, status, message, nil, codes...)
}
func writeErrorWithDetails(w http.ResponseWriter, status int, message string, details map[string]any, codes ...string) {
code := ""
if len(codes) > 0 {
code = strings.TrimSpace(codes[0])
}
standard := publicerror.FromFields(code, message, status, status == http.StatusRequestTimeout || status == http.StatusTooManyRequests || status >= 500)
standard = publicErrorWithRetryAfter(standard, details)
publicerror.Observe(standard)
status = standard.HTTPStatus
errorPayload := map[string]any{
"message": standard.Message,
"status": status,
"code": standard.Code,
"category": standard.Category,
"source": standard.Source,
"httpStatus": standard.HTTPStatus,
"retryable": standard.Retryable,
"action": standard.Action,
"version": standard.Version,
}
if standard.Code == "invalid_parameter" || standard.Code == "unsupported_response_parameter" {
errorPayload["type"] = "invalid_request_error"
if _, ok := details["param"]; !ok {
errorPayload["param"] = nil
}
}
for key, value := range safePublicErrorDetails(details, standard, false) {
errorPayload[key] = value
}
writeJSON(w, status, map[string]any{"error": errorPayload})
}
func writeLocalUserRequired(w http.ResponseWriter) {
writeError(w, http.StatusForbidden, "该账号尚未开通 EasyAI Gateway", errorCodeGatewayUserNotProvisioned)
}
func sendSSE(w http.ResponseWriter, event string, payload any) {
bytes, _ := json.Marshal(payload)
_, _ = fmt.Fprintf(w, "event: %s\n", event)
_, _ = fmt.Fprintf(w, "data: %s\n\n", bytes)
}