Files
NianAIGC/backend/internal/httpapi/httpapi.go

117 lines
3.0 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
}
// 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
}
}
}
// NewHandler returns the foundation health/readiness HTTP handler.
func NewHandler(readiness Readiness, options ...Option) http.Handler {
h := &handler{readiness: readiness, readinessTimeout: defaultReadinessTimeout}
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()
}
writeJSON(w, http.StatusOK, struct {
OK bool `json:"ok"`
AppID string `json:"appId"`
WebOnly bool `json:"webOnly"`
Database DatabaseStatus `json:"database"`
}{
OK: true,
AppID: appID,
WebOnly: true,
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)
}