Improve development environment loopback error message

Make the 403 error user-friendly when accessing the development
environment via 'localhost' instead of 127.0.0.1. The new message
tells the user the exact URL to use instead of the technical
'loopback requests' wording.

Co-Authored-By: Konstantin Wohlwend <n2d4xc@gmail.com>
This commit is contained in:
Devin AI 2026-06-01 17:48:40 +00:00
parent 609579abab
commit d796a91402
3 changed files with 23 additions and 4 deletions

View File

@ -79,7 +79,7 @@ async function localEmulatorIsHealthy(): Promise<boolean> {
export async function GET(req: NextRequest) {
if (!requestHostIsLoopback(req) || !originIsAllowed(req)) {
return NextResponse.json({ error: "Development environment health checks only accept loopback requests." }, { status: 403 });
return NextResponse.json({ error: "You're accessing the development environment using an unsupported address (such as 'localhost'). Please go to http://127.0.0.1:26700 instead — copy and paste this address into your browser." }, { status: 403 });
}
const isRemoteDevelopmentEnvironment = getPublicEnvVar("NEXT_PUBLIC_STACK_IS_REMOTE_DEVELOPMENT_ENVIRONMENT") === "true";

View File

@ -94,7 +94,20 @@ async function getRemoteDevelopmentEnvironmentAccessToken(): Promise<RemoteDevel
},
});
if (!response.ok) {
throw new Error(`Failed to authenticate local remote development environment dashboard (${response.status}): ${await response.text()}`);
const responseText = await response.text();
// For 403 errors (e.g. accessing via 'localhost' instead of 127.0.0.1),
// surface the server's user-friendly message directly
if (response.status === 403) {
try {
const errorMessage = JSON.parse(responseText)?.error;
if (typeof errorMessage === "string") {
throw new Error(errorMessage);
}
} catch (e) {
if (!(e instanceof SyntaxError)) throw e;
}
}
throw new Error(`Failed to authenticate local remote development environment dashboard (${response.status}): ${responseText}`);
}
return parseRemoteDevelopmentEnvironmentAccessTokenResponse(await response.json());

View File

@ -17,6 +17,12 @@ function requestHostIsLoopback(req: NextRequest): boolean {
return isLocalhost(`http://${host}`);
}
function loopbackRejectionMessage(req: NextRequest, state: RemoteDevelopmentEnvironmentState): string {
const port = state.localDashboard?.port;
const suggestedUrl = port != null ? `http://127.0.0.1:${port}` : "http://127.0.0.1:<port>";
return `You're accessing the development environment using an unsupported address (such as 'localhost'). Please go to ${suggestedUrl} instead — copy and paste this address into your browser.`;
}
function requestHostOrigin(req: NextRequest): string | null {
const host = req.headers.get("host");
if (host == null) return null;
@ -55,7 +61,7 @@ export function assertRemoteDevelopmentEnvironmentRequest(req: NextRequest): Nex
}
if (!requestHostIsLoopback(req)) {
return NextResponse.json({ error: "Remote development environment endpoints only accept loopback requests." }, { status: 403 });
return NextResponse.json({ error: loopbackRejectionMessage(req, state) }, { status: 403 });
}
const authorization = req.headers.get("authorization");
@ -78,7 +84,7 @@ export function assertRemoteDevelopmentEnvironmentBrowserRequest(req: NextReques
}
if (!requestHostIsLoopback(req) || !browserRequestOriginIsAllowed(req, state)) {
return NextResponse.json({ error: "Remote development environment endpoints only accept loopback requests." }, { status: 403 });
return NextResponse.json({ error: loopbackRejectionMessage(req, state) }, { status: 403 });
}
const fetchSite = req.headers.get("sec-fetch-site");