From c12e16c9f7b06e1389c3902aa50963e5d31d0225 Mon Sep 17 00:00:00 2001 From: Hein Date: Sat, 11 Apr 2026 21:03:29 +0200 Subject: [PATCH] fix(server): handle router setup panic and return error * return error from New function if route registration panics * add googleDispatch to handle model:action routing --- cmd/vecna/serve.go | 5 ++++- pkg/server/google.go | 31 +++++++++++++++++++++++++++++-- pkg/server/server.go | 20 +++++++++++++++----- pkg/server/trace.go | 5 ++++- 4 files changed, 52 insertions(+), 9 deletions(-) diff --git a/cmd/vecna/serve.go b/cmd/vecna/serve.go index 6dd3f72..3181b38 100644 --- a/cmd/vecna/serve.go +++ b/cmd/vecna/serve.go @@ -58,7 +58,10 @@ func runServe(cmd *cobra.Command, _ []string) error { reg = metrics.New() } - router := server.New(cfg, clients, adp, reg, logger) + router, err := server.New(cfg, clients, adp, reg, logger) + if err != nil { + return fmt.Errorf("build router: %w", err) + } addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port) srv := &http.Server{ diff --git a/pkg/server/google.go b/pkg/server/google.go index da9e036..0c39ca1 100644 --- a/pkg/server/google.go +++ b/pkg/server/google.go @@ -1,8 +1,10 @@ package server import ( + "context" "encoding/json" "net/http" + "strings" "time" "github.com/uptrace/bunrouter" @@ -10,6 +12,31 @@ import ( "github.com/Warky-Devs/vecna.git/pkg/embedclient" ) +// googleDispatch parses the wildcard "model:action" segment and routes to the +// correct Google handler. The colon is a literal method separator in the +// Google embedding API, not a bunrouter parameter prefix. +func (h *handler) googleDispatch(w http.ResponseWriter, req bunrouter.Request) error { + modelaction := req.Param("modelaction") // e.g. "text-embedding-foo:embedContent" + idx := strings.LastIndex(modelaction, ":") + if idx < 0 { + return writeJSON(w, http.StatusNotFound, map[string]string{"error": "invalid Google API path"}) + } + model := modelaction[:idx] + action := modelaction[idx+1:] + + ctx := context.WithValue(req.Context(), modelKey, model) + req = req.WithContext(ctx) + + switch action { + case "embedContent": + return h.googleEmbedContent(w, req) + case "batchEmbedContents": + return h.googleBatchEmbedContents(w, req) + default: + return writeJSON(w, http.StatusNotFound, map[string]string{"error": "unknown Google API method: " + action}) + } +} + // --- single embedContent --- type googleEmbedContentRequest struct { @@ -34,7 +61,7 @@ type googleEmbeddingValues struct { } func (h *handler) googleEmbedContent(w http.ResponseWriter, req bunrouter.Request) error { - model := req.Param("model") + model, _ := req.Context().Value(modelKey).(string) var body googleEmbedContentRequest if err := json.NewDecoder(req.Body).Decode(&body); err != nil { @@ -89,7 +116,7 @@ type googleBatchResponse struct { } func (h *handler) googleBatchEmbedContents(w http.ResponseWriter, req bunrouter.Request) error { - model := req.Param("model") + model, _ := req.Context().Value(modelKey).(string) var body googleBatchRequest if err := json.NewDecoder(req.Body).Decode(&body); err != nil { diff --git a/pkg/server/server.go b/pkg/server/server.go index 2d9ca27..e560bab 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -18,14 +18,22 @@ import ( ) // New builds and returns a configured bunrouter.Router. +// Returns an error if route registration panics (e.g. conflicting routes). func New( cfg *config.Config, clients map[string]embedclient.Client, adp adapter.Adapter, reg *metrics.Registry, logger *zap.Logger, -) *bunrouter.Router { - router := bunrouter.New( +) (router *bunrouter.Router, err error) { + defer func() { + if r := recover(); r != nil { + logger.Error("panic during router setup", zap.Any("recover", r)) + err = fmt.Errorf("router setup panic: %v", r) + } + }() + + router = bunrouter.New( bunrouter.WithMiddleware(authMiddleware(cfg.Server.APIKeys)), bunrouter.WithMiddleware(traceMiddleware()), bunrouter.WithMiddleware(metricsMiddleware(reg, adp)), @@ -35,8 +43,10 @@ func New( h := &handler{cfg: cfg, clients: clients, adapter: adp, logger: logger} router.POST("/v1/embeddings", h.openAIEmbeddings) - router.POST("/v1/models/:model:embedContent", h.googleEmbedContent) - router.POST("/v1/models/:model:batchEmbedContents", h.googleBatchEmbedContents) + // Google API uses a literal colon as a method separator (e.g. /v1/models/foo:embedContent). + // bunrouter can't distinguish two routes with the same :param prefix, so a single wildcard + // captures the full "model:action" segment and dispatches internally. + router.POST("/v1/models/*modelaction", h.googleDispatch) // OpenAPI spec + docs router.GET("/openapi.yaml", spec.SpecHandler()) @@ -59,7 +69,7 @@ func New( } } - return router + return router, nil } // authMiddleware rejects requests without a valid Bearer token when api_keys is configured. diff --git a/pkg/server/trace.go b/pkg/server/trace.go index 20f0060..57b89f5 100644 --- a/pkg/server/trace.go +++ b/pkg/server/trace.go @@ -7,7 +7,10 @@ import ( type contextKey int -const traceKey contextKey = iota +const ( + traceKey contextKey = iota + modelKey +) // RequestTrace holds per-request timing data populated by handlers and middleware. type RequestTrace struct {