
Separate browser CORS failures from Server Action origin rejections and fix the policy that is actually blocking the request
A Next.js application can work perfectly on localhost yet fail when opened through a public HTTPS tunnel. The public URL introduces a different browser origin, while a reverse-proxy path can also affect the host information that Next.js evaluates for Server Actions. This guide explains how to identify each failure, configure narrowly scoped CORS behavior, handle credentialed requests safely, verify preflights, and align Server Action origins with the active Localtonet address without weakening application security.
๐ What's in this guide
Why localhost and a public HTTPS URL are different origins
A browser origin is the combination of a URL's scheme, hostname, and port. If any one of those components changes, the browser sees a different origin. For example, http://localhost:3000 and https://app.example.test are different origins because both the scheme and hostname differ. Two localhost addresses can also be different origins when their ports differ.
This distinction matters because a Localtonet HTTP tunnel gives a locally reachable web application a public address. The browser connects to that public address, while the Localtonet client running on the selected device maintains an outbound connection to our relay. The local Next.js process remains the tunnel's target, but the browser-facing origin is the public HTTPS origin rather than the local development origin.
A change of origin does not automatically mean CORS is required. If a page loaded from the public tunnel URL calls a relative endpoint such as /api/profile, the browser normally resolves that endpoint against the same public origin. From the browser's perspective, that is a same-origin request. The fact that the request ultimately reaches a local process through a tunnel does not itself turn the request into a cross-origin fetch.
CORS becomes relevant when browser JavaScript loaded from one origin requests a resource on another. Common examples include a page opened on the public HTTPS origin that still calls an absolute http://localhost:3000/api/... URL, a local frontend calling an API through the public URL, or a separate frontend domain calling a tunneled Next.js API. Server Actions introduce a related but separate origin check, so a rejection involving a Server Action must not automatically be treated as a CORS problem.
The decisive question is which origin loaded the browser page and which origin the JavaScript is requesting. Inspect the complete request URL instead of assuming that the tunnel is the cause.
Identify CORS, preflight, and Server Action failures correctly

