Why Relative Paths Don’t Always Work: A FastAPI Static Assets Case Study

When building web applications, choosing between relative and absolute paths for static assets seems straightforward. Most developers reach for relative paths as the “safe” choice – after all, they’re supposed to work regardless of where your app is deployed, right?

Well, not always. I recently ran into a situation where relative paths completely broke my FastAPI application, and the solution required understanding some subtle but important concepts about how browsers resolve URLs.

The Setup: Multiple Entry Points to the Same Content

I had a FastAPI application where the same HTML file could be accessed in two different ways:

  1. Via FastAPI route: https://api.example.com/my-app/ (routes to /)
  2. Via static file: https://api.example.com/my-app/static/html/index.html

This is a common pattern – you want users to access your app via a clean URL, but you also mount static files for direct access.

Why Regular Relative Paths Failed

Let’s say I used relative paths in the HTML:

<link rel="stylesheet" href="../css/style.css">
<script src="../js/main.js"></script>

Here’s what happens in each scenario:

Scenario 1: Accessed via FastAPI route (/my-app/)

  • Browser thinks it’s at: https://api.example.com/my-app/
  • Relative path ../css/style.css resolves to: https://api.example.com/css/style.css
  • This is wrong – it should be /my-app/static/css/style.css

Scenario 2: Accessed via static file (index.html)

  • Browser thinks it’s at: https://api.example.com/my-app/static/html/
  • Relative path ../css/style.css resolves to: https://api.example.com/my-app/static/css/style.css
  • This is correct

The Root Cause: Context Matters

The problem is that relative paths are resolved based on the current URL, not the file location. The same HTML file needs to work from different URL contexts:

Context 1: /my-app/                    (FastAPI route)
Context 2: /my-app/static/html/        (Static file access)

So the same relative path ../css/style.css means completely different things depending on how the user reached the page.

Why Absolute Paths Also Failed

You might think, “Just use absolute paths!” But that creates a different problem:

<link rel="stylesheet" href="/static/css/style.css">
  • Localhost: Works fine → http://localhost:8080/static/css/style.css
  • Production subpath: Fails → tries https://api.example.com/static/css/style.css instead of https://api.example.com/my-app/static/css/style.css

The HTML <base> Tag Attempt

I tried using the <base> tag to fix the relative path context:

<script>
    const base = document.createElement('base');
    const path = window.location.pathname;

    if (path.includes('/static/html/')) {
        base.href = '../';  // For static file access
    } else {
        base.href = 'static/';  // For FastAPI route
    }

    document.head.insertBefore(base, document.head.firstChild);
</script>

But this created other issues and was still fragile because it required detecting the access method.

The Solution: Environment-Aware Dynamic Loading

The solution that worked was JavaScript-based dynamic loading with environment detection:

// Dynamic path detection for production subpath support
const isProduction = window.location.hostname.includes('api.example.com');
const basePath = isProduction ? '/my-app' : '';

// Add CSS with correct path
const css = document.createElement('link');
css.rel = 'stylesheet';
css.href = basePath + '/static/css/style.css';
document.head.appendChild(css);

// Store base path for use in other scripts
window.APP_BASE_PATH = basePath;

This works for both scenarios:

  • FastAPI route: /my-app/ → loads /my-app/static/css/style.css
  • Static file: index.html → loads /my-app/static/css/style.css

When Relative Paths Work vs. When They Don’t

Relative paths work great when:

  • You have predictable, single URL structures
  • Content is always accessed from the same context
  • You’re not dealing with subpath deployments
  • Your app runs at the domain root

Relative paths break down when:

  • The same content is accessible from multiple URL contexts
  • You’re dealing with subpath deployments
  • You have both route-based and file-based serving
  • Your deployment strategy involves path rewriting

Alternative Solutions

Beyond dynamic loading, other approaches include:

  1. Server-side templating: Inject the correct base URL during HTML generation
  2. Environment variables: Set a BASE_PATH variable and use it consistently
  3. Reverse proxy configuration: Handle path rewriting at the infrastructure level
  4. Eliminate dual access: Force all access through a single URL pattern

The Lesson

Context is everything when it comes to relative paths. They’re not automatically portable – they depend on where the browser thinks it is, not where your files actually live.

When building applications that might be deployed in various configurations, consider the URL contexts your static assets will need to work from. Sometimes the “simple” solution of relative paths is actually the most complex one to get right.

Environment-aware absolute paths, while requiring more setup, often provide more predictable and maintainable solutions for complex deployment scenarios.

Further Reading

For deeper understanding of URL resolution and related concepts:

Continue Reading