flowweb proxies /v1 and /v2 to the backend

The backend mounts its API at /v1 and /v2, but flowweb forwarded /api/,
so every API call from the UI came back as the SPA shell. A routing test
covers API paths, SPA routes and the method guard.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
zooqueen
2026-09-15 11:56:02 -07:00
co-authored by Hanzo Dev
parent 1e1310e05a
commit 27b12fc2a2
2 changed files with 59 additions and 3 deletions
+4 -3
View File
@@ -22,10 +22,11 @@ var frontendFS embed.FS
// apiPrefixes go to the backend, never the SPA — an unmatched path here is the
// backend's real JSON 404, never index.html (so a client never gets HTML where it
// expects JSON). Everything else is a client-side route → the SPA shell.
// expects JSON). The backend mounts its API at /v1 and /v2 (src/backend/base/flow/api/router.py).
// Everything else is a client-side route → the SPA shell.
var apiPrefixes = []string{
"/api/", "/health", "/health_check", "/openapi.json", "/docs", "/redoc",
"/.well-known/", "/logs", "/metrics",
"/v1/", "/v2/", "/health", "/health_check", "/openapi.json", "/docs", "/redoc",
"/.well-known/", "/logs",
}
type handler struct {
+55
View File
@@ -0,0 +1,55 @@
package main
import (
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestHandlerRoutes(t *testing.T) {
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, `{"path":"`+r.URL.Path+`"}`)
}))
defer backend.Close()
h, err := newHandler(backend.URL)
if err != nil {
t.Fatal(err)
}
cases := []struct {
method, path string
backend bool
status int
}{
{http.MethodGet, "/v1/version", true, http.StatusOK},
{http.MethodPost, "/v1/login", true, http.StatusOK},
{http.MethodGet, "/v1", true, http.StatusOK},
{http.MethodGet, "/v2/files", true, http.StatusOK},
{http.MethodGet, "/health_check", true, http.StatusOK},
{http.MethodGet, "/logs-stream", true, http.StatusOK},
{http.MethodGet, "/", false, http.StatusOK},
{http.MethodGet, "/flows", false, http.StatusOK},
{http.MethodGet, "/v10", false, http.StatusOK},
{http.MethodPost, "/flows", false, http.StatusMethodNotAllowed},
}
for _, c := range cases {
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(c.method, c.path, nil))
if rec.Code != c.status {
t.Errorf("%s %s: status %d, want %d", c.method, c.path, rec.Code, c.status)
continue
}
body := rec.Body.String()
proxied := strings.Contains(body, `{"path":"`+c.path+`"}`)
if proxied != c.backend {
t.Errorf("%s %s: proxied=%v, want %v (body %.60q)", c.method, c.path, proxied, c.backend, body)
}
if !c.backend && c.status == http.StatusOK && !strings.Contains(body, "<html") {
t.Errorf("%s %s: want the SPA shell, got %.60q", c.method, c.path, body)
}
}
}