ARFC-1002: Selective SPA Fallback for Subproject Static Assets

Status

Implemented Draft Date: 2026-02-20 Last Call Date: 2026-02-27 Publication Date: 2026-03-06 Version: 1.2

Abstract

This RFC proposes a selective Single Page Application (SPA) fallback mechanism for Vite-built subprojects hosted under /apps/{appName}/. The current configuration returns the subproject index.html for all requests under the subproject prefix, including requests for static assets and Vite-generated code-split JavaScript chunks. This breaks runtime module loading because chunk requests receive HTML instead of JavaScript.

The proposed solution introduces edge-side request rewriting that only falls back to the appropriate index.html for route-like requests, while allowing file-like requests (e.g., *.js, *.css, images, fonts) to be served directly from storage (S3) or return a true 404. This avoids changing Vite’s asset location behavior (which is base-path driven) and restores correct delivery of static assets and code-split chunks. oai_citation:0‡Ayode Institute


Table of Contents

  1. Introduction
  2. Motivation
  3. Problem Statement
  4. Requirements
  5. Proposed Solution
  6. Implementation Notes
  7. Error Handling
  8. Security Considerations
  9. Backward Compatibility
  10. Deployment Plan
  11. Examples
  12. References

1. Introduction

Codermerlin Academy hosts multiple Vite-built frontend subprojects under a static namespace, typically:

/apps/{appName}/

Vite’s production build rewrites all asset paths relative to the configured base option, and treats code-split JavaScript chunks as static assets that must be retrievable from the same base path. oai_citation:1‡v3.vitejs.dev

This RFC defines how the platform must serve those static outputs so that:

The ARFC structure and required sections follow the established template used by ARFC-1001. oai_citation:2‡Ayode Institute


2. Motivation

