fix(api): stop reporting a misconfigured server as an unreachable platform
`POST /api/auth/login` answered 502 platform_unreachable — "Could not reach Loyaly. Check your connection and try again." — for a fault that is entirely ours and that no connection check can fix. `buildUrl()` is where LOYALY_API_BASE is read, and it was called INSIDE the try block whose catch turns a failed fetch into UpstreamError(0, 'network'). So the config guard threw, the catch swallowed it, and "nobody set LOYALY_API_BASE" arrived at the route indistinguishable from "the platform is down". Measured on the pre-fix code, all of these produced the identical UpstreamError(status=0, code=network): LOYALY_API_BASE unset LOYALY_API_BASE=https://platform.loyaly.ai (the known-wrong host) LOYALY_API_BASE=not-a-url nothing listening on the far end (the only real network failure) The message survived, so the truth was reachable, but only by reading the prose of an error the code had already classified as a network fault — and the login route had by then replaced it with advice about the user's wifi. ConfigError now exists for this, buildUrl is resolved before the try in both upstreamRequest and upstreamRaw, and callers branch on it: 500 misconfigured, not 502 unreachable. 500 is the honest status — a bad gateway says the thing upstream is unwell, and this server has not got as far as having an upstream. Verified after the change: the four config faults raise ConfigError, and a dead port still raises UpstreamError(0, 'network'). The detail is logged, never returned. It names an environment variable and the hosts this console accepts, which belongs in the Dokploy log pane rather than in an anonymous sign-in form's response body. `[loyaly] configuration error:` is now the line to grep for. Also fixes failJson labelling a 5xx as `unauthorized` in the envelope: the code is derived from the status now, so a misconfigured server can no longer tell a browser the password was wrong. That one costs somebody a password reset for a fault they cannot see. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import 'server-only';
|
||||
import type {NextRequest} from 'next/server';
|
||||
import {UpstreamError} from '@/services/api/apiClient';
|
||||
import {ConfigError, UpstreamError} from '@/services/api/apiClient';
|
||||
import {NoSessionError, withUpstream} from '@/features/auth/services/upstreamSession';
|
||||
import {ok, parseQuery, type Query} from '@/shared/services/apiRoute';
|
||||
import type {ApiErrorCode} from '@/shared/types/api';
|
||||
@@ -46,6 +46,28 @@ export function failureFrom(err: unknown): UpstreamFailure {
|
||||
return {status: 401, code: 'unauthorized', message: err.message};
|
||||
}
|
||||
|
||||
/**
|
||||
* A misconfigured deployment. 500, never 502 — a bad gateway says "the thing
|
||||
* upstream is unwell, try later", and this server has not got as far as
|
||||
* having an upstream. Retrying and checking the platform's health both waste
|
||||
* the reader's time.
|
||||
*
|
||||
* The detail is LOGGED, never returned. It names an environment variable and
|
||||
* the hosts this console does and does not accept, which is operator
|
||||
* information and not something to hand an anonymous browser. The log line is
|
||||
* the only copy, and it is what shows up in `docker logs` / the Dokploy log
|
||||
* pane the moment the first request comes in.
|
||||
*/
|
||||
if (err instanceof ConfigError) {
|
||||
console.error('[loyaly] configuration error:', err.message);
|
||||
return {
|
||||
status: 500,
|
||||
code: 'internal',
|
||||
message: 'This console is not configured correctly. Please contact support.',
|
||||
reason: 'misconfigured',
|
||||
};
|
||||
}
|
||||
|
||||
if (err instanceof UpstreamError) {
|
||||
// 501 is "the feature is off for this deployment", not a fault. It is
|
||||
// surfaced as its own reason so a panel can say "not available here"
|
||||
|
||||
Reference in New Issue
Block a user