160 lines
4.5 KiB
Go
160 lines
4.5 KiB
Go
// Package httpapi exposes the Go backend's process health and database
|
|
// readiness endpoints without wiring them into the production application.
|
|
package httpapi
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
const appID = "zhinian-web-studio"
|
|
|
|
const defaultReadinessTimeout = 3 * time.Second
|
|
|
|
// DatabaseStatus is the database configuration state exposed by health and
|
|
// readiness responses.
|
|
type DatabaseStatus struct {
|
|
Backend string `json:"backend"`
|
|
Configured bool `json:"configured"`
|
|
}
|
|
|
|
// Readiness is the HTTP module's seam to database configuration and probing.
|
|
type Readiness interface {
|
|
Status() DatabaseStatus
|
|
Ready(context.Context) (DatabaseStatus, error)
|
|
}
|
|
|
|
type handler struct {
|
|
readiness Readiness
|
|
readinessTimeout time.Duration
|
|
healthDetails HealthDetails
|
|
healthProvider func(context.Context) HealthDetails
|
|
}
|
|
|
|
// HealthDetails contains runtime compatibility fields assembled by the
|
|
// application. Keeping this as injected data prevents the transport layer
|
|
// from coupling itself to process environment configuration.
|
|
type HealthDetails struct {
|
|
VisualAPIMode string `json:"visualApiMode"`
|
|
EvolinkMode string `json:"evolinkMode"`
|
|
SeedanceMode string `json:"seedanceMode"`
|
|
BailianMode string `json:"bailianMode"`
|
|
MinimaxMode string `json:"minimaxMode"`
|
|
AuthMode string `json:"authMode"`
|
|
Capabilities []any `json:"capabilities"`
|
|
}
|
|
|
|
// Option configures the HTTP handler.
|
|
type Option func(*handler)
|
|
|
|
// WithReadinessTimeout sets the maximum duration of a database readiness probe.
|
|
func WithReadinessTimeout(timeout time.Duration) Option {
|
|
return func(h *handler) {
|
|
if timeout > 0 {
|
|
h.readinessTimeout = timeout
|
|
}
|
|
}
|
|
}
|
|
|
|
// WithHealthDetails injects the provider, authentication, and capability
|
|
// summary exposed by the TypeScript-compatible health contract.
|
|
func WithHealthDetails(details HealthDetails) Option {
|
|
return func(h *handler) {
|
|
h.healthDetails = details
|
|
if h.healthDetails.Capabilities == nil {
|
|
h.healthDetails.Capabilities = []any{}
|
|
}
|
|
}
|
|
}
|
|
|
|
// WithHealthDetailsProvider evaluates mutable provider state for every health
|
|
// request while preserving /api/health as a non-blocking liveness endpoint.
|
|
func WithHealthDetailsProvider(provider func(context.Context) HealthDetails) Option {
|
|
return func(h *handler) {
|
|
h.healthProvider = provider
|
|
}
|
|
}
|
|
|
|
// NewHandler returns the foundation health/readiness HTTP handler.
|
|
func NewHandler(readiness Readiness, options ...Option) http.Handler {
|
|
h := &handler{readiness: readiness, readinessTimeout: defaultReadinessTimeout, healthDetails: HealthDetails{Capabilities: []any{}}}
|
|
for _, option := range options {
|
|
option(h)
|
|
}
|
|
return h
|
|
}
|
|
|
|
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/api/health" && r.URL.Path != "/api/ready" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
if r.Method != http.MethodGet {
|
|
w.Header().Set("Allow", http.MethodGet)
|
|
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
if r.URL.Path == "/api/ready" {
|
|
h.serveReady(w, r)
|
|
return
|
|
}
|
|
|
|
status := DatabaseStatus{Backend: "invalid", Configured: false}
|
|
if h.readiness != nil {
|
|
status = h.readiness.Status()
|
|
}
|
|
details := h.healthDetails
|
|
if h.healthProvider != nil {
|
|
details = h.healthProvider(r.Context())
|
|
if details.Capabilities == nil {
|
|
details.Capabilities = []any{}
|
|
}
|
|
}
|
|
writeJSON(w, http.StatusOK, struct {
|
|
OK bool `json:"ok"`
|
|
AppID string `json:"appId"`
|
|
WebOnly bool `json:"webOnly"`
|
|
HealthDetails
|
|
Database DatabaseStatus `json:"database"`
|
|
}{
|
|
OK: true,
|
|
AppID: appID,
|
|
WebOnly: true,
|
|
HealthDetails: details,
|
|
Database: status,
|
|
})
|
|
}
|
|
|
|
func (h *handler) serveReady(w http.ResponseWriter, r *http.Request) {
|
|
status := DatabaseStatus{Backend: "invalid", Configured: false}
|
|
if h.readiness == nil {
|
|
writeReadyJSON(w, http.StatusServiceUnavailable, false, status)
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(r.Context(), h.readinessTimeout)
|
|
defer cancel()
|
|
status, err := h.readiness.Ready(ctx)
|
|
if err != nil {
|
|
writeReadyJSON(w, http.StatusServiceUnavailable, false, status)
|
|
return
|
|
}
|
|
writeReadyJSON(w, http.StatusOK, true, status)
|
|
}
|
|
|
|
func writeReadyJSON(w http.ResponseWriter, status int, ok bool, database DatabaseStatus) {
|
|
writeJSON(w, status, struct {
|
|
OK bool `json:"ok"`
|
|
Database DatabaseStatus `json:"database"`
|
|
}{OK: ok, Database: database})
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, status int, body any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
_ = json.NewEncoder(w).Encode(body)
|
|
}
|