Current Challenges

  1. Vite base-path coupling
    • Vite bundles assets and dynamic-import chunk paths using the configured base path; there is no supported “assets live elsewhere” override that fits this deployment model without re-architecting the build output layout. oai_citation:3‡v3.vitejs.dev
  2. SPA fallback applied too broadly
    • The current routing returns {appName}/index.html for requests that are actually static asset fetches (e.g., assets/*.js), causing chunk loads to return HTML.
  3. Operational fragility
    • Changes in routing or caching can silently break subprojects when chunk URLs begin resolving to index.html.

Desired Outcome


3. Problem Statement

Under the current configuration, any request to a subproject path is routed to a function/origin behavior that returns the subproject index.html. As a result, requests for:

receive HTML instead of the requested file content, breaking application execution.

This is a serving-layer problem (edge/origin routing), not a frontend bundling problem: Vite’s behavior is correct for a nested base path deployment. oai_citation:4‡v3.vitejs.dev


4. Requirements

  1. Correctness
    • Any request that targets an existing static object MUST be served as that object.
    • SPA fallback MUST NOT be applied to file-like requests.
  2. Per-subproject isolation
    • Each subproject MUST fall back to its own index.html, not the main application’s index.
  3. Low latency
    • Solution SHOULD be implementable at the edge (CloudFront Function preferred) without per-request origin probes.
  4. No changes required to Vite build outputs
    • Subprojects MUST continue to use Vite base="/apps/{appName}/" semantics.

5. Proposed Solution

Overview

Implement selective SPA fallback using a deterministic rule at the edge:

This approach aligns with CloudFront’s request processing model, where edge logic can rewrite the request URI before it reaches the origin. oai_citation:5‡AWS Documentation

5.1. Routing Rules

A request is considered file-like if the final path segment contains a period (.) followed by a non-empty extension:

Examples (file-like):

A request is considered route-like if:

Examples (route-like):

Rule:

5.2. Per-Subproject Fallback Target

For any request under:

/apps/{appName}/...

the fallback MUST be:

/apps/{appName}/index.html

This ensures:


6. Implementation Notes

6.1. CloudFront Function (Viewer Request) Rewrite

Attach a CloudFront Function to the viewer-request event for the static distribution behavior that serves /apps/*.

The function:

  1. Detects whether the path matches /apps/{appName}/...
  2. Determines file-like vs route-like
  3. Rewrites only route-like requests to the corresponding index.html

This avoids CloudFront “custom error response” patterns that can unintentionally rewrite asset 404s into 200 responses with HTML.

6.2. Why Not “Try S3 Then Fallback” at Runtime

A strategy of:

is operationally expensive at the edge, may require origin-response logic, and tends to introduce caching pitfalls (e.g., caching fallback HTML under an asset key). The deterministic rewrite rule avoids origin probing and produces stable caching semantics.


7. Error Handling

  1. Static asset not found
    • If the request is file-like and the object does not exist, the response MUST remain a true not-found (e.g., 404), not index.html.
  2. Subproject not found
    • If the request is under /apps/{appName}/ but the subproject does not exist (missing index.html), the response SHOULD be a true not-found.
  3. Main application fallback
    • Existing main-application fallback behavior MUST remain unchanged and MUST NOT interfere with subproject behaviors.

8. Security Considerations

  1. No privilege escalation
    • This RFC only changes request rewriting for static content paths and does not alter authorization or identity semantics.
  2. Cache correctness
    • Rewriting must be deterministic and only applied to route-like requests to prevent cache poisoning (e.g., storing HTML under a .js URL).
  3. Content-type integrity
    • Ensuring .js requests never return HTML prevents browsers from executing incorrect content and reduces the risk of confusing client-side security controls.

9. Backward Compatibility


10. Deployment Plan

  1. Remove or narrow any “global 404/403 ⇒ index.html” mappings that apply to /apps/* and would convert missing assets into HTML.
  2. Add CloudFront Function for viewer-request on the behavior that matches /apps/*.
  3. Validate
    • Fetch known chunk URLs and verify Content-Type: application/javascript.
    • Fetch known deep links and verify index.html is served.
  4. Rollout
    • Deploy to development, then staging, then production.
  5. Observability
    • Track 404 rates for static assets (expected to rise temporarily if assets were previously masked by fallback).

11. Examples

11.1. CloudFront Function (Viewer Request) Example

function isFileLike(pathname) {
        // File-like if the last segment contains a dot.
        // Examples: /assets/index-ABC123.js, /favicon.ico
        var lastSlashIndex = pathname.lastIndexOf("/");
        var lastSegment = (lastSlashIndex >= 0) ? pathname.substring(lastSlashIndex + 1) : pathname;
        return lastSegment.indexOf(".") !== -1;
}

function handler(event) {
        var request = event.request;
        var uri = request.uri;

        // Match: /apps/{appName}/...
        // Capture {appName} as the segment immediately after "/apps/"
        var prefix = "/apps/";
        if (uri.indexOf(prefix) !== 0) {
                return request;
        }

        var remainder = uri.substring(prefix.length); // "{appName}/..."
        var firstSlash = remainder.indexOf("/");
        if (firstSlash === -1) {
                // Path is exactly "/apps/{appName}" (no trailing slash)
                // Treat as route-like and rewrite.
                request.uri = prefix + remainder + "/index.html";
                return request;
        }

        var appName = remainder.substring(0, firstSlash);
        var afterApp = remainder.substring(firstSlash); // "/..."

        // If request is exactly the app root or a directory navigation, rewrite.
        if (afterApp === "/" || afterApp === "") {
                request.uri = prefix + appName + "/index.html";
                return request;
        }

        // If file-like, do not rewrite.
        if (isFileLike(uri)) {
                return request;
        }

        // Route-like: rewrite to subproject index.
        request.uri = prefix + appName + "/index.html";
        return request;
}

12. References

Infrastructure Files

File Purpose
partition/static/template.yaml Per-color CloudFront distribution with CustomErrorResponses (lines 321-329)
purple/distributor/template.yaml Main distributor CloudFront with Lambda@Edge associations
purple/distributor/functions/partitionDirector/index.js.src Lambda@Edge origin-request function with NARP routing

External References