让 webhook 渠道的入站 HTTP 回调真正进运行时(此前 handler 建了但未挂载,回调不达 runtime)。 - channelbridge:Bridge.WebhookHandlers() map[string]http.Handler——遍历适配器,对暴露 InboundHandler()/Config() 的(*webhook.Adapter)收集 endpointPath→handler。 - gateway/server.go:Deps 加 Webhooks map[string]http.Handler;Server 存 webhooks; Handler() 挂载到 mux(无网关鉴权,各 handler 自带 HMAC 签名校验)。 - gateway/static.go:spa 加 isWebhookRoute——已注册的 webhook 路径交给 API mux(否则被 SPA fallback 吞成 index.html,webhook 回调不达)。 - wire.go:gateway.Deps 传 Webhooks: a.channelBridge.WebhookHandlers()(wireChannels 在 wireFrontend 前执行,bridge 已建好)。 实机验证:POST /webhook 空体→bad_request(400,webhook 解码响应非 SPA)、带 JSON→accepted(200)、 GET /webhook 被 webhook 路由处理。go build/vet/test 全绿。
59 lines
1.8 KiB
Go
59 lines
1.8 KiB
Go
package gateway
|
|
|
|
import (
|
|
"net/http"
|
|
"path"
|
|
"strings"
|
|
)
|
|
|
|
// spa wraps the API mux with the SPA file server. Requests under /api/ and /ws/
|
|
// are served by the API handler; every other path is served from the configured
|
|
// frontend filesystem with a fallback to index.html so client-side routes (e.g.
|
|
// /chat) resolve in a single-page app.
|
|
func (s *Server) spa(api http.Handler) http.Handler {
|
|
fileServer := s.spaFileServer()
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if isAPIRoute(r.URL.Path) || s.isWebhookRoute(r.URL.Path) {
|
|
api.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
fileServer.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
// isWebhookRoute reports whether path is a registered external webhook callback
|
|
// route. Such paths must reach the API mux (where the webhook handler is
|
|
// mounted) rather than the SPA file fallback, so inbound callbacks are served.
|
|
func (s *Server) isWebhookRoute(p string) bool {
|
|
if len(s.webhooks) == 0 {
|
|
return false
|
|
}
|
|
_, ok := s.webhooks[p]
|
|
return ok
|
|
}
|
|
|
|
// spaFileServer serves static assets, falling back to index.html for paths that
|
|
// are not real files in the embedded/external dist.
|
|
func (s *Server) spaFileServer() http.Handler {
|
|
fsrv := http.FileServer(http.FS(s.frontend))
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
rel := strings.TrimPrefix(path.Clean(r.URL.Path), "/")
|
|
if rel != "" {
|
|
if f, err := s.frontend.Open(rel); err == nil {
|
|
_ = f.Close()
|
|
fsrv.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
}
|
|
r2 := r.Clone(r.Context())
|
|
r2.URL.Path = "/"
|
|
fsrv.ServeHTTP(w, r2)
|
|
})
|
|
}
|
|
|
|
// isAPIRoute reports whether path belongs to the REST/WS API surface the
|
|
// gateway owns, which the SPA handler must leave untouched.
|
|
func isAPIRoute(p string) bool {
|
|
return strings.HasPrefix(p, "/api/") || strings.HasPrefix(p, "/ws/")
|
|
}
|