fix(faza-e): refresh dedup, fetch timeout, error surfacing
4 fixes for the symptoms Marius hit on Faza E pilot — search returning
"Eroare la căutare" after sessions got stale, even after relogin:
1. Refresh deduplication
Authentik rotates refresh_tokens — exchange-once. Parallel map +
search + parcela.get all hit jwt callback concurrently, each fires
its own refresh, the first wins, the rest get invalid_grant and
poison the JWT with token.error=RefreshAccessTokenError → user
appears logged out for no good reason. Cache the inflight refresh
promise in-memory keyed by refresh_token so concurrent callers
share one Authentik exchange.
2. Fetch timeout in gis-api-client
AbortSignal.timeout(30s) on every api.gis.ac call. Without it, a
slow upstream (ANCPI scrape, orchestrator hiccup) hangs the route
for the full Next.js default → Marius saw 10s gaps with no
feedback. Throws GisApiError(504, upstream_timeout) instead.
3. Better error surfacing
/api/gis/* routes return { error, hint: <first 200 chars> } on
non-GisApiError throws instead of a bare "internal_error". Easier
to triage from browser DevTools without paging through container
logs.
4. Remove diagnostic [gis-search] logs
Diagnostic served its purpose (identified the stale-token cause
pre-refresh-fix). Now noise; keep only [auth] refresh success/fail
+ per-route internal_error.
Also adds AbortSignal.timeout(8s) on the Authentik refresh fetch
itself to keep the jwt callback bounded.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+47
-19
@@ -143,18 +143,34 @@ function parseRateLimit(res: Response): RateLimit | undefined {
|
||||
};
|
||||
}
|
||||
|
||||
// Default per-request timeout. Light queries (search, parcela.get) usually
|
||||
// finish in <1s; proxy calls (parcel/tech) hit ANCPI live and can take 5-15s.
|
||||
const DEFAULT_TIMEOUT_MS = 30_000;
|
||||
|
||||
async function request<T = unknown>(path: string, opts: RequestOpts = {}): Promise<T> {
|
||||
const token = opts.accessToken || (await bearerFromSession());
|
||||
const res = await fetch(buildUrl(path, opts.query), {
|
||||
method: opts.method || "GET",
|
||||
body: opts.body ?? null,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Accept: "application/json",
|
||||
...opts.headers,
|
||||
},
|
||||
cache: "no-store",
|
||||
});
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(buildUrl(path, opts.query), {
|
||||
method: opts.method || "GET",
|
||||
body: opts.body ?? null,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Accept: "application/json",
|
||||
...opts.headers,
|
||||
},
|
||||
cache: "no-store",
|
||||
signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS),
|
||||
});
|
||||
} catch (err) {
|
||||
const isAbort = (err as { name?: string })?.name === "TimeoutError" ||
|
||||
(err as { name?: string })?.name === "AbortError";
|
||||
throw new GisApiError(
|
||||
504,
|
||||
isAbort ? "upstream_timeout" : "upstream_unreachable",
|
||||
{ hint: (err as Error)?.message },
|
||||
);
|
||||
}
|
||||
|
||||
const rateLimit = parseRateLimit(res);
|
||||
|
||||
@@ -178,15 +194,27 @@ async function request<T = unknown>(path: string, opts: RequestOpts = {}): Promi
|
||||
|
||||
async function rawResponse(path: string, opts: RequestOpts = {}): Promise<Response> {
|
||||
const token = opts.accessToken || (await bearerFromSession());
|
||||
const res = await fetch(buildUrl(path, opts.query), {
|
||||
method: opts.method || "GET",
|
||||
body: opts.body ?? null,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
...opts.headers,
|
||||
},
|
||||
cache: "no-store",
|
||||
});
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(buildUrl(path, opts.query), {
|
||||
method: opts.method || "GET",
|
||||
body: opts.body ?? null,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
...opts.headers,
|
||||
},
|
||||
cache: "no-store",
|
||||
signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS),
|
||||
});
|
||||
} catch (err) {
|
||||
const isAbort = (err as { name?: string })?.name === "TimeoutError" ||
|
||||
(err as { name?: string })?.name === "AbortError";
|
||||
throw new GisApiError(
|
||||
504,
|
||||
isAbort ? "upstream_timeout" : "upstream_unreachable",
|
||||
{ hint: (err as Error)?.message },
|
||||
);
|
||||
}
|
||||
if (!res.ok) {
|
||||
let body: unknown = null;
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user