mirror of
https://github.com/hanzoai/docdb.git
synced 2026-10-30 18:39:39 +00:00
The Data API, MCP and debug listeners each hand-rolled a net/http.ServeMux. They now declare routes on one zip.App built by internal/util/zipapp, so ZAP is the transport and HTTP is a view of the same routes. zipapp keeps the net/http error rendering (plain-text 404/405, nosniff) so no client sees a new response shape; zipapp_test asserts that parity directly. zip.AdaptNetHTTP is the single escape hatch for the handlers that are genuinely net/http (pprof, expvar, statsviz, promhttp) — there is no second adapter. This work existed only locally: main's history was rewritten this morning and took it with it, so it is replayed here against current main rather than merged from the pre-rewrite lineage, and lands straight on the fleet-standard v1.17.2 instead of the v1.12.0 it was written against. zip v1.17.x requires go >= 1.26.5, hence the directive bump, and pulls zap-proto/http v0.3.1 — peers must be on v0.3.x to talk to it. Co-authored-by: Hanzo Dev <[email protected]>
136 lines
4.3 KiB
Go
136 lines
4.3 KiB
Go
// Copyright 2021 Hanzo AI Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
// Package zipapp is the single way DocDB builds and runs an HTTP router.
|
|
//
|
|
// Every DocDB listener (Data API, MCP, debug) declares its routes on a
|
|
// [zip.App] built by [New] and serves it on an already-bound [net.Listener]
|
|
// with [Serve]. Routing errors are rendered exactly as [net/http.ServeMux]
|
|
// renders them, so routes keep the status codes and bodies they had before
|
|
// the router moved to zip.
|
|
package zipapp
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log/slog"
|
|
"net"
|
|
"net/http"
|
|
|
|
luxlog "github.com/luxfi/log"
|
|
"github.com/zap-proto/fiber/v3"
|
|
"github.com/zap-proto/zip"
|
|
|
|
"github.com/hanzoai/docdb/internal/util/ctxutil"
|
|
"github.com/hanzoai/docdb/internal/util/logging"
|
|
)
|
|
|
|
// New returns an app named name with no routes.
|
|
//
|
|
// The app is silent: DocDB logs listener lifecycle itself via [Serve],
|
|
// and zip's own logger would duplicate it in a different format.
|
|
func New(name string) *zip.App {
|
|
return zip.New(zip.Config{
|
|
AppName: name,
|
|
Logger: luxlog.Noop(),
|
|
DisableStartupMessage: true,
|
|
ErrorHandler: errorHandler,
|
|
})
|
|
}
|
|
|
|
// BaseContext returns the middleware that makes ctx the parent of every
|
|
// request's context, the way [http.Server.BaseContext] did: a handler's context
|
|
// carries the listener's values and is canceled when the listener stops.
|
|
//
|
|
// It must be the first middleware an app uses, so that everything downstream
|
|
// derives from it.
|
|
func BaseContext(ctx context.Context) zip.Handler {
|
|
return func(c *zip.Ctx) error {
|
|
c.SetContext(ctx)
|
|
|
|
return c.Next()
|
|
}
|
|
}
|
|
|
|
// Text writes msg with the given status code exactly as [http.Error] does.
|
|
func Text(c *zip.Ctx, code int, msg string) error {
|
|
return text(c.Fiber(), code, msg)
|
|
}
|
|
|
|
// text is [Text] on the underlying Fiber context, so [errorHandler] can share it.
|
|
func text(c fiber.Ctx, code int, msg string) error {
|
|
c.Set(fiber.HeaderContentType, "text/plain; charset=utf-8")
|
|
c.Set("X-Content-Type-Options", "nosniff")
|
|
c.Status(code)
|
|
|
|
return c.SendString(msg + "\n")
|
|
}
|
|
|
|
// errorHandler renders an error the way net/http would.
|
|
//
|
|
// Unmatched paths and methods are the only errors DocDB handlers surface to the
|
|
// router (they write their own responses otherwise), and [http.ServeMux] answers
|
|
// those with plain text; anything else falls back to plain text too, never to a
|
|
// framework-shaped JSON body that no DocDB client expects.
|
|
func errorHandler(c fiber.Ctx, err error) error {
|
|
code := http.StatusInternalServerError
|
|
msg := err.Error()
|
|
|
|
var fe *fiber.Error
|
|
if errors.As(err, &fe) {
|
|
code = fe.Code
|
|
msg = fe.Message
|
|
}
|
|
|
|
switch code {
|
|
case http.StatusNotFound:
|
|
msg = "404 page not found"
|
|
case http.StatusMethodNotAllowed:
|
|
msg = "Method Not Allowed"
|
|
}
|
|
|
|
return text(c, code, msg)
|
|
}
|
|
|
|
// Serve runs app on the given already-bound listener until ctx is canceled.
|
|
//
|
|
// It exits when the handler is stopped and the listener closed.
|
|
// name is used for the "<name> server stopped" message.
|
|
func Serve(ctx context.Context, name string, app *zip.App, lis net.Listener, l *slog.Logger) {
|
|
done := make(chan struct{})
|
|
|
|
go func() {
|
|
defer close(done)
|
|
|
|
err := app.Fiber().Listener(lis, fiber.ListenConfig{DisableStartupMessage: true})
|
|
if err != nil && !errors.Is(err, net.ErrClosed) {
|
|
l.LogAttrs(ctx, logging.LevelDPanic, "Serve exited with unexpected error", logging.Error(err))
|
|
}
|
|
}()
|
|
|
|
<-ctx.Done()
|
|
|
|
// ctx is already canceled, but we want to inherit its values
|
|
shutdownCtx, shutdownCancel := ctxutil.WithDelay(ctx)
|
|
defer shutdownCancel(nil)
|
|
|
|
if err := app.Fiber().ShutdownWithContext(shutdownCtx); err != nil && !errors.Is(err, fiber.ErrNotRunning) {
|
|
l.LogAttrs(ctx, logging.LevelDPanic, "Shutdown exited with unexpected error", logging.Error(err))
|
|
}
|
|
|
|
<-done
|
|
|
|
l.InfoContext(ctx, name+" server stopped")
|
|
}
|