Browse Source

bug-fixes

imwald
Silberengel 4 weeks ago
parent
commit
ef3cf1160b
  1. 28
      src/lib/index-relay-http.test.ts
  2. 45
      src/lib/index-relay-http.ts
  3. 14
      src/lib/relay-strikes.test.ts
  4. 3
      src/lib/relay-strikes.ts
  5. 2
      src/services/client-query.service.ts

28
src/lib/index-relay-http.test.ts

@ -0,0 +1,28 @@
import {
IndexRelayTransportError,
clearDevIndexRelayUnavailableThisSession,
isDevIndexRelayUnavailableThisSession,
isIndexRelayTransportFailure
} from '@/lib/index-relay-http'
import { describe, expect, it, beforeEach } from 'vitest'
describe('isIndexRelayTransportFailure', () => {
it('treats IndexRelayTransportError as transport failure', () => {
expect(isIndexRelayTransportFailure(new IndexRelayTransportError())).toBe(true)
expect(isIndexRelayTransportFailure(new IndexRelayTransportError(new Error('HTTP 500')))).toBe(true)
})
it('treats network TypeError as transport failure', () => {
expect(isIndexRelayTransportFailure(new TypeError('Failed to fetch'))).toBe(true)
})
})
describe('dev index relay session skip', () => {
beforeEach(() => {
clearDevIndexRelayUnavailableThisSession()
})
it('starts available after clear', () => {
expect(isDevIndexRelayUnavailableThisSession()).toBe(false)
})
})

45
src/lib/index-relay-http.ts

@ -79,6 +79,7 @@ function warnIndexRelayHttpThrottled(endpoint: string, message: string, meta: Re
/** True when the relay cannot be reached (down, DNS, browser blocked, etc.). Not HTTP 4xx/5xx from a live server. */ /** True when the relay cannot be reached (down, DNS, browser blocked, etc.). Not HTTP 4xx/5xx from a live server. */
export function isIndexRelayTransportFailure(err: unknown): boolean { export function isIndexRelayTransportFailure(err: unknown): boolean {
if (err instanceof IndexRelayTransportError) return true
if (err == null || typeof err !== 'object') return false if (err == null || typeof err !== 'object') return false
const e = err as Error & { name?: string; cause?: unknown } const e = err as Error & { name?: string; cause?: unknown }
if (e.name === 'AbortError') return false if (e.name === 'AbortError') return false
@ -107,6 +108,39 @@ function isDevViteIndexRelayProxyPath(endpoint: string): boolean {
) )
} }
/**
* When the Vite `/dev-index-relay` proxy returns 5xx, skip further dev index HTTP fetches for this tab
* (same pattern as optional `/sites/` and translate proxies). Cleared on a successful filter response.
*/
let devIndexRelayUnavailableThisSession = false
let devIndexRelaySkipLogged = false
export function isDevIndexRelayUnavailableThisSession(): boolean {
return devIndexRelayUnavailableThisSession
}
export function clearDevIndexRelayUnavailableThisSession(): void {
devIndexRelayUnavailableThisSession = false
devIndexRelaySkipLogged = false
}
function markDevIndexRelayUnavailableFromHttpStatus(status: number, endpoint: string): void {
if (!isDevViteIndexRelayProxyPath(endpoint)) return
if (status < 500 || status > 599) return
if (devIndexRelayUnavailableThisSession) return
devIndexRelayUnavailableThisSession = true
if (!devIndexRelaySkipLogged) {
devIndexRelaySkipLogged = true
logger.debug(
`[IndexRelayHttp] Dev index relay returned HTTP ${status}; skipping further dev-index-relay fetches this session (other relays continue).`
)
}
}
function shouldSkipDevIndexRelayFetch(endpoint: string): boolean {
return import.meta.env.DEV && devIndexRelayUnavailableThisSession && isDevViteIndexRelayProxyPath(endpoint)
}
function maybeLogDevIndexRelayUnreachableHint(): void { function maybeLogDevIndexRelayUnreachableHint(): void {
if (import.meta.env.PROD || typeof window === 'undefined') return if (import.meta.env.PROD || typeof window === 'undefined') return
const now = Date.now() const now = Date.now()
@ -191,6 +225,9 @@ export async function queryIndexRelay(
const filters = Array.isArray(filter) ? filter : [filter] const filters = Array.isArray(filter) ? filter : [filter]
const out: NEvent[] = [] const out: NEvent[] = []
const seen = new Set<string>() const seen = new Set<string>()
if (shouldSkipDevIndexRelayFetch(endpoint)) {
return out
}
for (const f of filters) { for (const f of filters) {
const body = nostrFilterToIndexRelayBody(filterForIndexRelay(f)) const body = nostrFilterToIndexRelayBody(filterForIndexRelay(f))
try { try {
@ -213,6 +250,7 @@ export async function queryIndexRelay(
/* ignore */ /* ignore */
} }
if (res.status >= 500 && res.status <= 599) { if (res.status >= 500 && res.status <= 599) {
markDevIndexRelayUnavailableFromHttpStatus(res.status, endpoint)
maybeLogDevIndexRelayHttpErrorHint(res.status, detail || undefined) maybeLogDevIndexRelayHttpErrorHint(res.status, detail || undefined)
} else { } else {
logger.debug('[IndexRelayHttp] filter HTTP response', { logger.debug('[IndexRelayHttp] filter HTTP response', {
@ -228,10 +266,12 @@ export async function queryIndexRelay(
}) })
} }
if (res.status >= 500) { if (res.status >= 500) {
markDevIndexRelayUnavailableFromHttpStatus(res.status, endpoint)
throw new IndexRelayTransportError(new Error(`HTTP ${res.status}`)) throw new IndexRelayTransportError(new Error(`HTTP ${res.status}`))
} }
continue continue
} }
clearDevIndexRelayUnavailableThisSession()
const json = (await res.json()) as { data?: unknown } const json = (await res.json()) as { data?: unknown }
const data = json.data const data = json.data
if (!Array.isArray(data)) continue if (!Array.isArray(data)) continue
@ -245,11 +285,12 @@ export async function queryIndexRelay(
} }
} catch (e) { } catch (e) {
if ((e as Error).name === 'AbortError') throw e if ((e as Error).name === 'AbortError') throw e
if (e instanceof IndexRelayTransportError) throw e
if (isIndexRelayTransportFailure(e)) { if (isIndexRelayTransportFailure(e)) {
handleFilterTransportFailure(endpoint, e) handleFilterTransportFailure(endpoint, e)
} else { throw new IndexRelayTransportError(e)
warnIndexRelayHttpThrottled(endpoint, '[IndexRelayHttp] filter request error', { endpoint, error: e })
} }
warnIndexRelayHttpThrottled(endpoint, '[IndexRelayHttp] filter request error', { endpoint, error: e })
} }
} }
return out return out