Several failures can look similar in the browser because they all occur after switching from localhost to a public address. Before changing configuration, determine which layer rejected the request. Broadly enabling CORS will not fix a Server Action origin mismatch, and adding a Server Action origin will not make a separate API's CORS response valid.
| Symptom | Likely layer | What to inspect |
|---|---|---|
| The console says the CORS header is missing or the origin is not allowed | Browser CORS enforcement | The request origin and the API's Access-Control-Allow-Origin response header |
An OPTIONS request fails before the intended method runs |
CORS preflight handling | Allowed methods, allowed headers, route matching, status code, and headers on the preflight response |
| The browser reports that wildcard origin cannot be used with credentials | Credentialed CORS policy | Whether the response combines Access-Control-Allow-Origin: * with credential use |
| A Server Action is rejected because the request origin and host do not match | Next.js Server Action validation | The browser's Origin, the host information received by Next.js, and the version-specific approved-origin setting |
| The request returns a normal HTTP error in command-line testing but appears blocked in a browser | Application error plus browser enforcement | The actual status and body in the Network panel, followed by the CORS headers on that same response |
| The request succeeds but the session is missing | Fetch credentials or cookie policy | The fetch credentials mode, cookie attributes, domain, scheme, and whether the server permits credentialed CORS |
Start with the browser Network panel
Select the failing request and record its Request URL, Request Method, Status Code, Origin request header, and response headers. If an OPTIONS request appears immediately before the intended request, inspect both entries. The preflight can fail even when the endpoint's GET, POST, or other method would otherwise work.
Also inspect the server terminal. If the application never logs the intended request, the browser may have stopped after a failed preflight. If Next.js logs a Server Action origin or host rejection, work on Server Action validation rather than adding unrelated API headers.
Do not use a command-line success as proof that CORS is correct
CORS is primarily enforced by browsers. A command-line HTTP client can demonstrate that an address is reachable and can show response headers, but it does not reproduce the browser's decision automatically. To test a CORS policy outside the browser, send an explicit Origin header and, for a preflight, send an OPTIONS request with the requested method and requested headers.
Configure CORS only where cross-origin browser access is required
A Next.js API does not need a permissive CORS policy merely because it is reachable through a public URL. If the web page and API use the same public origin, prefer relative URLs and same-origin requests. This removes unnecessary cross-origin complexity and keeps the browser's normal same-origin protections intact.
When a separate trusted frontend must call the API, the API response must explicitly permit that frontend origin. The relevant policy should be limited to the required routes, methods, and request headers. CORS is a response policy, so setting request headers in the frontend cannot grant the frontend permission.
The core CORS response headers
| Header | Purpose | Safe configuration principle |
|---|---|---|
Access-Control-Allow-Origin |
Identifies the origin allowed to read the response | Return the exact trusted origin for authenticated or otherwise sensitive endpoints |
Access-Control-Allow-Methods |
Lists methods accepted after a preflight | List only methods the route actually supports |
Access-Control-Allow-Headers |
Lists non-simple request headers accepted by the endpoint | Include only the headers the client needs, such as Content-Type or an application-specific header |
Access-Control-Allow-Credentials |
Allows credentialed cross-origin browser requests | Enable only when credentials are required and use an explicit allowed origin |
Vary: Origin |
Tells caches that the response can differ according to the request origin | Use it when selecting Access-Control-Allow-Origin dynamically from an allowlist |
For an App Router route handler, one safe pattern is to compare the incoming Origin with an application-owned allowlist, return the matching origin only when it is trusted, and handle OPTIONS explicitly. The following example uses a custom environment variable named APP_ALLOWED_ORIGIN. That name belongs to this example and is not a built-in Next.js or Localtonet setting.
import { NextRequest, NextResponse } from "next/server";
function corsHeaders(request: NextRequest): Headers {
const headers = new Headers();
const requestOrigin = request.headers.get("origin");
const allowedOrigin = process.env.APP_ALLOWED_ORIGIN;
if (
requestOrigin &&
allowedOrigin &&
requestOrigin === allowedOrigin
) {
headers.set("Access-Control-Allow-Origin", requestOrigin);
headers.set("Vary", "Origin");
headers.set("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
headers.set("Access-Control-Allow-Headers", "Content-Type");
}
return headers;
}
export function OPTIONS(request: NextRequest) {
const headers = corsHeaders(request);
if (!headers.has("Access-Control-Allow-Origin")) {
return new NextResponse(null, { status: 403 });
}
return new NextResponse(null, {
status: 204,
headers,
});
}
export async function GET(request: NextRequest) {
const headers = corsHeaders(request);
if (
request.headers.get("origin") &&
!headers.has("Access-Control-Allow-Origin")
) {
return NextResponse.json(
{ error: "Origin not allowed" },
{ status: 403 }
);
}
return NextResponse.json(
{ ok: true },
{ headers }
);
}
Adapt the route path, methods, headers, and response data to the application. If the route accepts Authorization or another custom header, the preflight response must permit that exact header. Do not copy a broad list of methods or headers merely to silence the browser.
The actual response also needs the applicable CORS header. Returning correct headers only from OPTIONS is insufficient because the browser checks the response to the real request as well. Error responses deserve the same attention. An authentication failure or validation error without the expected CORS header can appear to frontend code as an opaque CORS failure, hiding the useful application error.
Pages Router endpoints follow the same policy
A Pages Router API handler sets headers through its response object and should terminate an OPTIONS request before running normal endpoint logic. The security model remains the same: validate the origin against a fixed allowlist, return only an approved origin, advertise only supported methods and headers, and ensure the actual response carries the required policy.
Reading the request's Origin and copying it directly into Access-Control-Allow-Origin is effectively an allow-all policy. Compare it against a server-controlled allowlist first. CORS also does not replace authentication, authorization, CSRF defenses, input validation, or rate controls.
Handle cookies and credentialed cross-origin requests safely

Credentialed CORS is stricter than public, unauthenticated cross-origin access. Browser credentials can include cookies and HTTP authentication information. For a cross-origin fetch, the frontend must request the appropriate credentials mode, and the server must explicitly permit credentials. The browser's cookie rules still apply independently.
A response that uses Access-Control-Allow-Origin: * cannot be used for a credentialed browser request. The server must return the specific approved origin and include Access-Control-Allow-Credentials: true. The origin value is exact, including its scheme and port.
If a cookie does not arrive, do not assume CORS is the only issue. Cookie domain, path, expiry, Secure, and SameSite behavior can affect whether a browser stores or sends it. A cookie created for localhost is not automatically a cookie for the public tunnel hostname. Likewise, moving from local HTTP to public HTTPS changes the browser context in which cookie rules are evaluated.
Use the browser's cookie and Network inspection tools to answer four separate questions:
- Did the server send a
Set-Cookieheader? - Did the browser accept or block that cookie?
- Does the later request include the cookie?
- Does the response contain the exact credentialed CORS headers required by the requesting origin?
Avoid solving development problems by opening authenticated endpoints to every origin. Keep a development allowlist that names only the frontend origins currently needed. Remove obsolete tunnel origins when addresses change, and keep production origins separate from temporary development values.
CORS determines whether browser JavaScript may access a cross-origin response. The endpoint must still authenticate the user and authorize the requested operation. A non-browser client is not constrained by browser CORS enforcement, so sensitive routes cannot rely on CORS as their access-control system.
Fix Next.js Server Action origin validation separately

Server Actions are not ordinary cross-origin API calls with a different label. Next.js applies framework-side request validation to help protect Server Actions from forged cross-site submissions. In the normal case, the request origin should agree with the host through which the application believes it is being accessed. A public HTTPS tunnel or another reverse-proxy layer can expose a mismatch between the browser-visible origin and the host information received by Next.js.
This is why adding Access-Control-Allow-Origin to an API route may have no effect on a rejected Server Action. CORS headers tell a browser whether it can expose a response. Server Action validation can reject the request on the server before a useful action response exists.
Inspect the values involved in the mismatch
Start with the browser request's Origin and the exact public hostname in the address bar. Then inspect the Next.js server error and the host-related information the application receives. Depending on the deployment path, Next.js may evaluate host or forwarded-host information. Do not overwrite forwarded headers in application code simply to force a match. Those headers are security-sensitive and should represent the trusted proxy path accurately.
Next.js has documented an approved-origins configuration for Server Actions, commonly referred to as allowedOrigins. Its exact placement and supported matching syntax are version-specific. Next.js configuration has changed across releases, so this article intentionally does not provide a copy-and-paste configuration block that could be wrong for the installed release.
Check the version declared in the project's lockfile and package manifest, then use the matching official Next.js documentation before adding an approved origin. Confirm whether that release treats the setting as experimental, where it belongs in the configuration object, and whether values are hostnames or complete origins. Do not guess the syntax or approve a broad wildcard.
The safe configuration objective is stable even when syntax changes: approve only the browser-visible development origin that must submit Server Actions, preserve accurate host information through the proxy path, and restart the Next.js process after changing configuration if that release loads the setting at startup.
Treat the active public address as configuration data rather than hardcoding it throughout the source tree. If a generated tunnel address changes, update the approved development value deliberately. A stable selected subdomain or custom domain can reduce address churn where supported, but exact availability and DNS requirements can vary. Check the current Localtonet dashboard and documentation before relying on either option.
Do not confuse a Server Action with an API route
If another origin needs a general-purpose HTTP interface, consider whether an API route or route handler is the clearer boundary. It can expose an intentional request contract, validate authentication, and apply an explicit CORS policy. Server Actions are closely integrated with Next.js rendering and form workflows, and their origin protection should not be disabled merely to turn them into an unrestricted cross-origin API.
Expose the verified local Next.js application with Localtonet

Complete local verification before adding the tunnel. The application should start with its existing project command, render locally, and process the relevant API request or Server Action from its intended local origin. Use the host and port printed by the development server or defined by the project. This guide does not assume a port because projects can override framework defaults.
With Localtonet, the client application on the device establishes an outbound connection to our relay. This means the workflow does not require inbound router port forwarding, firewall changes, VPN setup, or a public IP address. The device running our client must be able to reach the local Next.js listener.
Install and run the Localtonet client
Install our client for the operating system on the device that runs Next.js or can reach it over the local network. Keep the Next.js process running and note its actual local IP address and port.
Authenticate the correct device
Select the device using its device-specific authentication token. Treat the token as a secret and never place it in source code, screenshots, logs, or a CORS configuration.
Select an available relay server
Choose a currently available server or region from the dashboard. Available server codes and regions must come from the current product interface rather than a hardcoded value in the application.
Create an HTTP tunnel to the local listener
Configure an HTTP tunnel with the local IP address and port on which the Next.js application is listening. HTTP tunnels can use Random Sub Domain, Custom Sub Domain, or Custom Domain process types, with current availability determined by the dashboard and applicable plan.
Start the tunnel and record its public HTTPS URL
Creating the tunnel does not start it. Press Start, wait until the selected client and tunnel are connected, and then open the assigned public HTTPS address. Record the origin exactly, without adding an unrelated path.
Align and retest the application policy
If a separate frontend genuinely needs cross-origin API access, add the public origin to the application's narrow development allowlist. If Next.js rejects a Server Action, approve the required origin using the syntax documented for the installed Next.js release. Restart the application when its configuration requires it, then repeat the browser test.
Our HTTP tunnel documentation provides the current product workflow. The public address remains available only while the selected client is connected and the tunnel is running. Stop or delete the tunnel when public access is no longer required.
A development server exposed through a public URL can receive internet traffic. Require authentication for sensitive functions, apply least privilege, avoid exposing debug or administrative endpoints, validate all input, and stop the tunnel after the test. An origin allowlist is not a substitute for endpoint security.
Verify the tunnel, CORS policy, and Server Actions independently
A layered verification process makes failures easier to locate. Test basic reachability first, then same-origin behavior, then intentional cross-origin behavior, and finally authenticated or Server Action workflows. Changing multiple policies before retesting makes it difficult to know which change fixed or weakened the application.
Verify the application locally
Open the local address reported by the Next.js process. Confirm that the target page renders and that the affected endpoint or action works in the original local workflow.
Verify basic public reachability
Open the Localtonet HTTPS URL directly. If the page cannot load, resolve the client connection, tunnel state, local target address, local port, or application listener before investigating CORS.
Test a same-origin request through the public URL
Load the page from the public origin and use a relative API URL. Confirm in the Network panel that the page and request share the same scheme, hostname, and port.
Test the intentional cross-origin request
From the trusted separate frontend, issue the request and inspect its Origin. If there is a preflight, verify the OPTIONS status and allowed origin, method, and headers before inspecting the actual response.
Test credential behavior
Confirm that the frontend requests credentials only when needed, the server returns an exact allowed origin and credential permission, and the browser accepts and sends the relevant cookie.
Invoke the Server Action separately
Trigger the action from the public page while watching the server output. If Next.js reports an origin or host mismatch, correct the version-specific Server Action policy or trusted proxy information rather than broadening API CORS.
Use explicit requests to inspect a preflight
The following command shape can inspect the response to a preflight. Replace every placeholder with the exact public endpoint, trusted frontend origin, intended method, and headers used by the application. The values shown are placeholders, not Localtonet addresses or product commands.
curl -i -X OPTIONS "PUBLIC_ENDPOINT_URL" \
-H "Origin: TRUSTED_FRONTEND_ORIGIN" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type"
Verify that the response permits the supplied trusted origin, the intended method, and every requested non-simple header. Then inspect the actual request separately. A successful preflight does not prove that authentication, validation, or application execution will succeed.
Troubleshooting common Next.js tunnel failures
The page loads, but frontend requests still point to localhost
Search the browser's Network panel for absolute localhost URLs. A browser opened on the public site interprets localhost as the browser user's own machine, not as the tunnel target. For APIs served by the same Next.js application, prefer a relative URL such as /api/items. If a separate origin is intentional, configure that origin explicitly and verify that the requested service is actually reachable from the browser.
The preflight returns 404 or 405
The route may not handle OPTIONS, middleware may not match it, or another layer may reject it before the route runs. Confirm the preflight's exact URL and ensure that the handler serving that path responds to OPTIONS. A method list in a header does not by itself create an OPTIONS handler.
The preflight works, but the actual request is blocked
Check the CORS headers on the actual response. They must be present on successful responses and relevant error responses. Also compare the allowed origin byte for byte with the browser's Origin. A missing scheme, an extra path, or a different port does not match.
The server reflects the origin, but untrusted sites also work
Reflection without validation is the problem. Build the value from a fixed server-side allowlist. If multiple development origins are necessary, compare against all approved values and return only the one that matched the current request. Add Vary: Origin when the response can differ by origin.
Cookies work locally but disappear through HTTPS
Inspect whether the public response sets a cookie for an appropriate domain and whether the browser reports a blocked-cookie reason. Then check the later request. The localhost cookie jar and public hostname cookie jar are different. For a truly cross-origin request, verify the fetch credentials mode and exact credentialed CORS response as separate steps.
The Server Action still reports an origin mismatch
Confirm that the approved value matches the public hostname currently in use and that the application was restarted if necessary. Check that the setting is valid for the installed Next.js release. If the public address changed, an old approved origin will not match. Also investigate whether an application proxy or custom server is replacing host-related headers.
The tunnel exists, but the public URL is unavailable
Creation and execution are separate lifecycle states. Confirm that the correct Localtonet client is connected and that the tunnel was started. Verify that the client device can reach the configured local IP and port. The public endpoint is available only while that selected client remains connected and the tunnel is running.
The public page works intermittently after code or configuration changes
Determine whether the Next.js process restarted, changed its listener, or stopped during compilation. Recheck the local address first. Also confirm that the environment value containing the development origin was loaded by the running process rather than only written to a file. Avoid adding additional origins until the active configuration is known.
Opening every origin appears to fix the issue
That result proves only that origin policy was involved. It does not make the broad policy appropriate. Restore an explicit allowlist and identify the precise browser origin that needs access. For credentialed endpoints and Server Actions, broad wildcard approval weakens protections and may still fail browser credential rules.
Frequently asked questions
Does a Localtonet HTTPS tunnel automatically require CORS?
No. A page loaded from the public HTTPS URL can call a relative API route on that same public origin without becoming cross-origin. CORS is needed only when browser JavaScript loaded from one origin intentionally requests another origin.
Why does my Next.js application work on localhost but fail through the public URL?
The browser-visible scheme, hostname, and possibly port changed. The application may still call an absolute localhost URL, its API may not allow the new requesting origin, its cookies may not apply to the public hostname, or Next.js may reject a Server Action because the origin and host information do not agree.
Will adding Access-Control-Allow-Origin fix a Server Action rejection?
Not necessarily. CORS headers govern browser access to cross-origin responses. Next.js Server Action origin validation is a framework-side protection with its own host and approved-origin behavior. Diagnose and configure the two mechanisms separately.
Can I use Access-Control-Allow-Origin with a wildcard for authenticated APIs?
A wildcard origin is incompatible with credentialed browser requests. Return the exact trusted origin, permit credentials only when required, and continue to enforce authentication and authorization at the endpoint.
Should I add the public tunnel URL to the Next.js Server Action allowed-origins setting?
Add it only when the Server Action is legitimately submitted from that public origin and the normal host comparison does not already accept the request. Verify the exact setting name, placement, and value format in the official documentation for the installed Next.js version. Avoid wildcard approval.
Why does curl work when the browser reports a CORS error?
Command-line clients do not automatically enforce the browser's CORS security model. Use them to inspect connectivity and headers, but include explicit Origin and preflight headers when testing how the server responds to a cross-origin scenario.
Does creating a Localtonet tunnel make it immediately available?
No. Creating a tunnel and running it are separate lifecycle steps. Start the tunnel and keep the selected client connected. The public URL remains available only while that client is connected and the tunnel is running.
Do I need router port forwarding to test Next.js publicly?
No. Our client establishes an outbound connection to a Localtonet relay, so this workflow does not require inbound router port forwarding, firewall changes, VPN setup, or a public IP address. The client device must still be able to reach the local Next.js service.
Test your Next.js application through a controlled public HTTPS origin
Verify the application locally, start a Localtonet HTTP tunnel, and then align only the CORS or Server Action policy that the browser and server evidence show is required. Keep approved origins narrow and stop the tunnel when testing is complete.
Get Started Free โ