14 KiB
SDK issues FA Droid
Bugs whose root cause is in the FA SDK (go-fa-api, at
/var/home/soxx/git/go-fa-api, replaced in go.mod) not in this app.
The SDK is a separate module, maintained and fixed by a separate AI that does not see this app's code or conversation context. This file is the handoff: every entry is a self-contained bug report meant to be copy-pasted to that AI as-is.
App-side bugs (frontend / go-service) live in ISSUES.md; fixed issues in
SOLVED.md. Features with UI but no backend are in STUBS.md; Wails/Android
stack traps in PITFALLS.md.
Status legend: [ ] open · [~] diagnosed handoff brief drafted ·
[x] fixed in the SDK (then moved to SOLVED.md)
Numbering: issue numbers are shared across ISSUES.md, SOLVED.md and
this file draw the next free number from the same sequence.
SDK handoff brief required fields
Every entry MUST carry a handoff brief detailed enough to be copy-pasted to the SDK AI as a standalone bug report. Do not write "sdk issue?" and stop — that is not actionable for someone without this repo open.
A handoff brief must answer all seven of:
- SDK entry point the exact exported function involved
(e.g.
Client.Fav(ctx, SubmissionID)inactions.go). Name the file. - How the app calls it which
internal/services/*.gowrapper invokes it, and with what arguments. - Observed behaviour what the SDK call returns: an error (quote it verbatim), a wrong value, or a silent no-op.
- Expected behaviour what FurAffinity should do server-side and what the SDK should return.
- Reproduction concrete inputs (submission ID, username) and the steps that trigger it.
- Suspected layer HTML parsing (FA changed its markup?), request
shape (wrong form fields / missing CSRF / wrong URL), auth/cookies, or
rate-limiting. Point at the likely file: parsers are
*_parser.go, form posts are inactions.go/*_post.go/*_send.go. - What is NOT the SDK explicitly rule out the app layer so the SDK AI does not chase a frontend bug. State what you verified.
If you cannot yet fill all seven fields, the issue stays [~] and the brief
lists exactly which diagnostics are still needed never hand over a
half-brief as if it were complete.
Issues
#4 [~] Audit the FA SDK for bugs
- Where:
go-fa-api(sibling module,replaced ingo.mod). - Task: Standing umbrella review the SDK surface used by
internal/services/for incorrect parsing, missing endpoints, or wrong request shapes. - Cause tag:
sdk - Progress: The first audit pass produced #1, #5, #9, #10 and #14 all
now fixed and verified (#1/#5/#9/#10 in app v0.0.7, #14 in v0.0.10; see
SOLVED.md). #15 was then fixed too (SDK gaveWithResolvedAvatarsa count limit; app v0.0.14). #17 (priority rate limiting) was implemented too — verified in app v0.0.16. No open SDK issues currently. Keep this entry as the umbrella for future audit passes; file each concrete finding as its own numbered issue.
No open SDK issues. Fixed ones are recorded in SOLVED.md.
Add new SDK issues below
#18 [x] Rate limiter: upgrade from 2-level to N-level priority
Implemented in the SDK update options.go now exposes WithPriority
and a multi-level Priority type; WithPrioritizedRateLimiting honours it.
FA Droid follow-up (assign tiers to reads/writes/preload/crawl) is tracked as
app-side work, not an SDK issue.
- Component:
go-fa-apiratelimit.go,options.go. - Today: the limiter has exactly two priorities.
WithBackgroundPriority(ctx)marks a request background; everything else is foreground. Background requests wait until no foreground request is queued. Enabled viaWithPrioritizedRateLimiting(true). - Need: a consumer (FA Droid) wants three+ tiers, not two:
- user-interactive (the page on screen, user write actions),
- speculative neighbor preload (likely-next submissions),
- bulk background crawl (inbox/watchlist warming). With only two levels, neighbor preload must either compete with the live page (tier 1) or interleave with the bulk crawl (tier 2) neither is right.
- Proposed API: keep
WithBackgroundPriorityworking (maps to the lowest level) and addWithPriority(ctx, Priority)wherePriorityis an ordered enum, e.g.PriorityInteractive,PriorityNormal(default),PriorityLow,PriorityBackground. The limiter serves waiting goroutines highest-priority-first; the token emission rate is unchanged. - Compatibility: default (no marker) must remain
PriorityNormal;WithBackgroundPrioritymust remain equivalent toPriorityBackground. - Why it matters to the app: FA Droid will then assign tier 1 to
current-page reads + the write-action queue worker, tier
PriorityLowto neighbor preload, andPriorityBackgroundto the inbox crawler.
#21 [x] GetSubmission doesn't expose the viewer's favorite state
- Where:
go-fa-apisubmission.go(Submissionstruct +parseSubmission). - Symptom: A submission the logged-in user has favorited shows the
favorite heart as empty on the app's submission detail page. There is no
way to know a submission's favorite state from a
GetSubmissionresult. - Expected:
GetSubmissionshould report whether the authenticated viewer has favorited the submission, so the UI can render the correct heart state. - Cause tag:
sdk - SDK handoff brief:
- Entry point:
Client.GetSubmission(ctx, SubmissionID)insubmission.go, which builds the result viaparseSubmission(id, doc). The returnedSubmissionstruct (submission.go:17-38) has fields ID, Title, Author, PostedAt, Rating, Category, Type, Species, Gender, Description, DescriptionText, Tags, FileURL, ThumbURL, Width, Height, Stats, Folders, Prev, Next and nothing indicating favorite state. - How the app calls it:
SubmissionService.getCachedininternal/services/submission.gocallsGetSubmission, thendto.FromSubmission(internal/dto/types.go) copies the struct to the wire DTO. The frontend (SubmissionView.svelte) doesfavorited = !!sub.favorited. - Observed behaviour:
GetSubmissionreturns a*Submissionwith no favorite-state field. It is not a wrong value or an error the datum is simply absent from the SDK's public type. - Expected behaviour: When
/view/{id}/is fetched with valida/bcookies, FA renders either a+Fav(/fav/{id}/...) or a−Fav(/unfav/{id}/...) anchor exactly one, matching the viewer's current state. The SDK should surface this onSubmission, e.g. a newFavorited boolfield (final name your call), set true when the page shows the/unfav/link. On an anonymous (no-cookie) fetch neither link is present →Favoritedfalse, which is correct. - Reproduction: With cookies set, favorite submission X on FA, then
call
GetSubmission(ctx, X)the result cannot express that X is favorited. - Suspected layer: HTML parsing
parseSubmission. The SDK already has the exact parser:findFavLinks(doc, subID) (favURL, unfavURL string)inactions.go(used bytoggleFavoritefor its idempotency check).unfavURL != ""means "currently favorited."parseSubmissionjust needs to run that check and set the new field. No new scraping logic required. - What is NOT the SDK: The app side is verified ready and correct.
SubmissionView.sveltealready readssub.favoritedand types it (favorited?: boolean);getCachedcorrectly busts and re-fetches the submission cache after a fav write (viaWriteService/ the action queue). The value never reaches the app only because the SDKSubmissionstruct has no field to carry itdto.FromSubmissionhas nothing to map.
- Entry point:
- Related (please also check): the same gap likely exists for watch
state
findWatchLinksexists inactions.go, but theUserstruct may not expose whether the viewer watches that user. Worth fixing in the same pass. - When it lands (FA Droid follow-up): add
Favorited booltodto.Submission(json:"favorited"), map it indto.FromSubmission, and regenerate bindings. The frontend already consumessub.favorited, so no UI change is needed. - Status: done SDK update added
Submission.Favorited; app wired it in v0.0.22.
#23 [ ] SubmissionInbox yields only the first page (~72 items)
- Where:
go-fa-apiinbox.go(SubmissionInbox+parseSubmissionInboxPage). - Symptom: The new-submission inbox shows only ~72 items even when the account has thousands pending. A user with ~4718 inbox submissions sees one page.
- Expected:
SubmissionInboxshould walk every cursor page until FA stops rendering a "Next 72" link. - Cause tag:
sdk - SDK handoff brief:
- Entry point:
Client.SubmissionInbox(ctx, ListOptions)ininbox.go, whose iterator followsparseSubmissionInboxPage's returnednextURL. - How the app calls it:
InboxService.StreamSubmissions(internal/services/inbox.go) runs one goroutine that doesfor sub, err := range client.SubmissionInbox(ctx, fa.ListOptions{})—ListOptions{}meansMaxPages: 0(unbounded), so the app does not cap the crawl. - Observed behaviour: the
rangecompletes after ~72 items the iterator yields one page then ends. Verified on-device: the app's crawl channel (fed one item peryield) closes after exactly one ~72-item chunk, soStreamSubmissionsreportsHasMore: falseimmediately after page 1. - Expected behaviour: FA's
/msg/submissions/paginates via a "Next 72" link encoding a from-id cursor; the iterator should follow it across all ~66 pages for a 4718-item inbox. - Reproduction: logged-in client with a large submission inbox;
count := 0; for range client.SubmissionInbox(ctx, ListOptions{}) { count++ }yields ~72, not the true total. - Suspected layer: HTML parsing
parseSubmissionInboxPage. Its next- cursor selector isdiv.messagecenter-navigation a.button.more; if FA changed that markup the parser returnsnextURL == ""and the iterator stops after page 1. Check the selector against current/msg/submissions/HTML (and the cursor-URL construction). - What is NOT the SDK: app side verified.
StreamSubmissionsranges the iterator fully on a single goroutine and streams everything it yields; it passesListOptions{}(noMaxPagescap). On-device logging shows the crawl ends after one chunk the iterator simply stops yielding.
- Entry point:
#24 [x] Request logger drops context.Context (breaks trace propagation)
Fixed in the SDK transport.go's logRequest now emits its record via
logger.InfoContext(req.Context(), "fa.request", …) instead of logger.Info,
so a context-aware slog.Handler recovers the caller's active span and the
HTTP span nests under the RPC span. A regression test,
TestTransport_LogRequest_PropagatesRequestContext in transport_test.go,
installs a context-capturing handler and asserts a sentinel value threaded
through req.Context() reaches the slog record guarding against a silent
revert to Info.
- Where:
go-fa-apithe HTTP transport's request logging (transport.go, thelogRequesthelper / whereverslogrecords an outgoing request). - Type: Enhancement, not a bug the SDK works correctly; this is a one-line change the app needs for distributed tracing.
- Symptom: The app (FA Droid, WI-10) added OpenTelemetry spans. An app RPC
opens an
rpcspan and threads itscontext.Contextinto the SDK call. The SDK's per-requestslogline is currently emitted withlogger.Info(...), which carries no context so the app'ssloghandler cannot recover the active span, and each HTTP span becomes an unparented root instead of a child of the RPC span. - Expected: the request log record should carry the request's context so a
context-aware
slog.Handlercan read the active span from it. - The fix (one line): change the request-logging call from
logger.Info("fa.request", …)tologger.InfoContext(req.Context(), "fa.request", …)(use the*http.Request's ownContext()).slogalready supportsInfoContext; no API change, no new dependency. - Cause tag:
sdk - SDK handoff brief:
- SDK entry point: the HTTP transport
RoundTrip/ request path intransport.gospecifically theslogcall that logs each outgoing FA request (logRequest, or inline). All SDK client methods route through it. - How the app calls it: every
internal/services/*.gowrapper calls aClient.*method with acontext.Contextthat now carries an OTel span; that ctx reaches the*http.Request(req.Context()). - Observed behaviour: the request
slogrecord is created without a context, soHandler.Handlereceivescontext.Background(). - Expected behaviour: the record carries
req.Context(), so a context-aware handler can extract the active span. - Reproduction: with WI-10's
diagslog handler installed, every HTTP span hasparentSpanId == ""even when the call was made inside an RPC span. After theInfoContextchange, the HTTP span nests under the RPC span on the same trace id. - Suspected layer: request shape / logging purely the logging call, no parsing or auth involved.
- What is NOT the SDK behaviour-wise: no functional SDK behaviour
changes request execution, parsing, retries, rate-limiting are all
untouched. This only changes which
slogmethod is used so context flows.
- SDK entry point: the HTTP transport
- Note: WI-10 applied this change to the local
replaced working copy ofgo-fa-apiso FA Droid v0.0.33 has working rpc→http span linkage. It is not committed in the SDK repo. Until it is upstreamed, a clean SDK checkout will degrade gracefully HTTP spans become valid but unparented roots (no crash, no data loss, only the rpc↔http link is lost).