14
src/lib/relay-strikes.test.ts

@ -43,6 +43,20 @@ describe('relaySessionStrikes.observeSubscribeBatch', () => {
}) })
}) })
describe('relaySessionStrikes HTTP read failures', () => {
beforeEach(() => {
relaySessionStrikes.reset()
})
it('session-skips after five parallel HTTP failures (no debounce)', () => {
const url = 'https://index.example.com/'
for (let i = 0; i < 5; i++) {
relaySessionStrikes.recordReadFailure(url, 'http')
}
expect(relaySessionStrikes.isReadHttpSkipped(url)).toBe(true)
})
})
describe('relaySessionStrikes.clearKey', () => { describe('relaySessionStrikes.clearKey', () => {
beforeEach(() => { beforeEach(() => {
relaySessionStrikes.reset() relaySessionStrikes.reset()

3
src/lib/relay-strikes.ts

@ -174,7 +174,8 @@ class RelaySessionStrikes {
if (now < e.rateLimitUntil && !this.cacheRelayKeys.has(key)) return if (now < e.rateLimitUntil && !this.cacheRelayKeys.has(key)) return
if (!this.cacheRelayKeys.has(key)) { if (!this.cacheRelayKeys.has(key)) {
if (now - e.readLastStrikeIncrementAt < STRIKE_INCREMENT_DEBOUNCE_MS) return // HTTP index failures often arrive in parallel; count each so session skip engages quickly.
if (_source !== 'http' && now - e.readLastStrikeIncrementAt < STRIKE_INCREMENT_DEBOUNCE_MS) return
e.readLastStrikeIncrementAt = now e.readLastStrikeIncrementAt = now
} }

2
src/services/client-query.service.ts

@ -475,7 +475,7 @@ export class QueryService {
urls urls
.filter((u) => isHttpRelayUrl(u)) .filter((u) => isHttpRelayUrl(u))
.map((u) => normalizeHttpRelayUrl(u) || u) .map((u) => normalizeHttpRelayUrl(u) || u)
.filter(Boolean) .filter((u): u is string => Boolean(u) && !relaySessionStrikes.isReadHttpSkipped(u))
) )
) )
const wsQueryUrls = urls.filter((u) => !isHttpRelayUrl(u)) const wsQueryUrls = urls.filter((u) => !isHttpRelayUrl(u))

Loading…
Cancel
Save