fix(admin): retry GoTrue admin reads on transient empty-body responses (bulk-load robustness)

This commit is contained in:
Gerhard Scheikl
2026-05-31 15:35:33 +02:00
parent d317e8c758
commit 17fe642168
4 changed files with 165 additions and 14 deletions
+22 -7
View File
@@ -1,5 +1,6 @@
import type { User } from '@supabase/supabase-js';
import { getSupabaseAdmin } from '@/lib/supabase/admin';
import { withAdminRetry } from '@/lib/admin/retry';
import {
parseOrder,
parseSort,
@@ -128,10 +129,14 @@ export async function getUsersList(opts: {
// paginate the filtered+sorted set.
const matched: User[] = [];
for (let p = 1; p <= USER_SCAN_MAX_PAGES; p++) {
const { data, error } = await admin.auth.admin.listUsers({
page: p,
perPage: USER_SCAN_PER_PAGE,
});
// Retry transient empty-body GoTrue responses so a burst-induced flake
// doesn't abort the full directory scan mid-way.
const { data, error } = await withAdminRetry(() =>
admin.auth.admin.listUsers({
page: p,
perPage: USER_SCAN_PER_PAGE,
}),
);
if (error) throw new Error(error.message);
const us = data.users;
if (us.length === 0) break;
@@ -147,8 +152,12 @@ export async function getUsersList(opts: {
const from = (page - 1) * perPage;
pageUsers = matched.slice(from, from + perPage);
} else {
// Common no-search, default-sort path: cheap single-page lookup.
const { data, error } = await admin.auth.admin.listUsers({ page, perPage });
// Common no-search, default-sort path: cheap single-page lookup. Retry
// transient empty-body responses so the post-mutation auto-refresh that
// hits this path doesn't intermittently 500.
const { data, error } = await withAdminRetry(() =>
admin.auth.admin.listUsers({ page, perPage }),
);
if (error) throw new Error(error.message);
pageUsers = data.users;
total = (data as unknown as { total?: number }).total ?? pageUsers.length;
@@ -281,10 +290,16 @@ export async function getTunnelsList(opts: {
}
// Resolve owner emails (per-row getUserById; acceptable for current scale).
// The user_id comes from an existing tunnel row, so an empty body here is a
// transient burst flake rather than a genuine not-found — retry it. The
// try/catch null fallback remains as a last resort so one bad row can never
// 500 the whole list (it surfaces as "—" only if every retry still fails).
const emails = await Promise.all(
rows.map(async (t) => {
try {
const { data: u } = await admin.auth.admin.getUserById(t.user_id);
const { data: u } = await withAdminRetry(() =>
admin.auth.admin.getUserById(t.user_id),
);
return u.user?.email ?? null;
} catch {
return null;