# 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
