Angular 404 on Page Refresh After Deployment: 5 Server Fixes

Last updated: October 20, 2026 · Tested with Angular CLI 22.2.3 on Node 24.21, ASP.NET Core 10 (runtime 10.0.12), nginx 1.30.6, http-server 14.1.1 and serve 14.2.6, Windows 11

Short answer: your Angular app works until you refresh a page or open a deep link like /products/42, then the server returns 404 Not Found. The server is looking for a file called products/42 and there isn’t one: only the Angular router knows that route. The fix is a fallback rule that returns index.html for every URL that isn’t a real file, so Angular can boot and route it:

  • ASP.NET Core: app.MapFallbackToFile("index.html");
  • nginx: try_files $uri $uri/ /index.html;
  • IIS: a URL Rewrite rule in web.config (and the URL Rewrite module installed)
  • Apache: FallbackResource /index.html

I first wrote this post in 2022 with only the IIS rule. For this update I built a fresh Angular 22 app and tested every server below. The basic fix takes one line, but a careless fallback creates three new bugs: missing JavaScript files come back as HTML, unknown API calls return 200 with your index.html, and the app breaks in a subfolder. All three are covered below with the real output.

Why the 404 Happens Only on Refresh

When you click a routerLink, Angular changes the URL with the History API and swaps the component. No request goes to the server. When you press F5, paste the URL or open it from a bookmark, the browser asks the server for /products/42. A static server maps that to a file path, finds nothing and returns 404. The home page works because / maps to index.html.

I reproduced it with a production build (ng build, output in dist/shop/browser) and three routes: /products, /products/:id and a ** wildcard. Served by two popular static servers with default options:

## http-server 14.1.1, no options
/                    200 text/html; charset=UTF-8
/products            404
/products/42         404
/main-TTO7UTVS.js    200 application/javascript; charset=UTF-8

## serve 14.2.6, no options
/                    200 text/html; charset=utf-8
/products            404 text/html; charset=utf-8
/products/42         404 text/html; charset=utf-8

The same happens on ASP.NET Core with only UseStaticFiles(), nginx without try_files, and IIS without a rewrite rule. Running ng serve locally never shows the bug, because the dev server already has the fallback built in. That’s why the error usually appears for the first time after deployment.

Side note on old tutorials: many of them (including my 2022 version) say to build with ng build --prod. That flag no longer exists. On Angular CLI 22.2.3:

> ng build --prod
Error: Unknown argument: prod

Use plain ng build. In a new project, angular.json sets "defaultConfiguration": "production" for the build target.

Fix 1: ASP.NET Core (Angular + .NET API in One App)

If your Angular app is served by an ASP.NET Core app (copy the contents of dist/<app>/browser into wwwroot), this is the whole Program.cs:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.UseDefaultFiles();   // "/" -> index.html
app.UseStaticFiles();    // serves wwwroot

app.MapGet("/api/products", () => new[] { new { Id = 42, Name = "Keyboard" } });

// Unknown /api/* URLs must stay 404, not fall through to index.html
app.MapFallback("/api/{**rest}", () => Results.NotFound());

// Any other unmatched route without a file extension -> index.html
app.MapFallbackToFile("index.html");

app.Run();

I ran four versions of this host on .NET 10 against the same Angular build and requested the same URLs from each:

## UseDefaultFiles + UseStaticFiles only
/products            404
/products/42         404
/missing.js          404
/api/missing         404

## + MapFallbackToFile("index.html")
/products            200 text/html
/products/42         200 text/html
/missing.js          404
/api/products        200 text/html      <-- no API yet, gets index.html
/api/missing         200 text/html

## + MapGet("/api/products") + MapFallbackToFile
/api/products        200 application/json; charset=utf-8
/api/missing         200 text/html      <-- the trap
/api/orders/7        200 text/html

## + MapFallback("/api/{**rest}", () => Results.NotFound())
/products/42         200 text/html
/main-TTO7UTVS.js    200 text/javascript
/missing.js          404
/api/products        200 application/json; charset=utf-8
/api/missing         404
/api/orders/7        404

Two details from this output:

  • /missing.js stays 404 with MapFallbackToFile. Its default route pattern is {*path:nonfile}, which skips any URL whose last segment has a file extension. That’s the behaviour you want (see the MIME error below), and it’s why ASP.NET Core is the safest of the servers I tested.
  • The catch-all also catches your API. Without the /api/{**rest} rule, a typo in an API URL returns your index.html with status 200. Your Angular service then tries to parse HTML as JSON and you get this error, which has nothing to do with the real problem:
SyntaxError: Unexpected token '<', "<!doctype "... is not valid JSON

In Angular’s HttpClient it shows up as an “Http failure during parsing” error. If you see either message, check the API URL before you debug the JSON. Order doesn’t matter for these two calls: I registered them both ways and got identical results, because routing prefers the more specific /api/{**rest} pattern over the catch-all. For error responses from real API endpoints, see global error handling with IExceptionHandler.

