Skip to content

Commit c7bb9a7

Browse files
committed
🤖 fix(proxy): address provider-scoped routing review issues
What: - split provider-scoped proxy handling into a dedicated ProviderProxyHandler instead of extending the project-scoped handler - route /project/... and /provider/... through separate handlers in both server wiring and static fallback dispatch - make provider-scoped matches bypass project custom-route selection and use provider-filtered global routes only - add handler and router regression coverage for the separated scope flow Why: - keep provider scope independent from project semantics and avoid reusing project-specific control flow for a new proxy surface - make the scoped proxy routing easier to reason about, review, and extend without hidden coupling between scopes - preserve the existing provider-scope fixes while aligning the implementation with the intended architecture Tests: - go test ./internal/handler ./internal/router ./tests/e2e/... (pass)
1 parent 324bae9 commit c7bb9a7

File tree

13 files changed

+457
-177
lines changed

13 files changed

+457
-177
lines changed

cmd/maxx/main.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,8 @@ func main() {
375375

376376
// Use already-created cached project repository for project proxy handler
377377
modelsHandler := handler.NewModelsHandler(responseModelRepo, cachedProviderRepo, cachedModelMappingRepo)
378-
projectProxyHandler := handler.NewProjectProxyHandler(proxyHandler, modelsHandler, cachedProjectRepo, cachedProviderRepo)
378+
projectProxyHandler := handler.NewProjectProxyHandler(proxyHandler, modelsHandler, cachedProjectRepo)
379+
providerProxyHandler := handler.NewProviderProxyHandler(proxyHandler, modelsHandler, cachedProviderRepo)
379380

380381
// Setup routes
381382
mux := http.NewServeMux()
@@ -420,9 +421,9 @@ func main() {
420421
// WebSocket endpoint
421422
mux.HandleFunc("/ws", wsHub.HandleWebSocket)
422423

423-
// Serve static files (Web UI) with project proxy support - must be last (default route)
424+
// Serve static files (Web UI) with scoped proxy support - must be last (default route)
424425
staticHandler := handler.NewStaticHandler()
425-
combinedHandler := handler.NewCombinedHandler(projectProxyHandler, staticHandler)
426+
combinedHandler := handler.NewCombinedHandler(projectProxyHandler, providerProxyHandler, staticHandler)
426427
mux.Handle("/", combinedHandler)
427428

428429
// Wrap with logging middleware
@@ -530,7 +531,8 @@ func main() {
530531
log.Printf(" OpenAI: http://localhost%s/v1/chat/completions", *addr)
531532
log.Printf(" Codex: http://localhost%s/v1/responses", *addr)
532533
log.Printf(" Gemini: http://localhost%s/v1beta/models/{model}:generateContent", *addr)
533-
log.Printf("Project proxy: http://localhost%s/project/{project-slug}/v1/messages (etc.)", *addr)
534+
log.Printf("Project proxy: http://localhost%s/project/{project-slug}/v1/messages (etc.)", *addr)
535+
log.Printf("Provider proxy: http://localhost%s/provider/{provider-id}/v1/messages (etc.)", *addr)
534536

535537
go func() {
536538
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {

internal/core/database.go

Lines changed: 46 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -73,27 +73,28 @@ type DatabaseRepos struct {
7373

7474
// ServerComponents 包含服务器运行所需的所有组件
7575
type ServerComponents struct {
76-
Router *router.Router
77-
WebSocketHub *handler.WebSocketHub
78-
WailsBroadcaster *event.WailsBroadcaster
79-
Executor *executor.Executor
80-
ClientAdapter *client.Adapter
81-
AdminService *service.AdminService
82-
ProxyHandler *handler.ProxyHandler
83-
ModelsHandler *handler.ModelsHandler
84-
AdminHandler *handler.AdminHandler
85-
AntigravityHandler *handler.AntigravityHandler
86-
KiroHandler *handler.KiroHandler
87-
CodexHandler *handler.CodexHandler
88-
CodexOAuthServer *CodexOAuthServer
89-
ClaudeHandler *handler.ClaudeHandler
90-
ClaudeOAuthServer *ClaudeOAuthServer
91-
ProjectProxyHandler *handler.ProjectProxyHandler
92-
RequestTracker *RequestTracker
93-
PprofManager *PprofManager
94-
AuthMiddleware *handler.AuthMiddleware
95-
AuthHandler *handler.AuthHandler
96-
BackupService *service.BackupService
76+
Router *router.Router
77+
WebSocketHub *handler.WebSocketHub
78+
WailsBroadcaster *event.WailsBroadcaster
79+
Executor *executor.Executor
80+
ClientAdapter *client.Adapter
81+
AdminService *service.AdminService
82+
ProxyHandler *handler.ProxyHandler
83+
ModelsHandler *handler.ModelsHandler
84+
AdminHandler *handler.AdminHandler
85+
AntigravityHandler *handler.AntigravityHandler
86+
KiroHandler *handler.KiroHandler
87+
CodexHandler *handler.CodexHandler
88+
CodexOAuthServer *CodexOAuthServer
89+
ClaudeHandler *handler.ClaudeHandler
90+
ClaudeOAuthServer *ClaudeOAuthServer
91+
ProjectProxyHandler *handler.ProjectProxyHandler
92+
ProviderProxyHandler *handler.ProviderProxyHandler
93+
RequestTracker *RequestTracker
94+
PprofManager *PprofManager
95+
AuthMiddleware *handler.AuthMiddleware
96+
AuthHandler *handler.AuthHandler
97+
BackupService *service.BackupService
9798
}
9899

99100
// InitializeDatabase 初始化数据库和所有仓库
@@ -408,34 +409,36 @@ func InitializeServerComponents(
408409
claudeHandler := handler.NewClaudeHandler(adminService, wailsBroadcaster)
409410
claudeOAuthServer := NewClaudeOAuthServer(claudeHandler)
410411
claudeHandler.SetOAuthServer(claudeOAuthServer)
411-
projectProxyHandler := handler.NewProjectProxyHandler(proxyHandler, modelsHandler, repos.CachedProjectRepo, repos.CachedProviderRepo)
412+
projectProxyHandler := handler.NewProjectProxyHandler(proxyHandler, modelsHandler, repos.CachedProjectRepo)
413+
providerProxyHandler := handler.NewProviderProxyHandler(proxyHandler, modelsHandler, repos.CachedProviderRepo)
412414

413415
log.Printf("[Core] Creating request tracker for graceful shutdown")
414416
requestTracker := NewRequestTracker()
415417
proxyHandler.SetRequestTracker(requestTracker)
416418

417419
components := &ServerComponents{
418-
Router: r,
419-
WebSocketHub: wsHub,
420-
WailsBroadcaster: wailsBroadcaster,
421-
Executor: exec,
422-
ClientAdapter: clientAdapter,
423-
AdminService: adminService,
424-
ProxyHandler: proxyHandler,
425-
ModelsHandler: modelsHandler,
426-
AdminHandler: adminHandler,
427-
AntigravityHandler: antigravityHandler,
428-
KiroHandler: kiroHandler,
429-
CodexHandler: codexHandler,
430-
CodexOAuthServer: codexOAuthServer,
431-
ClaudeHandler: claudeHandler,
432-
ClaudeOAuthServer: claudeOAuthServer,
433-
ProjectProxyHandler: projectProxyHandler,
434-
RequestTracker: requestTracker,
435-
PprofManager: pprofMgr,
436-
AuthMiddleware: authMiddleware,
437-
AuthHandler: authHandler,
438-
BackupService: backupService,
420+
Router: r,
421+
WebSocketHub: wsHub,
422+
WailsBroadcaster: wailsBroadcaster,
423+
Executor: exec,
424+
ClientAdapter: clientAdapter,
425+
AdminService: adminService,
426+
ProxyHandler: proxyHandler,
427+
ModelsHandler: modelsHandler,
428+
AdminHandler: adminHandler,
429+
AntigravityHandler: antigravityHandler,
430+
KiroHandler: kiroHandler,
431+
CodexHandler: codexHandler,
432+
CodexOAuthServer: codexOAuthServer,
433+
ClaudeHandler: claudeHandler,
434+
ClaudeOAuthServer: claudeOAuthServer,
435+
ProjectProxyHandler: projectProxyHandler,
436+
ProviderProxyHandler: providerProxyHandler,
437+
RequestTracker: requestTracker,
438+
PprofManager: pprofMgr,
439+
AuthMiddleware: authMiddleware,
440+
AuthHandler: authHandler,
441+
BackupService: backupService,
439442
}
440443

441444
log.Printf("[Core] Server components initialized successfully")

internal/core/server.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,12 @@ func (s *ManagedServer) setupRoutes() *http.ServeMux {
106106

107107
if s.config.ServeStatic {
108108
staticHandler := handler.NewStaticHandler()
109-
combinedHandler := handler.NewCombinedHandler(components.ProjectProxyHandler, staticHandler)
109+
combinedHandler := handler.NewCombinedHandler(components.ProjectProxyHandler, components.ProviderProxyHandler, staticHandler)
110110
mux.Handle("/", combinedHandler)
111111
log.Printf("[Server] Static file serving enabled")
112112
} else {
113-
mux.Handle("/", components.ProjectProxyHandler)
113+
mux.Handle("/project/", components.ProjectProxyHandler)
114+
mux.Handle("/provider/", components.ProviderProxyHandler)
114115
log.Printf("[Server] Static file serving disabled (Wails mode)")
115116
}
116117

internal/handler/project_proxy.go

Lines changed: 38 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -3,131 +3,100 @@ package handler
33
import (
44
"log"
55
"net/http"
6-
"strconv"
76
"strings"
87

98
maxxctx "github.com/awsl-project/maxx/internal/context"
109
"github.com/awsl-project/maxx/internal/repository"
1110
)
1211

13-
// ProjectProxyHandler wraps ProxyHandler to handle scoped proxy requests
14-
// like /project/{slug}/v1/messages and /provider/{id}/v1/messages.
12+
// ProjectProxyHandler wraps ProxyHandler to handle project-scoped proxy requests
13+
// like /project/{slug}/v1/messages.
1514
type ProjectProxyHandler struct {
1615
proxyHandler *ProxyHandler
1716
modelsHandler *ModelsHandler
1817
projectRepo repository.ProjectRepository
19-
providerRepo repository.ProviderRepository
2018
}
2119

22-
// NewProjectProxyHandler creates a new scoped proxy handler.
20+
// NewProjectProxyHandler creates a new project-scoped proxy handler.
2321
func NewProjectProxyHandler(
2422
proxyHandler *ProxyHandler,
2523
modelsHandler *ModelsHandler,
2624
projectRepo repository.ProjectRepository,
27-
providerRepo repository.ProviderRepository,
2825
) *ProjectProxyHandler {
2926
return &ProjectProxyHandler{
3027
proxyHandler: proxyHandler,
3128
modelsHandler: modelsHandler,
3229
projectRepo: projectRepo,
33-
providerRepo: providerRepo,
3430
}
3531
}
3632

37-
// ServeHTTP handles scoped proxy requests.
33+
// ServeHTTP handles project-scoped proxy requests.
3834
func (h *ProjectProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
39-
scopeType, scopeValue, apiPath, ok := h.parseScopedPath(r.URL.Path)
35+
projectSlug, apiPath, ok := h.parseScopedPath(r.URL.Path)
4036
if !ok {
41-
writeError(w, http.StatusNotFound, "invalid scoped proxy path")
37+
writeError(w, http.StatusNotFound, "invalid project proxy path")
4238
return
4339
}
4440

4541
tenantID := maxxctx.GetTenantID(r.Context())
46-
47-
switch scopeType {
48-
case "project":
49-
project, err := h.projectRepo.GetBySlug(tenantID, scopeValue)
50-
if err != nil {
51-
log.Printf("[ScopedProxy] Project not found for slug: %s", scopeValue)
52-
writeError(w, http.StatusNotFound, "project not found")
53-
return
54-
}
55-
log.Printf("[ScopedProxy] Routing request through project: %s (ID: %d)", project.Name, project.ID)
56-
r.Header.Set("X-Maxx-Project-ID", strings.TrimSpace(itoa(project.ID)))
57-
case "provider":
58-
providerID, err := strconv.ParseUint(scopeValue, 10, 64)
59-
if err != nil || providerID == 0 {
60-
writeError(w, http.StatusBadRequest, "invalid provider id")
61-
return
62-
}
63-
provider, err := h.providerRepo.GetByID(tenantID, providerID)
64-
if err != nil || provider == nil {
65-
log.Printf("[ScopedProxy] Provider not found for id: %s", scopeValue)
66-
writeError(w, http.StatusNotFound, "provider not found")
67-
return
68-
}
69-
log.Printf("[ScopedProxy] Routing request through provider: %s (ID: %d)", provider.Name, provider.ID)
70-
r.Header.Set("X-Maxx-Provider-ID", strings.TrimSpace(itoa(provider.ID)))
71-
default:
72-
writeError(w, http.StatusNotFound, "unknown scope type")
42+
project, err := h.projectRepo.GetBySlug(tenantID, projectSlug)
43+
if err != nil {
44+
log.Printf("[ProjectProxy] Project not found for slug: %s", projectSlug)
45+
writeError(w, http.StatusNotFound, "project not found")
7346
return
7447
}
7548

49+
log.Printf("[ProjectProxy] Routing request through project: %s (ID: %d)", project.Name, project.ID)
50+
r.Header.Set("X-Maxx-Project-ID", strings.TrimSpace(itoa(project.ID)))
7651
r.URL.Path = apiPath
7752
if apiPath == "/v1/models" {
7853
h.modelsHandler.ServeHTTP(w, r)
7954
return
8055
}
56+
8157
h.proxyHandler.ServeHTTP(w, r)
8258
}
8359

84-
// parseScopedPath extracts scope type/value and API path.
85-
// Input: /project/my-project/v1/messages or /provider/1/v1/messages
86-
func (h *ProjectProxyHandler) parseScopedPath(path string) (scopeType, scopeValue, apiPath string, ok bool) {
87-
if strings.HasPrefix(path, "/project/") {
88-
return parseScopePath("project", strings.TrimPrefix(path, "/project/"))
89-
}
90-
if strings.HasPrefix(path, "/provider/") {
91-
return parseScopePath("provider", strings.TrimPrefix(path, "/provider/"))
92-
}
93-
return "", "", "", false
60+
// parseScopedPath extracts project slug and API path.
61+
// Input: /project/my-project/v1/messages
62+
func (h *ProjectProxyHandler) parseScopedPath(path string) (projectSlug, apiPath string, ok bool) {
63+
if !strings.HasPrefix(path, "/project/") {
64+
return "", "", false
65+
}
66+
_, projectSlug, apiPath, ok = parseScopePath("project", strings.TrimPrefix(path, "/project/"))
67+
return projectSlug, apiPath, ok
9468
}
9569

9670
func parseScopePath(scopeType, path string) (resolvedType, scopeValue, apiPath string, ok bool) {
9771
parts := strings.SplitN(path, "/", 2)
98-
if len(parts) < 2 || strings.TrimSpace(parts[0]) == "" {
72+
if len(parts) < 2 {
73+
return "", "", "", false
74+
}
75+
76+
trimmed := strings.TrimSpace(parts[0])
77+
if trimmed == "" {
9978
return "", "", "", false
10079
}
10180

102-
scopeValue = parts[0]
10381
apiPath = "/" + parts[1]
10482
if !isValidAPIPath(apiPath) {
10583
return "", "", "", false
10684
}
107-
return scopeType, scopeValue, apiPath, true
85+
return scopeType, trimmed, apiPath, true
10886
}
10987

11088
// isValidAPIPath checks if the path is a known proxy API endpoint.
11189
func isValidAPIPath(path string) bool {
112-
if strings.HasPrefix(path, "/v1/messages") {
113-
return true
114-
}
115-
if strings.HasPrefix(path, "/v1/chat/completions") {
116-
return true
117-
}
118-
if strings.HasPrefix(path, "/responses") {
119-
return true
120-
}
121-
if strings.HasPrefix(path, "/v1/responses") {
122-
return true
123-
}
124-
if strings.HasPrefix(path, "/v1/models") {
125-
return true
126-
}
127-
if strings.HasPrefix(path, "/v1beta/models/") {
128-
return true
129-
}
130-
return false
90+
return matchesEndpointPath(path, "/v1/messages") ||
91+
matchesEndpointPath(path, "/v1/chat/completions") ||
92+
matchesEndpointPath(path, "/responses") ||
93+
matchesEndpointPath(path, "/v1/responses") ||
94+
matchesEndpointPath(path, "/v1/models") ||
95+
matchesEndpointPath(path, "/v1beta/models")
96+
}
97+
98+
func matchesEndpointPath(path, endpoint string) bool {
99+
return path == endpoint || strings.HasPrefix(path, endpoint+"/")
131100
}
132101

133102
func itoa(n uint64) string {

0 commit comments

Comments
 (0)