How to Fix 403 Forbidden Errors When Deploying Next.js Static Exports on IIS


Deploying a Next.js 14 static export application to Microsoft Internet Information Services (IIS) can be challenging, especially when transitioning from a containerized environment like Azure or SUSE Rancher to a Windows Server with IIS. This article details the process of debugging a 403 - Forbidden: You do not have permission to access this resource error encountered during page refreshes on non-homepage routes (e.g., /dashboard
, /auth/login
) in a Next.js 14 app using the App Router and output: 'export'
. Initially mistaken for a session management issue, the error was ultimately traced to a web.config
misconfiguration. This guide provides a step-by-step troubleshooting process to resolve the issue and ensure clean URLs load correctly on refresh.
Project Setup
The application is a payment web platform built with Next.js 14. Configured with output: 'export'
and trailingSlash: false
in next.config.js
. Upon build, it generates static HTML, CSS, and JavaScript files in the out
directory.
next.config.js
module.exports = {
output: 'export',
trailingSlash: false,
};
Deployed File Structure
The app is deployed to C:\inetpub\wwwroot\Nextjs14App
on IIS 10.0 (Windows Server). The file structure is:
C:\inetpub\wwwroot\Nextjs14App
├── auth
│ ├── create-password.html
│ └── login.html
├── dashboard
│ ├── setup
│ │ └── admin.html
│ ├── transactions
│ │ └── transaction-error.html
│ ├── transactions.html
│ └── audit-logs.html
├── dashboard.html
├── index.html
├── 404.html
└── web.config
Deployment Context
The application was developed and tested in a containerized environment, with a CI/CD pipeline running on Azure and deploying to a SUSE Rancher-managed Kubernetes cluster. However, due to business requirements, the production deployment was shifted to the IIS web server, necessitating manual builds (npm run build && npm run export
) and copying the out
directory to C:\inetpub\wwwroot\Nextjs14App
. While initial post-deployment tests confirmed that users could log in and perform transactions, QA tester later reported a 403 - Forbidden error when refreshing non-root routes (e.g., baseUrl/dashboard
).
The 403 Error
The error, displayed as “403 - Forbidden: You do not have permission to access this resource” in the browser, occurred on page refreshes for routes like /dashboard
or /dashboard/transactions
. Direct .html
access (e.g., baseUrl/dashboard/transactions.html
) worked, and the homepage (/
) loaded correctly, suggesting an issue with URL rewriting or permissions. Initially, I suspected a client-side authentication issue due to session management, but the absence of API routes shifted focus to IIS configuration.
Troubleshooting Process
Step 1: Investigating Session Management
The application initially stored session tokens in sessionStorage
, which persists within a single browser tab and should survive page refreshes. However, I hypothesized that during a hard refresh, the session might be temporarily undefined before client-side hydration completes, potentially triggering a premature 403 – Forbidden error.
To test this theory, I refactored the authentication flow to store session tokens in HTTP cookies instead. Additionally, I introduced a rendering delay on the root page to ensure that client-side hydration fully completed before rendering protected content.
// app/page.tsx
'use client';
import { useState, useEffect } from 'react';
export default function RootPage() {
const [isRendered, setIsRendered] = useState(false);
useEffect(() => {
setIsRendered(true);
}, []);
if (!isRendered) {
return null;
}
return <section>app</section>;
}
After rebuilding and redeploying, the 403 error persisted. The browser’s Network tab showed no API requests before the error, ruling out session management and pointing to an IIS or client-side routing issue.
Step 2: Ruling Out Other Causes
I verified:
Client Code: Token handling and navigation (e.g.,
useRouter
fromnext/navigation
) were correct.Backend: Logs and developer feedback confirmed all services were stable.
Local Testing: Running the built application locally confirmed that routes like
baseUrl/dashboard/transactions
loaded and refreshed correctly, indicating that the issue is isolated to the IIS environment.
The absence of API requests during route refresh suggested a potential IIS configuration issue, most likely related to the web.config
file
Step 3: Analyzing IIS Configuration
The initial web.config
was adapted from a React SPA, designed to rewrite all requests to /index.html
:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<rewrite>
<rules>
<rule name="React Routes" stopProcessing="true">
<match url=".*" />
<conditions>
<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="/" />
</rule>
</rules>
<outboundRules>
<rule name="Remove Server">
<match serverVariable="RESPONSE_Server" pattern=".+" />
<action type="Rewrite" value="DEFAULT" />
</rule>
</outboundRules>
</rewrite>
<security>
<requestFiltering allowDoubleEscaping="true" />
</security>
</system.webServer>
</configuration>
Issues
SPA Rewrite: The rule rewrote all requests to
/
, servingindex.html
for every route (e.g.,/dashboard
). Next.js static exports generate route-specific.html
files (e.g.,dashboard.html
), so this caused incorrect page loads, triggering 403 errors when IIS couldn’t access the expected resource.No Clean URL Mapping: The configuration didn’t map clean URLs (e.g.,
/dashboard
) to their corresponding.html
files (e.g.,dashboard.html
).Permissions Misdiagnosis: Initially, I suspected permissions (e.g.,
IIS_IUSRS
lacking access), but direct.html
access working confirmed permissions were sufficient.
Step 4: Verifying URL Rewrite Module
For rewrite rules to work, the IIS URL Rewrite module must be installed:
In IIS Manager, select the site (
Nextjs14App
).Check for the URL Rewrite icon in Features View.
If missing, download and install from Microsoft’s URL Rewrite Module.
Verify that rules appeared in the URL Rewrite UI, confirming
web.config
syntax was valid.Then restart the IIS
Step 5: Checking File Permissions
To rule out permissions:
Right-clicked
C:\inetpub\wwwroot\Nextjs14App
, selected Properties > Security.Confirmed
IIS_IUSRS
had Read & Execute, List folder contents, and Read permissions for “this folder, subfolders, and files.”Checked the application pool (
DefaultAppPool
, identityApplicationPoolIdentity
), part ofIIS_IUSRS
, ensuring access.No changes were needed, as permissions were correct.
Step 6: Updating web.config
I replaced the React SPA web.config
with one tailored for Next.js static exports:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<httpErrors errorMode="Detailed" />
<rewrite>
<rules>
<!-- Rewrite clean URLs to .html files -->
<rule name="Rewrite to HTML files" stopProcessing="true">
<match url="^(.*)/?$" />
<conditions>
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
<add input="{REQUEST_FILENAME}.html" matchType="IsFile" />
</conditions>
<action type="Rewrite" url="{R:1}.html" appendQueryString="true" />
</rule>
<!-- Fallback to index.html for unmatched routes -->
<rule name="SPA Fallback" stopProcessing="true">
<match url=".*" />
<conditions>
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
</conditions>
<action type="Rewrite" url="/index.html" appendQueryString="true" />
</rule>
</rules>
</rewrite>
<staticContent>
<mimeMap fileExtension=".html" mimeType="text/html" />
<mimeMap fileExtension=".json" mimeType="application/json" />
<mimeMap fileExtension=".woff" mimeType="font/woff" />
<mimeMap fileExtension=".woff2" mimeType="font/woff2" />
</staticContent>
</system.webServer>
</configuration>
Why It Worked
Clean URL Mapping: The first rule rewrites
/dashboard
todashboard.html
ifdashboard.html
exists, supporting Next.js static exports.Trailing Slash Handling: The
^(.*)/?$
pattern matches URLs with or without trailing slashes, ensuring compatibility.SPA Fallback: Unmatched routes (e.g., dynamic routes or 404s) rewrite to
index.html
, allowing client-side routing to handle them.MIME Types: Ensures static assets (e.g.,
.html
,.json
,.woff
) are served correctly.Detailed Errors:
<httpErrors errorMode="Detailed" />
helped diagnose issues during testing.
Step 7: Testing and Results
I deployed the updated
web.config
file toC:\inetpub\wwwroot\Nextjs14App
, restarted the server, and tested the application routes in Incognito mode:baseUrl/
loadedindex.html
baseUrl/dashboard
loadeddashboard.html
baseUrl/dashboard/transactions
loadeddashboard/transactions.html
baseUrl/auth/login
loadedauth/login.html
I also refreshed each route, including variants with trailing slashes (e.g., /dashboard/
). All routes loaded correctly, confirming that the 403 – Forbidden error was resolved.
Root Cause
The 403 – Forbidden error originated from the initial web.config
, which was configured for a React Single Page Application (SPA). It rewrote all incoming requests to /index.html
, preventing IIS from serving route-specific static HTML files (e.g., dashboard.html
). As a result, any direct access or page refresh on non-root routes triggered 403 errors, since the requested resources did not exist under that rewrite rule.
The updated web.config
resolves this by correctly mapping clean URLs to their corresponding .html
files, aligning with Next.js static export behavior and allowing IIS to serve the appropriate content for each route.
Additional Notes
Permissions: While
IIS_IUSRS
permissions were suggested online, they were already correct, as direct.html
access worked.Debugging Tip: If 403 or 500 errors persist, enable Failed Request Tracing in IIS (under Health and Diagnostics) to log detailed rewrite failures.
Conclusion
Deploying a Next.js 14 application using static export to IIS requires a web.config
specifically tailored to map clean URLs to their corresponding route-specific .html
files. This contrasts with typical React SPA configurations that rewrite all requests to a single index.html
.
The 403 – Forbidden error occurred because the initial configuration failed to account for Next.js’s static export structure. Updating the web.config
to correctly handle route mapping resolved the issue, allowing all routes to load properly, including on page refresh.
Subscribe to my newsletter
Read articles from Chukwudi Nweze directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Chukwudi Nweze
Chukwudi Nweze
A frontend developer with 5 years of experience. I simplify complex technical concepts through technical writing.