Finally, I loaded the deep links in headless Edge against this host to confirm the Angular router takes over after the fallback:

http://localhost:5094/products/42      <h1 id="page">Product 42</h1>
http://localhost:5094/products         <h1 id="page">Products</h1>
http://localhost:5094/no/such/page     <h1 id="page">Not found (Angular)</h1>

Note the last line: /no/such/page returns HTTP 200 and Angular’s own “not found” component. Every fallback setup on this page behaves like that. Users see a proper 404 page, but search engines and monitoring see a 200. If your app is public and you care about SEO, pre-render or server-render the real routes, or at least add <meta name="robots" content="noindex"> from your wildcard component.

Fix 2: nginx

The standard fix is one line in the location / block. I tested four configs on nginx 1.30.6 with the same build in the root folder:

# A) no fallback
location / { }

# B) the standard fix
location / { try_files $uri $uri/ /index.html; }

# C) a typo I see a lot: no leading slash
location / { try_files $uri $uri/ index.html; }
                             A (none)   B (try_files /index.html)   C (typo)
/products                    404        200 text/html               404
/products/42                 404        200 text/html               404
/missing.js                  404        200 text/html               404
/products/main-TTO7UTVS.js   404        200 text/html               404
/api/missing                 404        200 text/html               404

Config C looks right but does nothing useful. The last try_files argument is a URI, and without the leading slash nginx joins it straight onto the root folder. The error log shows the exact path it tried (on Linux you’d see /usr/share/nginx/htmlindex.html):

[error] CreateFile() "D:/.../SpaHost/wwwrootindex.html" failed (2: The system cannot find the file specified)

Config B fixes the refresh 404 but returns index.html for everything, including JavaScript files that don’t exist. This is the config I’d put in production:

server {
    listen 80;
    root /var/www/shop/browser;

    # 1) real files (hashed bundles, assets): serve or 404, never index.html
    location ~* \.(?:js|mjs|css|map|ico|png|jpg|svg|webp|woff2?|json|txt)$ {
        try_files $uri =404;
    }

    # 2) API: proxy to ASP.NET Core; unknown API routes keep their 404
    location /api/ {
        proxy_pass http://127.0.0.1:5000;
    }

    # 3) everything else is an Angular route
    location / {
        try_files $uri $uri/ /index.html;
    }
}

With it, nginx gave the same answers as the ASP.NET Core version: /products/42 200, /missing.js 404, /products/main-TTO7UTVS.js 404, /api/products 200 JSON from the .NET app, /api/missing 404. One caution: with this setup an Angular route that starts with /api/ never reaches Angular, so keep client routes out of that prefix.

Fix 3: IIS (web.config)

This is the rule from the original post. Put web.config in the site root, next to index.html:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <rewrite>
      <rules>
        <rule name="Angular Routes" stopProcessing="true">
          <match url=".*" />
          <conditions logicalGrouping="MatchAll">
            <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
            <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
            <add input="{REQUEST_URI}" pattern="^/api/" negate="true" />
          </conditions>
          <action type="Rewrite" url="/index.html" />
        </rule>
      </rules>
    </rewrite>
  </system.webServer>
</configuration>

Changes from 2022: the extra ^/api/ condition keeps unknown API calls out of the fallback, and the action uses /index.html. The old ./index.html only works when the app sits at the site root. For a subfolder, use /your-folder/index.html.

Gotcha: the <rewrite> section needs the IIS URL Rewrite module, which isn’t installed with IIS. Without it, every page fails with HTTP Error 500.19, HRESULT 0x8007000d (“the related configuration data for the page is invalid”). Microsoft’s 500.19 guide lists a missing module such as URL Rewrite as a cause of that exact code. Install the module and the error goes away. I don’t have IIS on this machine, so this section is checked against Microsoft’s documentation rather than run. On Azure App Service for Windows, URL Rewrite is already available. For deploying the .NET side, see deploy ASP.NET Core to Azure App Service.

Fix 4: Apache (.htaccess)

Apache 2.4 doesn’t need mod_rewrite for this. mod_dir has a directive for it:

# .htaccess next to index.html
FallbackResource /index.html

# app in a subfolder: the path must include the folder
# FallbackResource /shop/index.html

Per the Apache documentation, existing files are served normally and only URLs that don’t map to anything fall back. I didn’t run Apache for this update. Like nginx config B, this also sends missing .js files to index.html. If that bites you, add FallbackResource disabled in a <Directory> for your assets folder.

Fix 5: Static Servers (http-server, serve)

Handy for checking a production build locally before you deploy:

npx serve dist/shop/browser -s
npx http-server dist/shop/browser --proxy "http://localhost:8080?"

