search for standard sites pub-search.waow.tech
search zig blog atproto
11
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(subscriptions): translate bsky delivery errors to actionable text

when the bot fails to DM a subscriber the raw error (e.g.
"FetchFailed: getConvo bad_request: NotFollowedBySender") meant
nothing to the user. now we map known bsky chat error codes to
friendly text with an action link:

- NotFollowedBySender / ActorNotMessageable →
"bsky blocked this DM — follow @pub-search.waow.tech or set your DM
preference to 'Everyone'" with a direct profile link
- RateLimit / TooManyRequests → "bsky rate-limited us, will retry"
- LoginFailed / BotNotConfigured → "server problem, not yours"

raw error still shown underneath in dim italic for debugging.
fallback: show raw for unknown codes.

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>

+30 -2
+30 -2
site/subscriptions.html
··· 240 240 margin-top: 2px; 241 241 word-break: break-word; 242 242 } 243 + .sub-error a { color: inherit; text-decoration: underline; } 244 + .sub-error .raw { 245 + color: var(--text-dim); 246 + font-size: 10px; 247 + font-style: italic; 248 + } 243 249 244 250 .bot-notice { 245 251 font-size: 12px; ··· 373 379 } 374 380 375 381 // build a lookup map: publication at-uri → subscription rkey 382 + // translate a raw backend lastError into a user-facing string with 383 + // an optional action link. matches known bsky chat error codes; 384 + // falls back to the raw string for anything else. 385 + function friendlyDeliveryError(raw) { 386 + if (!raw) return null; 387 + const botProfile = `https://bsky.app/profile/${BOT_HANDLE}`; 388 + if (raw.includes('NotFollowedBySender') || raw.includes('ActorNotMessageable')) { 389 + return { 390 + html: `bsky blocked this DM — <a href="${botProfile}" target="_blank" rel="noopener">follow @${escapeHtml(BOT_HANDLE)}</a> or set your bsky DM preference to "Everyone"`, 391 + showRaw: true, 392 + }; 393 + } 394 + if (raw.includes('RateLimit') || raw.includes('TooManyRequests')) { 395 + return { html: 'bsky rate-limited us — next matching document will retry', showRaw: true }; 396 + } 397 + if (raw.includes('LoginFailed') || raw.includes('BotNotConfigured')) { 398 + return { html: 'the pub-search bot can\'t sign in right now — this is a server problem, not yours', showRaw: true }; 399 + } 400 + return { html: escapeHtml(raw), showRaw: false }; 401 + } 402 + 376 403 // build pubUri → sub map so each row can show its toggle state + 377 404 // any last delivery error in one lookup. 378 405 function indexSubs(subs) { ··· 395 422 const subbed = !!sub; 396 423 const row = document.createElement('div'); 397 424 row.className = 'pub'; 398 - const errorLine = sub?.lastError 399 - ? `<div class="sub-error">⚠️ last delivery: ${escapeHtml(sub.lastError)}</div>` 425 + const friendly = sub?.lastError ? friendlyDeliveryError(sub.lastError) : null; 426 + const errorLine = friendly 427 + ? `<div class="sub-error">⚠️ ${friendly.html}${friendly.showRaw ? `<br><span class="raw">${escapeHtml(sub.lastError)}</span>` : ''}</div>` 400 428 : ''; 401 429 402 430 row.innerHTML = `