# FastAPI Trailing Slash Issue - 307 Redirect & 403 Forbidden ## Problem Description When making authenticated API calls to FastAPI endpoints, a mismatch in trailing slashes between the frontend request and backend route definition causes a **307 Temporary Redirect** that **loses the authentication header**, resulting in a **403 Forbidden** error. ## Root Cause FastAPI automatically redirects requests to add or remove trailing slashes to match the route definition: - If route is defined as `@router.get("/tasks/")` (with slash) and you call `/tasks` (without slash) → 307 redirect to `/tasks/` - If route is defined as `@router.get("/tasks")` (without slash) and you call `/tasks/` (with slash) → 307 redirect to `/tasks` **The problem:** HTTP redirects (307) do NOT preserve the `Authorization` header by default, so the redirected request arrives without authentication, causing a 403 Forbidden error. ## Symptoms ### Backend Logs ``` INFO: 127.0.0.1:59653 - "GET /tasks?shot_id=12 HTTP/1.1" 307 Temporary Redirect INFO: 127.0.0.1:59615 - "GET /tasks/?shot_id=12 HTTP/1.1" 403 Forbidden ``` ### Frontend Console ``` GET http://localhost:8000/tasks/?shot_id=12 403 (Forbidden) AxiosError {message: 'Request failed with status code 403', ...} ``` ## Solution **Always ensure trailing slashes match between frontend API calls and backend route definitions.** ### Option 1: Add Trailing Slash to Frontend (Recommended) **Frontend Service:** ```typescript // ❌ WRONG - No trailing slash const response = await apiClient.get(`/tasks?${params}`) // ✅ CORRECT - With trailing slash const response = await apiClient.get(`/tasks/?${params}`) ``` **Backend Route:** ```python # Route defined WITH trailing slash @router.get("/tasks/") async def get_tasks(...): ... ``` ### Option 2: Remove Trailing Slash from Backend **Backend Route:** ```python # Route defined WITHOUT trailing slash @router.get("/tasks") async def get_tasks(...): ... ``` **Frontend Service:** ```typescript // Call WITHOUT trailing slash const response = await apiClient.get(`/tasks?${params}`) ``` ## Prevention Checklist When adding or modifying routes, **always check**: 1. **Backend Route Definition** - Does it have a trailing slash? ```python @router.get("/endpoint/") # Has trailing slash @router.get("/endpoint") # No trailing slash ``` 2. **Frontend API Call** - Does it match the backend? ```typescript apiClient.get(`/endpoint/`) // Has trailing slash apiClient.get(`/endpoint`) // No trailing slash ``` 3. **Query Parameters** - Trailing slash goes BEFORE the `?` ```typescript // ✅ CORRECT apiClient.get(`/tasks/?shot_id=12`) // ❌ WRONG apiClient.get(`/tasks?shot_id=12/`) ``` 4. **Path Parameters** - Usually no trailing slash ```typescript // ✅ CORRECT apiClient.get(`/tasks/${taskId}`) // ❌ WRONG (usually) apiClient.get(`/tasks/${taskId}/`) ``` ## Historical Issues Fixed ### Issue 1: Shots Endpoint (Fixed) - **Problem:** Frontend called `/shots/1/` but backend defined `/shots/{shot_id}` - **Solution:** Changed frontend to call `/shots/1` (no trailing slash) - **Files:** `frontend/src/services/shot.ts` ### Issue 2: Tasks Endpoint (Fixed) - **Problem:** Frontend called `/tasks?shot_id=12` but backend defined `/tasks/` - **Solution:** Changed frontend to call `/tasks/?shot_id=12` (with trailing slash) - **Files:** `frontend/src/services/task.ts` ## Testing To verify there's no redirect issue: 1. **Check backend logs** - Should see only ONE request, not two: ``` ✅ GOOD: INFO: "GET /tasks/?shot_id=12 HTTP/1.1" 200 OK ❌ BAD (redirect happening): INFO: "GET /tasks?shot_id=12 HTTP/1.1" 307 Temporary Redirect INFO: "GET /tasks/?shot_id=12 HTTP/1.1" 403 Forbidden ``` 2. **Check frontend network tab** - Should see 200 OK, not 307 or 403 3. **Test with authentication** - Ensure authenticated endpoints work correctly ## Quick Reference ### Common Patterns | Endpoint Type | Backend Route | Frontend Call | |--------------|---------------|---------------| | List with query params | `@router.get("/items/")` | `get("/items/?param=value")` | | Get by ID | `@router.get("/items/{id}")` | `get("/items/123")` | | Create | `@router.post("/items/")` | `post("/items/", data)` | | Update by ID | `@router.put("/items/{id}")` | `put("/items/123", data)` | | Delete by ID | `@router.delete("/items/{id}")` | `delete("/items/123")` | ## Related Files - Backend routes: `backend/routers/*.py` - Frontend services: `frontend/src/services/*.ts` - API client: `frontend/src/services/api.ts` ## Additional Notes - This issue only affects authenticated endpoints because the `Authorization` header is lost during redirect - Public endpoints might not show this issue as clearly - Always test with actual authentication tokens, not just in development mode - Consider adding a linter rule or pre-commit hook to check for trailing slash consistency