Both fixed the refresh 404 in my test (/products/42 → 200). Both also returned 200 text/html for /missing.js, so they hide the MIME problem described next.

The MIME Error: “Expected a JavaScript-or-Wasm module script”

This is what a catch-all fallback does when a script URL is wrong. I deployed a build with <base href="./"> behind serve -s and opened /products/42. The browser resolved main.js relative to the current URL, requested /products/main-TTO7UTVS.js, got index.html back with status 200 and this console error (Edge 155):

Failed to load module script: Expected a JavaScript-or-Wasm module script but the server
responded with a MIME type of "text/html". Strict MIME type checking is enforced for module
scripts per HTML spec.

The page stays blank. Older Chrome versions say “Expected a JavaScript module script”. The cause is never the MIME type configuration: it’s a wrong script URL plus a fallback that hides the 404. Fix the <base href> (next section), and use a fallback that returns 404 for missing files (ASP.NET Core’s MapFallbackToFile or the nginx config above) so the real error is visible.

Deploying in a Subfolder: base href

If the app lives at https://example.com/shop/, the fallback is only half the fix. I built the same app three times, deployed each under its own folder on ASP.NET Core with a matching per-folder fallback, and loaded /<folder>/products/42 in headless Edge:

base href "/"        render = <h1>Not found (Angular)</h1>   router saw "/root/products/42"
base href "./"       render = blank page                     main.js requested at /dot/products/ -> 404
base href "/shop/"   render = <h1>Product 42</h1>           correct

Build with the folder name, with slashes on both sides, and point the fallback at that folder’s index.html:

ng build --base-href /shop/
// ASP.NET Core, app in wwwroot/shop
app.MapFallbackToFile("shop/{*path:nonfile}", "shop/index.html");

Windows gotcha I hit: in Git Bash, ng build --base-href /shop/ produced <base href="C:/Program Files/Git/shop/">. Git Bash rewrites arguments that look like Unix paths. Run the command in PowerShell or cmd, or prefix it with MSYS_NO_PATHCONV=1. After that the output was <base href="/shop/">. If a subfolder deployment works on your colleague’s machine but not from yours, check the built index.html.

What About HashLocationStrategy?

The old workaround was provideRouter(routes, withHashLocation()), which gives URLs like /#/products/42. The server only ever sees /, so no fallback is needed. It works anywhere, including hosts where you can’t change the server config. The downsides: uglier URLs, and search engines ignore everything after #, so all your routes look like a single page. Use it for internal tools on locked-down hosting; for anything public, fix the server instead.

Checklist

  1. Reproduce it locally: ng build, then serve dist/<app>/browser without SPA mode and open a deep link. You should see the 404.
  2. Add the fallback for your server (ASP.NET Core, nginx, IIS or Apache above).
  3. Check that /missing.js returns 404, not your HTML.
  4. Check that a wrong API URL returns 404, not your HTML.
  5. In a subfolder: build with --base-href /folder/ and point the fallback at /folder/index.html.

FAQ

Why does refresh work with ng serve but not in production? The Angular dev server already returns index.html for unknown routes. Your production server doesn’t until you configure it.

Does this apply to React or Vue? Yes. Any app that uses the browser History API needs the same fallback. The server rules on this page don’t depend on the framework.

Where do the Angular routes and the API live in a full app? See my Angular CRUD example with ASP.NET Core Web API, the AuthGuard tutorial for protected routes, and the ASP.NET Core tutorials hub for the backend.

References

Similar Posts

4 Comments

  1. Angular bloggers are content creators and software developers who share knowledge, tutorials, best practices, and updates related to the Angular framework. Through blogs, articles, and technical guides, they help developers learn concepts such as components, services, dependency injection, routing, and state management. Their content ranges from beginner-friendly introductions to advanced topics, making Angular development more accessible to a wide audience. Many Angular bloggers also review new framework releases and explain how developers can adopt the latest features effectively.

  2. The article clearly explains a common Angular deployment issue where client-side routes work during navigation but produce a 404 error when the browser is refreshed or a route is entered directly. The key point is that Angular handles subsequent navigation through its client-side router, while a direct browser request sends the route to the production server.

    The explanation of IIS URL rewriting provides a practical solution by redirecting requests that do not correspond to an existing file or directory back to index.html. This allows the Angular router to take over and resolve the requested route correctly. These concepts are useful to explore through Angular Online Course.

  3. The article also demonstrates why understanding the difference between server-side requests and client-side routing is important when deploying single-page applications. Developers working with Angular can use this knowledge to diagnose refresh-related routing problems and configure production environments more effectively through TypeScript Online Course.

  4. The IIS web.config example makes the solution particularly practical, showing how non-file and non-directory requests can be rewritten to the application's index.html. This approach helps ensure that routes such as /products continue to work correctly after deployment, making the topic relevant when developing and deploying Angular Projects.

Leave a Reply

Your email address will not be published. Required fields are marked *