Skip to content

CORS Error Handling and Authentication Token Management

Problem

When the FastAPI application generates a 500 error, the response is sent to the client without the CORS headers. This causes browsers to block the response with a misleading CORS error message:

Access to XMLHttpRequest at 'http://localhost:8100/api/v1/production-runs/10/trackers' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

This gives a false impression that the client is suffering from a CORS-related issue, when there is actually a different underlying cause (a server error).

Vite Proxy Configuration

When using Vite's development server with a proxy configuration, it's important to understand how the proxy works:

  1. The proxy forwards requests from the Vite dev server to the target server
  2. Requests must be made to the Vite server (using relative URLs), not directly to the target server
  3. The proxy configuration in vite.config.ts should use patterns that match all API routes

Common Issues with Vite Proxy

If you're experiencing CORS errors with the Vite proxy, check for these common issues:

  1. Using absolute URLs in API requests: Always use relative URLs (e.g., /api/v1/users instead of http://localhost:8000/api/v1/users)
  2. Restrictive proxy patterns: Use more general patterns like /api instead of specific regex patterns
  3. Missing proxy options: Ensure changeOrigin: true is set in the proxy configuration

Our Solution

We've updated the Vite proxy configuration to handle HTTP redirects and use Docker service names:

server: {
  proxy: {
    '/api/v1': {  // This matches paths starting with /api/v1
      target: 'http://dev:8000',
      changeOrigin: true,
      secure: false,
      followRedirects: true,  // Automatically follow HTTP redirects
      // Other options...
    },
    '/static': {  // This matches all paths starting with /static
      target: 'http://dev:8000',
      changeOrigin: true,
      secure: false,
      followRedirects: true,  // Automatically follow HTTP redirects
    },
  },
},

This configuration ensures that all API requests are properly proxied to the backend server, avoiding CORS issues and handling redirects correctly.

Our API client in tracker-admin/src/api/client.ts is already configured to use relative URLs and includes safeguards to convert any absolute URLs to relative ones.

Key Changes Made

  1. More specific proxy pattern: Using /api/v1 to match exactly what the API client is using
  2. Docker service name: Using http://dev:8000 as the target, where "dev" is the Docker service name
  3. Handling redirects: Added followRedirects: true to automatically follow HTTP redirects (like 307 Temporary Redirect)
  4. Enhanced logging: Added logging for redirect locations to help troubleshoot redirect issues

Understanding HTTP Redirects in Proxies

HTTP redirects (like 307 Temporary Redirect) can cause issues with proxies if not handled correctly:

  1. Default behavior: By default, the Vite proxy doesn't automatically follow redirects
  2. Client-side redirects: Without followRedirects: true, the browser would need to handle the redirect, which can break the proxy
  3. Relative vs. absolute redirects: Redirects with relative paths work differently than those with absolute URLs
  4. Preserving HTTP methods: 307 redirects preserve the original HTTP method (GET, POST, etc.), which is important for API requests

The Critical Role of 307 Temporary Redirect

The 307 Temporary Redirect status code is particularly important in API proxying scenarios:

  1. What is a 307 Redirect? A 307 status code tells the client to make the same request (with the same HTTP method and body) to a different URL
  2. Why it's used in APIs: Many APIs use 307 redirects for load balancing, URL normalization, or to handle moved resources
  3. Proxy challenges: When a proxy receives a 307 response but doesn't follow it, the client receives the redirect instead of the actual response
  4. CORS implications: If the redirect target has a different origin, it can trigger CORS errors even if the original request would have been allowed

In our case, the backend was returning 307 redirects, but the Vite proxy wasn't following them. This caused requests to fail with CORS errors or network errors. By adding followRedirects: true to our proxy configuration, we ensure that the proxy handles these redirects internally, making the process transparent to the client.

Example log showing a 307 redirect:

Sending Request to the Target: GET /api/v1/images?skip=0&limit=20
Proxy Request Headers: [Object: null prototype] { ... }
Received Response from the Target: 307 /api/v1/images?skip=0&limit=20
Redirect Location: /api/v1/images/?skip=0&limit=20

Notice the subtle difference in the redirect location - the addition of a trailing slash. Without proper redirect handling, this small difference can break your application.

Troubleshooting Network Errors

If you're still seeing network errors, check:

  1. That all Docker services are running (docker-compose ps to verify)
  2. That you're using relative URLs in all API requests (e.g., /api/v1/users instead of http://dev:8000/api/v1/users)
  3. That the proxy configuration in vite.config.ts is correctly set up
  4. That there are no CORS headers being explicitly set in your API requests that might override the proxy settings
  5. That your browser's network tab shows the requests are being made to the correct URL (should be to localhost:3000)
  6. That the server logs show the correct redirect paths (if any)

Common Issues with Vite Proxy in Docker Environments

When working with Docker and Vite proxy, several issues can arise:

  1. Docker networking: Docker containers have their own network namespace, which allows them to use service names as hostnames
  2. Port mapping: Make sure you're using the correct port (8000 inside the container, 8100 mapped to the host)
  3. CORS headers: The backend server must be configured to accept requests from the frontend's origin
  4. HTTP redirects: Redirects can break the proxy if not handled correctly with followRedirects: true

Solution

To address this issue, we've implemented a custom middleware called CORSHeadersMiddleware that ensures CORS headers are included in all responses, including error responses.

The middleware:

  1. Intercepts all requests and responses
  2. Adds appropriate CORS headers to all responses
  3. Catches unhandled exceptions and returns a 500 response with CORS headers

This ensures that even when errors occur, the browser will receive the proper CORS headers and display the actual error instead of a misleading CORS error.

Implementation

The implementation is in app/main.py and consists of:

# Custom middleware to ensure CORS headers are present in all responses, including errors
class CORSHeadersMiddleware(BaseHTTPMiddleware):
    def __init__(
        self,
        app: ASGIApp,
        origins: list[str] = ["*"],
        allow_credentials: bool = False
    ):
        super().__init__(app)
        self.origins = origins
        self.allow_credentials = allow_credentials

    async def dispatch(self, request: Request, call_next: Callable) -> Response:
        try:
            # Try to process the request normally
            response = await call_next(request)

            # Ensure CORS headers are present in the response
            self._add_cors_headers(request, response)

            return response
        except Exception as e:
            # For unhandled exceptions, create a response with CORS headers
            response = JSONResponse(
                status_code=500,
                content={"detail": f"Internal Server Error: {str(e)}"},
            )

            # Add CORS headers to the error response
            self._add_cors_headers(request, response)

            return response

    def _add_cors_headers(self, request: Request, response: Response) -> None:
        origin = request.headers.get("origin", "")

        # If we have a specific list of origins, check if the request origin is allowed
        if "*" not in self.origins and origin:
            if origin in self.origins:
                response.headers["Access-Control-Allow-Origin"] = origin
        else:
            # For wildcard, we can just set the header to *
            response.headers["Access-Control-Allow-Origin"] = "*"

        # Add other CORS headers
        response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS, PATCH"
        response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization, X-Requested-With"

        if self.allow_credentials:
            response.headers["Access-Control-Allow-Credentials"] = "true"

The middleware is added to the application before the standard FastAPI CORSMiddleware:

# Add performance monitoring middleware
app.add_middleware(PerformanceMiddleware)

# Set all CORS enabled origins
if "*" in settings.CORS_ORIGINS:
    # If wildcard is present, use it but disable credentials
    app.add_middleware(
        CORSHeadersMiddleware,
        origins=["*"],
        allow_credentials=False,
    )
else:
    # Otherwise use specific origins with credentials
    app.add_middleware(
        CORSHeadersMiddleware,
        origins=settings.CORS_ORIGINS,
        allow_credentials=True,
    )

# Add standard CORS middleware as a fallback
if "*" in settings.CORS_ORIGINS:
    app.add_middleware(
        CORSMiddleware,
        allow_origins=["*"],
        allow_credentials=False,
        allow_methods=["*"],
        allow_headers=["*"],
    )
else:
    app.add_middleware(
        CORSMiddleware,
        allow_origins=settings.CORS_ORIGINS,
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
    )

Why Two Middleware Layers?

We've added both our custom CORSHeadersMiddleware and the standard FastAPI CORSMiddleware for a few reasons:

  1. The custom middleware ensures CORS headers are added to error responses
  2. The standard middleware handles the normal CORS preflight requests and other CORS-related functionality
  3. Having both provides a fallback mechanism in case one fails

Testing

To test this implementation, you can:

  1. Intentionally cause a server error (e.g., by requesting a non-existent resource or triggering an exception in an endpoint)
  2. Check the browser's network tab to verify that the response includes the CORS headers
  3. Confirm that the browser displays the actual error message instead of a CORS error

Troubleshooting

If you're still seeing CORS errors:

  1. Check that the middleware is being applied correctly
  2. Verify that the origins in settings.CORS_ORIGINS include the origin of your client application
  3. Ensure that the middleware is being added before the routes are registered
  4. Check for any other middleware that might be interfering with the CORS headers

Authentication Token Management

Problem

The admin panel was experiencing 403 errors when authentication tokens expired, which were not being handled gracefully. This led to a poor user experience where users would suddenly lose access to the application without warning.

Solution

We've implemented an automatic token refresh mechanism in the frontend that:

  1. Checks if the access token is about to expire (within 10 minutes of expiration)
  2. Automatically refreshes the token before it expires using the refresh token stored in an HttpOnly cookie
  3. Queues any in-flight requests during the refresh process and retries them with the new token
  4. Handles 401 errors by attempting to refresh the token and retrying the request

This ensures a seamless user experience without interruptions due to token expiration.

Implementation

The implementation consists of:

  1. A token refresh endpoint in the backend (/auth/refresh-cookie) that uses the HttpOnly refresh token cookie
  2. A function to check if a token is about to expire
  3. Request and response interceptors in the API client that handle token refresh

Token Expiry Check

// Function to check if token is about to expire (within 10 minutes)
const isTokenAboutToExpire = (token: string): boolean => {
  try {
    const decoded = jwtDecode<{ exp: number }>(token);
    const currentTime = Date.now() / 1000;
    // Check if token will expire in less than 10 minutes (600 seconds)
    return decoded.exp - currentTime < 600;
  } catch {
    return true; // If we can't decode the token, assume it's about to expire
  }
};

Request Interceptor

The request interceptor checks if the token is about to expire and refreshes it if needed:

// Request interceptor for adding auth token and handling token refresh
apiClient.interceptors.request.use(async (config) => {
  const token = localStorage.getItem("token");

  // If we have a token and it's about to expire, try to refresh it
  if (
    token &&
    isTokenAboutToExpire(token) &&
    !config.url?.includes("/auth/refresh")
  ) {
    // Refresh token logic here...
  }

  // Add token to request
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }

  return config;
});

Response Interceptor

The response interceptor handles 401 errors by attempting to refresh the token and retrying the request:

// Response interceptor for handling errors
apiClient.interceptors.response.use(
  (response) => response,
  async (error: AxiosError) => {
    const { response, config } = error;

    // Skip if this is a refresh request to avoid infinite loops
    const isRefreshRequest = config?.url?.includes("/auth/refresh");

    if (response && response.status === 401 && !isRefreshRequest) {
      // Token refresh and retry logic here...
    }

    // Other error handling...

    return Promise.reject(error);
  },
);

Testing

To test this implementation, you can:

  1. Log in to the admin panel
  2. Wait until the token is close to expiration (or modify the token in localStorage to have an earlier expiration)
  3. Make a request and verify that the token is refreshed automatically
  4. Check the network tab to see the refresh request and the subsequent request with the new token

Troubleshooting

If you're experiencing authentication issues:

  1. Check that the refresh token cookie is being set correctly
  2. Verify that the token refresh endpoint is working as expected
  3. Ensure that the withCredentials: true option is set for cross-origin requests
  4. Check that the CORS configuration allows credentials