From 6df2ce1f84bc4b636e7972a5698ff50ad0258a40 Mon Sep 17 00:00:00 2001 From: "Sergey B." Date: Wed, 25 Mar 2026 12:30:23 +0300 Subject: [PATCH 1/2] fix: checks for ok status code in responses --- api/api.go | 10 +++ api/esplora.go | 10 +++ api/rebalance.go | 2 +- .../src/screens/internal-apps/ZapPlanner.tsx | 3 + frontend/src/screens/settings/Settings.tsx | 3 + lnclient/phoenixd/phoenixd.go | 62 +++++++++++++++++-- swaps/swaps_service.go | 30 +++++++++ 7 files changed, 113 insertions(+), 7 deletions(-) diff --git a/api/api.go b/api/api.go index 47ca130bb..1494be9c3 100644 --- a/api/api.go +++ b/api/api.go @@ -1244,6 +1244,16 @@ func (api *api) RequestMempoolApi(ctx context.Context, endpoint string) (interfa return nil, errors.New("failed to read response body") } + if res.StatusCode != http.StatusOK { + logger.Logger.WithFields(logrus.Fields{ + "endpoint": endpoint, + "url": url, + "status_code": res.StatusCode, + "body": string(body), + }).Error("Mempool endpoint returned non-success code") + return nil, fmt.Errorf("mempool endpoint returned non-success code: %s", string(body)) + } + var jsonContent interface{} jsonErr := json.Unmarshal(body, &jsonContent) if jsonErr != nil { diff --git a/api/esplora.go b/api/esplora.go index 791aef4a1..6e8a07125 100644 --- a/api/esplora.go +++ b/api/esplora.go @@ -46,6 +46,16 @@ func (api *api) RequestEsploraApi(ctx context.Context, endpoint string) (interfa return nil, errors.New("failed to read response body") } + if res.StatusCode != http.StatusOK { + logger.Logger.WithFields(logrus.Fields{ + "endpoint": endpoint, + "url": url, + "status_code": res.StatusCode, + "body": string(body), + }).Error("Esplora endpoint returned non-success code") + return nil, fmt.Errorf("esplora endpoint returned non-success code: %s", string(body)) + } + var jsonContent interface{} jsonErr := json.Unmarshal(body, &jsonContent) if jsonErr != nil { diff --git a/api/rebalance.go b/api/rebalance.go index 44b1c8b31..7adbf0832 100644 --- a/api/rebalance.go +++ b/api/rebalance.go @@ -84,7 +84,7 @@ func (api *api) RebalanceChannel(ctx context.Context, rebalanceChannelRequest *R return nil, errors.New("failed to read response body") } - if res.StatusCode >= 300 { + if res.StatusCode != http.StatusOK { logger.Logger.WithFields(logrus.Fields{ "request": newRspCreateOrderRequest, "body": string(body), diff --git a/frontend/src/screens/internal-apps/ZapPlanner.tsx b/frontend/src/screens/internal-apps/ZapPlanner.tsx index 9058a6915..ed6dd442f 100644 --- a/frontend/src/screens/internal-apps/ZapPlanner.tsx +++ b/frontend/src/screens/internal-apps/ZapPlanner.tsx @@ -126,6 +126,9 @@ export function ZapPlanner() { async function fetchCurrencies() { try { const res = await fetch("https://getalby.com/api/rates"); + if (!res.ok) { + throw new Error(`Failed to load currencies: ${res.status}`); + } const data: Record = await res.json(); const fiatCodes = Object.keys(data) diff --git a/frontend/src/screens/settings/Settings.tsx b/frontend/src/screens/settings/Settings.tsx index e9394ebef..bd4970602 100644 --- a/frontend/src/screens/settings/Settings.tsx +++ b/frontend/src/screens/settings/Settings.tsx @@ -40,6 +40,9 @@ function Settings() { async function fetchCurrencies() { try { const response = await fetch(`https://getalby.com/api/rates`); + if (!response.ok) { + throw new Error(`Failed to fetch currencies: ${response.status}`); + } const data: Record = await response.json(); const mappedCurrencies: [string, string][] = Object.entries(data).map( diff --git a/lnclient/phoenixd/phoenixd.go b/lnclient/phoenixd/phoenixd.go index 812cbf323..941cc8da4 100644 --- a/lnclient/phoenixd/phoenixd.go +++ b/lnclient/phoenixd/phoenixd.go @@ -5,6 +5,8 @@ import ( b64 "encoding/base64" "encoding/json" "errors" + "fmt" + "io" "net/http" "net/url" "strconv" @@ -105,8 +107,16 @@ func (svc *PhoenixService) GetBalances(ctx context.Context, includeInactiveChann } defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("phoenixd /getbalance returned non-success status: %d %s", resp.StatusCode, string(body)) + } + var balanceRes BalanceResponse - if err := json.NewDecoder(resp.Body).Decode(&balanceRes); err != nil { + if err := json.Unmarshal(body, &balanceRes); err != nil { return nil, err } @@ -145,8 +155,16 @@ func fetchNodeInfo(ctx context.Context, svc *PhoenixService) (info *lnclient.Nod } defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("phoenixd /getinfo returned non-success status: %d %s", resp.StatusCode, string(body)) + } + var infoRes InfoResponse - if err := json.NewDecoder(resp.Body).Decode(&infoRes); err != nil { + if err := json.Unmarshal(body, &infoRes); err != nil { return nil, err } return &lnclient.NodeInfo{ @@ -199,8 +217,16 @@ func (svc *PhoenixService) MakeInvoice(ctx context.Context, amount int64, descri } defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("phoenixd /createinvoice returned non-success status: %d %s", resp.StatusCode, string(body)) + } + var invoiceRes MakeInvoiceResponse - if err := json.NewDecoder(resp.Body).Decode(&invoiceRes); err != nil { + if err := json.Unmarshal(body, &invoiceRes); err != nil { return nil, err } @@ -238,8 +264,16 @@ func (svc *PhoenixService) LookupInvoice(ctx context.Context, paymentHash string } defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("phoenixd /payments/incoming returned non-success status: %d %s", resp.StatusCode, string(body)) + } + var invoiceRes InvoiceResponse - if err := json.NewDecoder(resp.Body).Decode(&invoiceRes); err != nil { + if err := json.Unmarshal(body, &invoiceRes); err != nil { return nil, err } @@ -271,8 +305,16 @@ func (svc *PhoenixService) SendPaymentSync(payReq string, amount *uint64) (*lncl } defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("phoenixd /payinvoice returned non-success status: %d %s", resp.StatusCode, string(body)) + } + var payRes PayResponse - if err := json.NewDecoder(resp.Body).Decode(&payRes); err != nil { + if err := json.Unmarshal(body, &payRes); err != nil { return nil, err } @@ -312,8 +354,16 @@ func (svc *PhoenixService) GetNodeConnectionInfo(ctx context.Context) (nodeConne } defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("phoenixd /getinfo returned non-success status: %d %s", resp.StatusCode, string(body)) + } + var infoRes InfoResponse - if err := json.NewDecoder(resp.Body).Decode(&infoRes); err != nil { + if err := json.Unmarshal(body, &infoRes); err != nil { return nil, err } return &lnclient.NodeConnectionInfo{ diff --git a/swaps/swaps_service.go b/swaps/swaps_service.go index aa708ad28..6cb8f5591 100644 --- a/swaps/swaps_service.go +++ b/swaps/swaps_service.go @@ -1393,6 +1393,21 @@ func (svc *swapsService) doMempoolRequest(endpoint string, result interface{}) e return errors.New("failed to read response body") } + if res.StatusCode != http.StatusOK { + logger.Logger.WithFields(logrus.Fields{ + "component": "swaps", + "endpoint": endpoint, + "url": url, + "status_code": res.StatusCode, + "body": string(body), + }).Error("Swaps mempool API endpoint returned non-success code") + return fmt.Errorf( + "swaps mempool API endpoint returned non-success code for %s: %s", + endpoint, + string(body), + ) + } + jsonErr := json.Unmarshal(body, &result) if jsonErr != nil { logger.Logger.WithError(jsonErr).WithFields(logrus.Fields{ @@ -1530,6 +1545,21 @@ func (svc *swapsService) getNextUnusedAddressFromXpub() (string, error) { return nil, err } + if res.StatusCode != http.StatusOK { + logger.Logger.WithFields(logrus.Fields{ + "component": "swaps", + "endpoint": endpoint, + "url": url, + "status_code": res.StatusCode, + "body": string(body), + }).Error("Swaps esplora endpoint returned non-success code") + return nil, fmt.Errorf( + "swaps esplora endpoint returned non-success code for %s: %s", + endpoint, + string(body), + ) + } + var jsonContent interface{} err = json.Unmarshal(body, &jsonContent) if err != nil { From 4792c9e620bf6fb3ee1c785663fccbdedbc03d37 Mon Sep 17 00:00:00 2001 From: "Sergey B." Date: Wed, 25 Mar 2026 16:15:01 +0300 Subject: [PATCH 2/2] refactor: replacing native fetch with useSWR --- .../src/screens/internal-apps/ZapPlanner.tsx | 54 +++++++++---------- frontend/src/screens/settings/Settings.tsx | 53 +++++++++--------- 2 files changed, 52 insertions(+), 55 deletions(-) diff --git a/frontend/src/screens/internal-apps/ZapPlanner.tsx b/frontend/src/screens/internal-apps/ZapPlanner.tsx index ed6dd442f..20697c17f 100644 --- a/frontend/src/screens/internal-apps/ZapPlanner.tsx +++ b/frontend/src/screens/internal-apps/ZapPlanner.tsx @@ -1,4 +1,5 @@ import React from "react"; +import useSWR from "swr"; import AppCard from "src/components/connections/AppCard"; import { Card, @@ -114,41 +115,38 @@ export function ZapPlanner() { const [frequencyValue, setFrequencyValue] = React.useState("1"); const [frequencyUnit, setFrequencyUnit] = React.useState("months"); const [currency, setCurrency] = React.useState("USD"); - const [currencies, setCurrencies] = React.useState([]); - const [convertedAmount, setConvertedAmount] = React.useState(""); - const [satoshiAmount, setSatoshiAmount] = React.useState( - undefined - ); - - React.useEffect(() => { - // fetch the fiat list and prepend sats/BTC - async function fetchCurrencies() { - try { - const res = await fetch("https://getalby.com/api/rates"); + const { data: ratesData } = useSWR( + "https://getalby.com/api/rates", + (url: string) => + fetch(url).then((res) => { if (!res.ok) { throw new Error(`Failed to load currencies: ${res.status}`); } - const data: Record = - await res.json(); - const fiatCodes = Object.keys(data) - // drop "BTC" - ZapPlanner uses SATS for the bitcoin currency + return res.json() as Promise< + Record + >; + }), + { onError: (err) => console.error("Failed to load currencies", err) } + ); + + const currencies = ratesData + ? [ + "SATS", + ...Object.keys(ratesData) .filter((code) => code !== "BTC") .sort((a, b) => { - const priorityDiff = data[a].priority - data[b].priority; - if (priorityDiff !== 0) { - return priorityDiff; - } - return a.localeCompare(b); + const priorityDiff = ratesData[a].priority - ratesData[b].priority; + return priorityDiff !== 0 ? priorityDiff : a.localeCompare(b); }) - .map((c) => c.toUpperCase()); - setCurrencies(["SATS", ...fiatCodes]); - } catch (err) { - console.error("Failed to load currencies", err); - } - } - fetchCurrencies(); - }, []); + .map((c) => c.toUpperCase()), + ] + : ["SATS"]; + + const [convertedAmount, setConvertedAmount] = React.useState(""); + const [satoshiAmount, setSatoshiAmount] = React.useState( + undefined + ); React.useEffect(() => { // reset form on close diff --git a/frontend/src/screens/settings/Settings.tsx b/frontend/src/screens/settings/Settings.tsx index bd4970602..d6a7625a2 100644 --- a/frontend/src/screens/settings/Settings.tsx +++ b/frontend/src/screens/settings/Settings.tsx @@ -1,5 +1,4 @@ import { StarsIcon } from "lucide-react"; -import { useEffect, useState } from "react"; import { toast } from "sonner"; import Loading from "src/components/Loading"; import SettingsHeader from "src/components/SettingsHeader"; @@ -27,39 +26,39 @@ import { useInfo } from "src/hooks/useInfo"; import { cn } from "src/lib/utils"; import { handleRequestError } from "src/utils/handleRequestError"; import { request } from "src/utils/request"; +import useSWR from "swr"; + +const albyRatesFetcher = (url: string) => + fetch(url).then((res) => { + if (!res.ok) { + throw new Error(`Failed to fetch currencies: ${res.status}`); + } + return res.json() as Promise>; + }); function Settings() { const { data: albyMe } = useAlbyMe(); const { theme, darkMode, setTheme, setDarkMode } = useTheme(); - const [fiatCurrencies, setFiatCurrencies] = useState<[string, string][]>([]); - - const { data: info, mutate: reloadInfo } = useInfo(); - - useEffect(() => { - async function fetchCurrencies() { - try { - const response = await fetch(`https://getalby.com/api/rates`); - if (!response.ok) { - throw new Error(`Failed to fetch currencies: ${response.status}`); - } - const data: Record = await response.json(); - - const mappedCurrencies: [string, string][] = Object.entries(data).map( - ([code, details]) => [code.toUpperCase(), details.name] - ); - - mappedCurrencies.sort((a, b) => a[1].localeCompare(b[1])); - - setFiatCurrencies(mappedCurrencies); - } catch (error) { - console.error(error); - handleRequestError("Failed to fetch currencies", error); - } + const { data: ratesData } = useSWR( + "https://getalby.com/api/rates", + albyRatesFetcher, + { + onError: (error) => + handleRequestError("Failed to fetch currencies", error), } + ); - fetchCurrencies(); - }, []); + const fiatCurrencies: [string, string][] = ratesData + ? Object.entries(ratesData) + .map(([code, details]): [string, string] => [ + code.toUpperCase(), + details.name, + ]) + .sort((a, b) => a[1].localeCompare(b[1])) + : []; + + const { data: info, mutate: reloadInfo } = useInfo(); async function updateSettings( payload: Record,