Spaces:
Running
Running
Thomas Wolf commited on
visibility: don't let a stale HF_TOKEN wedge a private Space forever (#1)
Browse filesAfter 3 consecutive non-OK authed /api/spaces responses (e.g. a stale
HF_TOKEN secret carried over when a Space was duplicated), give up on
bucket discovery and unlock with bucketUnverified:true instead of
keeping known:false forever. Network blips (thrown fetches) don't burn
the counter β only consistent auth failures do.
Confirmed-private Spaces can no longer be bricked closed by a bad token.
- server/src/visibility.js +32 -4
server/src/visibility.js
CHANGED
|
@@ -24,6 +24,13 @@ const hfToken = () => process.env.HF_TOKEN || process.env.HUGGING_FACE_HUB_TOKEN
|
|
| 24 |
// private β but we surface a warning so the operator can verify or add a token.
|
| 25 |
let state = { spaceId: SPACE_ID, public: false, known: false, checkedAt: 0, reason: null, bucket: null, buckets: [], bucketUnverified: false };
|
| 26 |
let volumes = null; // bucket ids mounted on this Space; discovered once (fixed until restart)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
const HEADERS = { 'user-agent': 'agent-manager' };
|
| 29 |
// Every HF API call is bounded: a hung request must never wedge the boot
|
|
@@ -31,6 +38,11 @@ const HEADERS = { 'user-agent': 'agent-manager' };
|
|
| 31 |
const HF_TIMEOUT_MS = 8000;
|
| 32 |
const hfFetch = (url, opts = {}) => fetch(url, { ...opts, signal: AbortSignal.timeout(HF_TIMEOUT_MS) });
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
async function discoverBuckets() {
|
| 35 |
if (volumes !== null) return volumes;
|
| 36 |
const token = hfToken();
|
|
@@ -39,12 +51,24 @@ async function discoverBuckets() {
|
|
| 39 |
const r = await hfFetch(`https://huggingface.co/api/spaces/${SPACE_ID}`, {
|
| 40 |
headers: { ...HEADERS, authorization: `Bearer ${token}` },
|
| 41 |
});
|
| 42 |
-
if (!r.ok)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
const j = await r.json();
|
| 44 |
volumes = ((j.runtime && j.runtime.volumes) || [])
|
| 45 |
.filter((v) => v && v.type === 'bucket' && v.source)
|
| 46 |
.map((v) => v.source);
|
| 47 |
-
} catch {
|
|
|
|
|
|
|
|
|
|
| 48 |
return volumes;
|
| 49 |
}
|
| 50 |
|
|
@@ -66,8 +90,12 @@ async function check() {
|
|
| 66 |
// Not publicly visible (401/404) β the Space is private. Now the bucket(s).
|
| 67 |
const buckets = await discoverBuckets();
|
| 68 |
if (buckets === null) { state = { ...state, checkedAt: Date.now() }; return state; } // keep last verdict
|
| 69 |
-
//
|
| 70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
state = { spaceId: SPACE_ID, public: false, known: true, checkedAt: Date.now(), reason: null, bucket: null, buckets: [], bucketUnverified: true };
|
| 72 |
return state;
|
| 73 |
}
|
|
|
|
| 24 |
// private β but we surface a warning so the operator can verify or add a token.
|
| 25 |
let state = { spaceId: SPACE_ID, public: false, known: false, checkedAt: 0, reason: null, bucket: null, buckets: [], bucketUnverified: false };
|
| 26 |
let volumes = null; // bucket ids mounted on this Space; discovered once (fixed until restart)
|
| 27 |
+
// Failure counter for the authed volumes call. A transient network blip
|
| 28 |
+
// should retry; a permanently bad token (e.g. a stale HF_TOKEN carried over
|
| 29 |
+
// when a Space was duplicated) must NOT wedge the Space forever. After this
|
| 30 |
+
// many consecutive failures we give up, treat the bucket as undiscoverable,
|
| 31 |
+
// and unlock with a `bucketUnverified` warning instead of staying closed.
|
| 32 |
+
let bucketDiscoverFails = 0;
|
| 33 |
+
const BUCKET_DISCOVER_MAX_FAILS = 3;
|
| 34 |
|
| 35 |
const HEADERS = { 'user-agent': 'agent-manager' };
|
| 36 |
// Every HF API call is bounded: a hung request must never wedge the boot
|
|
|
|
| 38 |
const HF_TIMEOUT_MS = 8000;
|
| 39 |
const hfFetch = (url, opts = {}) => fetch(url, { ...opts, signal: AbortSignal.timeout(HF_TIMEOUT_MS) });
|
| 40 |
|
| 41 |
+
// Returns:
|
| 42 |
+
// - an array of bucket ids (may be empty) once discovery has succeeded OR
|
| 43 |
+
// has been given up on (so the Space can unlock with a warning).
|
| 44 |
+
// - null only for a TRULY transient failure (first/short retry window) so
|
| 45 |
+
// the caller keeps the last verdict and tries again next cycle.
|
| 46 |
async function discoverBuckets() {
|
| 47 |
if (volumes !== null) return volumes;
|
| 48 |
const token = hfToken();
|
|
|
|
| 51 |
const r = await hfFetch(`https://huggingface.co/api/spaces/${SPACE_ID}`, {
|
| 52 |
headers: { ...HEADERS, authorization: `Bearer ${token}` },
|
| 53 |
});
|
| 54 |
+
if (!r.ok) {
|
| 55 |
+
// 401/403 β the token is bad/expired. A network blip would have thrown.
|
| 56 |
+
// Retry a couple of times in case HF is having a moment, then give up
|
| 57 |
+
// so we don't wedge a confirmed-private Space forever.
|
| 58 |
+
if (++bucketDiscoverFails >= BUCKET_DISCOVER_MAX_FAILS) {
|
| 59 |
+
console.warn(`[visibility] authed /api/spaces returned ${r.status} ${bucketDiscoverFails}x β giving up on bucket discovery; unlocking with bucketUnverified (check the Space's HF_TOKEN secret)`);
|
| 60 |
+
volumes = []; return volumes;
|
| 61 |
+
}
|
| 62 |
+
return null; // transient β retry next cycle
|
| 63 |
+
}
|
| 64 |
const j = await r.json();
|
| 65 |
volumes = ((j.runtime && j.runtime.volumes) || [])
|
| 66 |
.filter((v) => v && v.type === 'bucket' && v.source)
|
| 67 |
.map((v) => v.source);
|
| 68 |
+
} catch {
|
| 69 |
+
// Thrown (DNS/timeout/network) β genuinely transient, don't burn the counter.
|
| 70 |
+
return null;
|
| 71 |
+
}
|
| 72 |
return volumes;
|
| 73 |
}
|
| 74 |
|
|
|
|
| 90 |
// Not publicly visible (401/404) β the Space is private. Now the bucket(s).
|
| 91 |
const buckets = await discoverBuckets();
|
| 92 |
if (buckets === null) { state = { ...state, checkedAt: Date.now() }; return state; } // keep last verdict
|
| 93 |
+
// Treat the give-up path the same as having no token: the bucket is
|
| 94 |
+
// unverifiable, NOT public. Unlock the Space with a warning rather than
|
| 95 |
+
// wedging it closed forever (a bad/stale HF_TOKEN must never brick a
|
| 96 |
+
// confirmed-private Space).
|
| 97 |
+
const bucketUnverified = !hfToken() || bucketDiscoverFails >= BUCKET_DISCOVER_MAX_FAILS;
|
| 98 |
+
if (bucketUnverified) {
|
| 99 |
state = { spaceId: SPACE_ID, public: false, known: true, checkedAt: Date.now(), reason: null, bucket: null, buckets: [], bucketUnverified: true };
|
| 100 |
return state;
|
| 101 |
}
|