fix: resolve 404 on gateway root and unify webhook routing on single port
The health server on port 18790 only handled /health and /ready, returning 404 for all other paths including /. This also meant webhook-based channels (LINE) ran on separate ports not exposed in docker-compose or firewall. Changes: - Add root `/` handler returning gateway info JSON instead of 404 - Expose health server mux so channels can register webhook routes on it - LINE channel now registers webhook on shared gateway mux (port 18790) - Channels still fall back to standalone servers if not using shared mux https://claude.ai/code/session_019vXaqxGmkdCjM8m3jp6rYj
This commit is contained in:
parent
1ea879745b
commit
7ca93d2596
4 changed files with 75 additions and 2 deletions
|
|
@ -659,17 +659,20 @@ func gatewayCmd() {
|
||||||
fmt.Println("✓ Device event service started")
|
fmt.Println("✓ Device event service started")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create the gateway HTTP server first so channels can register webhooks on it
|
||||||
|
healthServer := health.NewServer(cfg.Gateway.Host, cfg.Gateway.Port)
|
||||||
|
channelManager.RegisterWebhooks(healthServer.Mux())
|
||||||
|
|
||||||
if err := channelManager.StartAll(ctx); err != nil {
|
if err := channelManager.StartAll(ctx); err != nil {
|
||||||
fmt.Printf("Error starting channels: %v\n", err)
|
fmt.Printf("Error starting channels: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
healthServer := health.NewServer(cfg.Gateway.Host, cfg.Gateway.Port)
|
|
||||||
go func() {
|
go func() {
|
||||||
if err := healthServer.Start(); err != nil && err != http.ErrServerClosed {
|
if err := healthServer.Start(); err != nil && err != http.ErrServerClosed {
|
||||||
logger.ErrorCF("health", "Health server error", map[string]interface{}{"error": err.Error()})
|
logger.ErrorCF("health", "Health server error", map[string]interface{}{"error": err.Error()})
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
fmt.Printf("✓ Health endpoints available at http://%s:%d/health and /ready\n", cfg.Gateway.Host, cfg.Gateway.Port)
|
fmt.Printf("✓ Gateway listening on http://%s:%d (health, webhooks)\n", cfg.Gateway.Host, cfg.Gateway.Port)
|
||||||
|
|
||||||
go agentLoop.Run(ctx)
|
go agentLoop.Run(ctx)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ type LINEChannel struct {
|
||||||
*BaseChannel
|
*BaseChannel
|
||||||
config config.LINEConfig
|
config config.LINEConfig
|
||||||
httpServer *http.Server
|
httpServer *http.Server
|
||||||
|
useSharedMux bool // true when webhook is registered on the gateway mux
|
||||||
botUserID string // Bot's user ID
|
botUserID string // Bot's user ID
|
||||||
botBasicID string // Bot's basic ID (e.g. @216ru...)
|
botBasicID string // Bot's basic ID (e.g. @216ru...)
|
||||||
botDisplayName string // Bot's display name for text-based mention detection
|
botDisplayName string // Bot's display name for text-based mention detection
|
||||||
|
|
@ -67,6 +68,20 @@ func NewLINEChannel(cfg config.LINEConfig, messageBus *bus.MessageBus) (*LINECha
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RegisterWebhook registers the LINE webhook handler on a shared HTTP mux
|
||||||
|
// (typically the gateway mux) so no separate server is needed.
|
||||||
|
func (c *LINEChannel) RegisterWebhook(mux *http.ServeMux) {
|
||||||
|
path := c.config.WebhookPath
|
||||||
|
if path == "" {
|
||||||
|
path = "/webhook/line"
|
||||||
|
}
|
||||||
|
mux.HandleFunc(path, c.webhookHandler)
|
||||||
|
c.useSharedMux = true
|
||||||
|
logger.InfoCF("line", "LINE webhook registered on gateway", map[string]interface{}{
|
||||||
|
"path": path,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Start launches the HTTP webhook server.
|
// Start launches the HTTP webhook server.
|
||||||
func (c *LINEChannel) Start(ctx context.Context) error {
|
func (c *LINEChannel) Start(ctx context.Context) error {
|
||||||
logger.InfoC("line", "Starting LINE channel (Webhook Mode)")
|
logger.InfoC("line", "Starting LINE channel (Webhook Mode)")
|
||||||
|
|
@ -86,6 +101,13 @@ func (c *LINEChannel) Start(ctx context.Context) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If webhook was already registered on the shared gateway mux, skip creating a standalone server
|
||||||
|
if c.useSharedMux {
|
||||||
|
c.setRunning(true)
|
||||||
|
logger.InfoC("line", "LINE channel started (shared gateway webhook)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
path := c.config.WebhookPath
|
path := c.config.WebhookPath
|
||||||
if path == "" {
|
if path == "" {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ package channels
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/bus"
|
"github.com/sipeed/picoclaw/pkg/bus"
|
||||||
|
|
@ -314,6 +315,19 @@ func (m *Manager) GetEnabledChannels() []string {
|
||||||
return names
|
return names
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RegisterWebhooks registers webhook routes for channels that support HTTP webhooks
|
||||||
|
// on the shared gateway mux, so they share the same port as the health server.
|
||||||
|
func (m *Manager) RegisterWebhooks(mux *http.ServeMux) {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
|
if line, ok := m.channels["line"]; ok {
|
||||||
|
if lc, ok := line.(*LINEChannel); ok {
|
||||||
|
lc.RegisterWebhook(mux)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) RegisterChannel(name string, channel Channel) {
|
func (m *Manager) RegisterChannel(name string, channel Channel) {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import (
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
server *http.Server
|
server *http.Server
|
||||||
|
mux *http.ServeMux
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
ready bool
|
ready bool
|
||||||
checks map[string]Check
|
checks map[string]Check
|
||||||
|
|
@ -30,14 +31,23 @@ type StatusResponse struct {
|
||||||
Checks map[string]Check `json:"checks,omitempty"`
|
Checks map[string]Check `json:"checks,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GatewayInfo struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Uptime string `json:"uptime"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
func NewServer(host string, port int) *Server {
|
func NewServer(host string, port int) *Server {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
s := &Server{
|
s := &Server{
|
||||||
|
mux: mux,
|
||||||
ready: false,
|
ready: false,
|
||||||
checks: make(map[string]Check),
|
checks: make(map[string]Check),
|
||||||
startTime: time.Now(),
|
startTime: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mux.HandleFunc("/", s.rootHandler)
|
||||||
mux.HandleFunc("/health", s.healthHandler)
|
mux.HandleFunc("/health", s.healthHandler)
|
||||||
mux.HandleFunc("/ready", s.readyHandler)
|
mux.HandleFunc("/ready", s.readyHandler)
|
||||||
|
|
||||||
|
|
@ -52,6 +62,11 @@ func NewServer(host string, port int) *Server {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mux returns the HTTP mux so channels can register webhook routes on the gateway port.
|
||||||
|
func (s *Server) Mux() *http.ServeMux {
|
||||||
|
return s.mux
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) Start() error {
|
func (s *Server) Start() error {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
s.ready = true
|
s.ready = true
|
||||||
|
|
@ -103,6 +118,25 @@ func (s *Server) RegisterCheck(name string, checkFn func() (bool, string)) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) rootHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path != "/" {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
|
uptime := time.Since(s.startTime)
|
||||||
|
resp := GatewayInfo{
|
||||||
|
Name: "PicoClaw Gateway",
|
||||||
|
Status: "running",
|
||||||
|
Uptime: uptime.String(),
|
||||||
|
Version: "1.0.0",
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(resp)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue