From 286b61de4dfc8ade0eca87c2744af270a0f0a020 Mon Sep 17 00:00:00 2001 From: ety001 Date: Thu, 28 May 2026 13:09:52 +0800 Subject: [PATCH] fix: emulate get_state for /~witnesses and /proposals paths steemd returns -32000 Server error for get_state(/~witnesses) and get_state(/proposals), causing wallet SSR to timeout with 504. This extends the get_state workaround to intercept these special paths and return a minimal synthetic response with feed_price, props, etc. The actual witness/proposal data is fetched client-side via separate API calls. Also cleans up DEBUG slog.Info calls from previous PR, converting them to slog.Debug for production-appropriate logging. DEPRECATED: All get_state workarounds in this file will be removed once the condenser rewrite and wallet rewrite are deployed. Both new versions do not use get_state at all. --- internal/handlers/get_state_workaround.go | 153 +++++++++++++++++++--- internal/handlers/processor.go | 39 ++++-- 2 files changed, 165 insertions(+), 27 deletions(-) diff --git a/internal/handlers/get_state_workaround.go b/internal/handlers/get_state_workaround.go index 999604a..1519b05 100644 --- a/internal/handlers/get_state_workaround.go +++ b/internal/handlers/get_state_workaround.go @@ -16,42 +16,54 @@ import ( ) // ============================================================================ -// TEMPORARY WORKAROUND: get_state sub-path emulation (with Redis cache) +// TEMPORARY WORKAROUND: get_state path emulation (with Redis cache) // ============================================================================ // // Background: // steemd's condenser_api.get_state supports a limited set of sub-paths: // - /@user/recent-replies, /@user/posts, /@user/comments // - /@user/blog, /@user/feed +// - /trending, /hot, /promoted, /created, etc. // -// The following sub-paths are NOT handled by steemd's get_state: +// The following paths are NOT handled by steemd's get_state: +// +// Category 1 — User sub-paths (returns -32602 "Invalid parameters"): // - /@user/transfers // - /@user/author-rewards // - /@user/curation-rewards // - /@user/delegations // -// Calling get_state with these paths causes steemd to return -// -32602 "Invalid parameters". Some third-party clients (old wallet), -// direct API calls, and SSR requests still use these paths, causing -// 504 timeouts when the client retries/waits for ~350 seconds. +// Category 2 — Special pages (returns -32000 "Server error"): +// - /~witnesses +// - /proposals +// +// Some third-party clients (old wallet), direct API calls, and SSR requests +// still use these paths, causing 504 timeouts when the client retries/waits +// for ~350 seconds. // -// The new wallet (Next.js) has completely migrated away from get_state -// and uses direct API calls (get_account_history, get_vesting_delegations). +// The new wallet (Next.js) and condenser rewrite will completely migrate +// away from get_state and use direct API calls. // // Solution: -// Intercept get_state requests with unsupported sub-paths at the jussi -// layer and emulate the expected get_state response by: +// Category 1: Intercept get_state with unsupported user sub-paths at the +// jussi layer and emulate the expected response by: // 1. Calling get_state("/@username") for base account data // 2. Calling get_account_history for transfers/author/curation rewards // 3. Calling get_vesting_delegations for delegation data // 4. Assembling a response in the same format get_state returns // +// Category 2: For /~witnesses and /proposals, fetch the base state via +// get_state("/") to obtain feed_price, props, etc., and return a minimal +// response. The actual witness/proposal data is loaded client-side via +// separate API calls (get_witnesses_by_vote, list_proposals). +// // All sub-requests are cached in Redis with fine-grained keys to maximize // reuse across different sub-paths for the same user. // // Cache key design (for maximum reuse): // // {prefix}gs:base:{username} — get_state("/@user") base account data +// {prefix}gs:base:__root__ — get_state("/") root state (feed_price, props) // {prefix}gs:hist:{username} — get_account_history full result // {prefix}gs:deleg_out:{username} — get_vesting_delegations (outgoing) // {prefix}gs:deleg_in:{username} — list_vesting_delegations (incoming) @@ -62,14 +74,15 @@ import ( // only differing in the client-side filter applied to the cached data. // // Removal: -// DELETE THIS FILE once all clients have migrated to the new wallet or -// to direct API calls. Track usage via the "workaround_success" metric -// in Prometheus and remove when request count drops to zero. +// DELETE THIS FILE once all clients have migrated to the condenser rewrite +// and wallet rewrite (both of which do not use get_state at all). +// Track usage via the "workaround_success" metric in Prometheus and remove +// when request count drops to zero. // Also remove the corresponding intercept block in processor.go // (search for "TEMPORARY WORKAROUND" comment). // -// Added: 2026-05-25 -// Related: beta-wallet 504 investigation, steemd get_state limitations +// Added: 2026-05-25 (Category 1), 2026-05-28 (Category 2) +// Related: wallet 504 investigation, steemd get_state limitations // ============================================================================ // Configuration constants @@ -117,6 +130,14 @@ var getStateSubPathRegex = regexp.MustCompile( `^/?@([^/\s]+)/(transfers|author-rewards|curation-rewards|delegations)$`, ) +// getStateSpecialPathRegex matches get_state paths that steemd's get_state +// does NOT handle at all (returns -32000 Server error). +// These are special pages like witnesses list and proposals. +// Matches: "/~witnesses", "~witnesses", "/proposals", "proposals" +var getStateSpecialPathRegex = regexp.MustCompile( + `^/?(?:~witnesses|proposals)$`, +) + // isGetStateUnsupportedSubPath checks if a request is condenser_api.get_state // with a sub-path that steemd does NOT handle natively. // Returns (username, subPath, true) when interception is needed. @@ -157,6 +178,108 @@ func isGetStateUnsupportedSubPath(jsonrpcReq *request.JSONRPCRequest) (string, s return matches[1], matches[2], true } +// isGetStateSpecialPath checks if a request is condenser_api.get_state +// with a path that steemd's get_state returns -32000 for (not implemented). +// Returns (pathType, true) when interception is needed. +// pathType is "witnesses" or "proposals". +func isGetStateSpecialPath(jsonrpcReq *request.JSONRPCRequest) (string, bool) { + if jsonrpcReq.URN.API != "condenser_api" || jsonrpcReq.URN.Method != "get_state" { + return "", false + } + + params, ok := jsonrpcReq.Params.([]interface{}) + if !ok { + return "", false + } + + var path string + switch len(params) { + case 1: + path, ok = params[0].(string) + case 3: + var args []interface{} + if args, ok = params[2].([]interface{}); ok && len(args) >= 1 { + path, ok = args[0].(string) + } + } + if !ok || path == "" { + return "", false + } + + if getStateSpecialPathRegex.MatchString(path) { + if path == "/~witnesses" || path == "~witnesses" { + return "witnesses", true + } + return "proposals", true + } + return "", false +} + +// emulateGetStateSpecialPath constructs a synthetic get_state response for +// paths that steemd's get_state does not implement (~witnesses, proposals). +// +// It fetches the base state via get_state("/") to obtain feed_price, props, +// etc., then adds a minimal structure for the requested page. +// +// The wallet SSR mainly needs feed_price and props to render the page shell. +// The actual witness/proposal data is fetched client-side via direct API calls +// (get_witnesses_by_vote, list_proposals), so we don't need to include it here. +func (p *RequestProcessor) emulateGetStateSpecialPath( + ctx context.Context, + jsonrpcReq *request.JSONRPCRequest, + pathType string, +) (map[string]interface{}, error) { + upstreamURL, err := p.getSteemdUpstreamURL() + if err != nil { + return nil, fmt.Errorf("get_state special path workaround: %w", err) + } + + // Fetch base state using get_state("/") for feed_price, props, etc. + baseResp, err := p.fetchCachedOrCall(ctx, upstreamURL, + cacheKeyPrefix+"gs:base:__root__", + "condenser_api.get_state", + []interface{}{"/"}, + jsonrpcReq, + ) + if err != nil { + return nil, fmt.Errorf("get_state special path workaround: base state failed: %w", err) + } + + if _, hasErr := baseResp["error"]; hasErr { + baseResp["id"] = jsonrpcReq.ID + return baseResp, nil + } + + result, ok := baseResp["result"].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("get_state special path workaround: unexpected base response format") + } + + // Build minimal response structure for the special page. + // Wallet SSR only needs the basic get_state envelope to render the page shell. + // The actual data (witnesses list, proposals list) is loaded via separate + // client-side API calls after the page loads. + result["current_route"] = "/" + pathType + if result["accounts"] == nil { + result["accounts"] = map[string]interface{}{} + } + if result["content"] == nil { + result["content"] = map[string]interface{}{} + } + if result["discussion_idx"] == nil { + result["discussion_idx"] = map[string]interface{}{} + } + if result["tag_idx"] == nil { + result["tag_idx"] = map[string]interface{}{} + } + if result["tags"] == nil { + result["tags"] = map[string]interface{}{} + } + + baseResp["id"] = jsonrpcReq.ID + return baseResp, nil +} + // emulateGetStateSubPath constructs a synthetic get_state response for // unsupported sub-paths by calling the steemd upstream directly, // with Redis caching at the sub-request level. diff --git a/internal/handlers/processor.go b/internal/handlers/processor.go index 4dba8ba..d6f2a29 100644 --- a/internal/handlers/processor.go +++ b/internal/handlers/processor.go @@ -111,19 +111,18 @@ func (p *RequestProcessor) ProcessSingleRequest(ctx context.Context, jsonrpcReq // Translate API calls to condenser_api when the upstream requires appbase format. translateToAppbase(jsonrpcReq, p.router) - // TEMPORARY WORKAROUND: Intercept get_state with unsupported sub-paths - // (transfers, author-rewards, curation-rewards, delegations) that steemd - // does not handle. See get_state_workaround.go for full documentation. - // TODO: Remove this block once all clients migrate to new wallet. - slog.Info("DEBUG: checking workaround", - "api", jsonrpcReq.URN.API, - "method", jsonrpcReq.URN.Method, - "namespace", jsonrpcReq.URN.Namespace, - "params_type", fmt.Sprintf("%T", jsonrpcReq.Params), - "params", fmt.Sprintf("%v", jsonrpcReq.Params), - ) + // TEMPORARY WORKAROUND: Intercept get_state with unsupported paths. + // There are two categories: + // 1. Sub-paths like /@user/transfers that steemd returns "Invalid parameters" for + // 2. Special paths like /~witnesses and /proposals that steemd returns -32000 for + // + // ALL of these workarounds will be removed once the condenser rewrite and + // wallet rewrite are deployed. The new versions do not use get_state at all. + // See get_state_workaround.go for full documentation. + // TODO: Remove this entire block after condenser rewrite + wallet rewrite launch. if username, subPath, ok := isGetStateUnsupportedSubPath(jsonrpcReq); ok { - slog.Info("DEBUG: workaround triggered", "username", username, "subPath", subPath) + slog.Debug("get_state workaround: sub-path intercepted", + "username", username, "subPath", subPath) span.SetAttributes(attribute.String("jussi.workaround", "get_state_subpath")) span.SetAttributes(attribute.String("jussi.workaround.subpath", subPath)) span.SetAttributes(attribute.String("jussi.workaround.username", username)) @@ -138,6 +137,22 @@ func (p *RequestProcessor) ProcessSingleRequest(ctx context.Context, jsonrpcReq return resp, nil } + if pathType, ok := isGetStateSpecialPath(jsonrpcReq); ok { + slog.Debug("get_state workaround: special path intercepted", + "pathType", pathType) + span.SetAttributes(attribute.String("jussi.workaround", "get_state_special_path")) + span.SetAttributes(attribute.String("jussi.workaround.path_type", pathType)) + + resp, err := p.emulateGetStateSpecialPath(ctx, jsonrpcReq, pathType) + if err != nil { + telemetry.RecordSpanError(span, err) + RequestsTotal.WithLabelValues(jsonrpcReq.URN.Namespace, jsonrpcReq.URN.Method, "workaround_error").Inc() + return nil, fmt.Errorf("get_state special path workaround failed: %w", err) + } + RequestsTotal.WithLabelValues(jsonrpcReq.URN.Namespace, jsonrpcReq.URN.Method, "workaround_success").Inc() + return resp, nil + } + // Add span attributes telemetry.AddSpanAttributes(span, map[string]string{ "jussi.namespace": jsonrpcReq.URN.Namespace,