Ashraf Al-Kassem Claude Opus 4.6 commited on
Commit
721b3fc
·
1 Parent(s): 38213a5

fix: eliminate 307 redirects that break login and API calls behind HF proxy

Browse files

The previous trailing-slash normalization in api.ts inadvertently broke
login (POST /auth/login/ → 307 → /auth/login, losing FormData).

Root cause: FastAPI routes defined as @router .get("/") only match paths
WITH trailing slash, causing 307 redirects for requests WITHOUT one.
Behind HuggingFace's reverse proxy, these redirects silently fail.

Fix: Change route definitions from "/" to "" so they match the exact
paths the frontend sends (no trailing slash). Revert the frontend
trailing-slash hack.

Routes fixed: prompt_config (GET, POST), integrations (GET),
workspaces (GET, POST), automations (GET).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

backend/app/api/v1/automations.py CHANGED
@@ -30,7 +30,7 @@ class BuilderPayload(BaseModel):
30
  steps: List[BuilderStep]
31
  publish: bool = False
32
 
33
- @router.get("/")
34
  async def list_flows(
35
  db: AsyncSession = Depends(get_db),
36
  current_user: User = Depends(deps.get_current_user)
 
30
  steps: List[BuilderStep]
31
  publish: bool = False
32
 
33
+ @router.get("")
34
  async def list_flows(
35
  db: AsyncSession = Depends(get_db),
36
  current_user: User = Depends(deps.get_current_user)
backend/app/api/v1/integrations.py CHANGED
@@ -17,7 +17,7 @@ from app.core.modules import require_module_enabled, MODULE_INTEGRATIONS_CONNECT
17
 
18
  router = APIRouter()
19
 
20
- @router.get("/", response_model=ResponseEnvelope[List[IntegrationRead]], dependencies=[Depends(require_module_enabled(MODULE_INTEGRATIONS_CONNECT, "read"))])
21
  async def list_integrations(
22
  db: AsyncSession = Depends(get_db),
23
  workspace: Workspace = Depends(deps.get_active_workspace),
 
17
 
18
  router = APIRouter()
19
 
20
+ @router.get("", response_model=ResponseEnvelope[List[IntegrationRead]], dependencies=[Depends(require_module_enabled(MODULE_INTEGRATIONS_CONNECT, "read"))])
21
  async def list_integrations(
22
  db: AsyncSession = Depends(get_db),
23
  workspace: Workspace = Depends(deps.get_active_workspace),
backend/app/api/v1/prompt_config.py CHANGED
@@ -50,7 +50,7 @@ def compile_prompt(data: Dict[str, Any]) -> str:
50
 
51
  return "\n".join(lines)
52
 
53
- @router.get("/", response_model=ResponseEnvelope[PromptConfigRead], dependencies=[Depends(require_module_enabled(MODULE_PROMPT_STUDIO, "read"))])
54
  async def get_prompt_config(
55
  db: AsyncSession = Depends(get_db),
56
  workspace: Workspace = Depends(deps.get_active_workspace),
@@ -87,7 +87,7 @@ async def get_prompt_config(
87
  import logging
88
  logger = logging.getLogger(__name__)
89
 
90
- @router.post("/", response_model=ResponseEnvelope[PromptVersionRead], dependencies=[Depends(require_module_enabled(MODULE_PROMPT_STUDIO, "write"))])
91
  async def create_prompt_version(
92
  *,
93
  db: AsyncSession = Depends(get_db),
 
50
 
51
  return "\n".join(lines)
52
 
53
+ @router.get("", response_model=ResponseEnvelope[PromptConfigRead], dependencies=[Depends(require_module_enabled(MODULE_PROMPT_STUDIO, "read"))])
54
  async def get_prompt_config(
55
  db: AsyncSession = Depends(get_db),
56
  workspace: Workspace = Depends(deps.get_active_workspace),
 
87
  import logging
88
  logger = logging.getLogger(__name__)
89
 
90
+ @router.post("", response_model=ResponseEnvelope[PromptVersionRead], dependencies=[Depends(require_module_enabled(MODULE_PROMPT_STUDIO, "write"))])
91
  async def create_prompt_version(
92
  *,
93
  db: AsyncSession = Depends(get_db),
backend/app/api/v1/workspaces.py CHANGED
@@ -14,7 +14,7 @@ from app.schemas.envelope import ResponseEnvelope, wrap_data, wrap_error
14
 
15
  router = APIRouter()
16
 
17
- @router.post("/", response_model=ResponseEnvelope[WorkspaceRead])
18
  async def create_workspace(
19
  *,
20
  db: AsyncSession = Depends(get_db),
@@ -36,7 +36,7 @@ async def create_workspace(
36
  await db.refresh(workspace)
37
  return wrap_data(workspace)
38
 
39
- @router.get("/", response_model=ResponseEnvelope[List[WorkspaceRead]])
40
  async def list_workspaces(
41
  db: AsyncSession = Depends(get_db),
42
  current_user: User = Depends(deps.get_current_user)
 
14
 
15
  router = APIRouter()
16
 
17
+ @router.post("", response_model=ResponseEnvelope[WorkspaceRead])
18
  async def create_workspace(
19
  *,
20
  db: AsyncSession = Depends(get_db),
 
36
  await db.refresh(workspace)
37
  return wrap_data(workspace)
38
 
39
+ @router.get("", response_model=ResponseEnvelope[List[WorkspaceRead]])
40
  async def list_workspaces(
41
  db: AsyncSession = Depends(get_db),
42
  current_user: User = Depends(deps.get_current_user)
frontend/src/lib/admin-api.ts CHANGED
@@ -27,9 +27,7 @@ async function adminRequest<T = any>(
27
  ): Promise<ApiResponse<T>> {
28
  const base = getBaseUrl().replace(/\/$/, "");
29
  const cleanPath = path.startsWith("/") ? path : `/${path}`;
30
- // Ensure trailing slash to prevent FastAPI 307 redirects
31
- const normalizedPath = cleanPath.endsWith("/") ? cleanPath : `${cleanPath}/`;
32
- const url = `${base}/api/v1${normalizedPath}`;
33
 
34
  const token = adminAuth.getToken();
35
  const headers = new Headers(options.headers || {});
 
27
  ): Promise<ApiResponse<T>> {
28
  const base = getBaseUrl().replace(/\/$/, "");
29
  const cleanPath = path.startsWith("/") ? path : `/${path}`;
30
+ const url = `${base}/api/v1${cleanPath}`;
 
 
31
 
32
  const token = adminAuth.getToken();
33
  const headers = new Headers(options.headers || {});
frontend/src/lib/api.ts CHANGED
@@ -50,10 +50,7 @@ class ApiClient {
50
  // Safe base URL with /api/v1 prefix - ALWAYS absolute
51
  const base = API_BASE_URL.replace(/\/$/, "");
52
  const cleanPath = path.startsWith("/") ? path : `/${path}`;
53
- // Ensure trailing slash to prevent FastAPI 307 redirects which can fail
54
- // behind reverse proxies (e.g. HuggingFace Spaces)
55
- const normalizedPath = cleanPath.endsWith("/") ? cleanPath : `${cleanPath}/`;
56
- const url = `${base}/api/v1${normalizedPath}`;
57
 
58
  const method = options.method || "GET";
59
  console.log(`[API] ${method} ${url}`);
 
50
  // Safe base URL with /api/v1 prefix - ALWAYS absolute
51
  const base = API_BASE_URL.replace(/\/$/, "");
52
  const cleanPath = path.startsWith("/") ? path : `/${path}`;
53
+ const url = `${base}/api/v1${cleanPath}`;
 
 
 
54
 
55
  const method = options.method || "GET";
56
  console.log(`[API] ${method} ${url}`);