Adding checks for ok status code in responses (#2178)

* fix: checks for ok status code in responses

* refactor: replacing native fetch with useSWR

* chore: remove url from logs

* chore: add phoenixd error logs for non-success responses

* chore: add use currencies hook

* chore: use loading from currencies hook and filter out btc

---------

Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
This commit is contained in:
Sergey B. 2026-03-31 20:51:26 +03:00 committed by GitHub
parent 895edabba7
commit f2ea668df8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 205 additions and 67 deletions

View file

@ -1273,6 +1273,15 @@ 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,
"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 {

View file

@ -46,6 +46,15 @@ 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,
"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 {

View file

@ -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),

View file

@ -25,3 +25,5 @@ export const DEFAULT_APP_BUDGET_RENEWAL = "monthly";
export const BITCOIN_DISPLAY_FORMAT_BIP177 = "bip177";
export const BITCOIN_DISPLAY_FORMAT_SATS = "sats";
export const RATES_API_URL = "https://getalby.com/api/rates";

View file

@ -0,0 +1,58 @@
import React from "react";
import useSWR from "swr";
import { RATES_API_URL } from "src/constants";
import { handleRequestError } from "src/utils/handleRequestError";
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<
Record<string, { name: string; priority: number }>
>;
});
export function useCurrencies(includeSats = false) {
const { data: ratesData, isLoading } = useSWR<
Record<string, { name: string; priority: number }>
>(RATES_API_URL, albyRatesFetcher, {
onError: (error) => handleRequestError("Failed to fetch currencies", error),
});
const currencies = React.useMemo(() => {
if (!ratesData) {
return [];
}
if (includeSats) {
return [
["SATS", "sats"],
...Object.entries(ratesData)
.filter(([code]) => code !== "BTC")
.sort((a, b) => {
const priorityDiff = a[1].priority - b[1].priority;
return priorityDiff !== 0 ? priorityDiff : a[0].localeCompare(b[0]);
})
.map(([code, details]): [string, string] => [
code.toUpperCase(),
details.name,
]),
];
}
return Object.entries(ratesData)
.filter(([code]) => code !== "BTC")
.map(([code, details]): [string, string] => [
code.toUpperCase(),
details.name,
])
.sort((a, b) => a[1].localeCompare(b[1]));
}, [ratesData, includeSats]);
return {
currencies,
isLoading,
};
}

View file

@ -7,6 +7,7 @@ import {
CardTitle,
} from "src/components/ui/card";
import { useApps } from "src/hooks/useApps";
import { useCurrencies } from "src/hooks/useCurrencies";
import { createApp } from "src/requests/createApp";
import { CreateAppRequest, UpdateAppRequest } from "src/types";
import { handleRequestError } from "src/utils/handleRequestError";
@ -114,39 +115,13 @@ export function ZapPlanner() {
const [frequencyValue, setFrequencyValue] = React.useState("1");
const [frequencyUnit, setFrequencyUnit] = React.useState("months");
const [currency, setCurrency] = React.useState<string>("USD");
const [currencies, setCurrencies] = React.useState<string[]>([]);
const { currencies, isLoading: isCurrenciesLoading } = useCurrencies(true);
const [convertedAmount, setConvertedAmount] = React.useState<string>("");
const [satoshiAmount, setSatoshiAmount] = React.useState<number | undefined>(
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: Record<string, { name: string; priority: number }> =
await res.json();
const fiatCodes = Object.keys(data)
// drop "BTC" - ZapPlanner uses SATS for the bitcoin currency
.filter((code) => code !== "BTC")
.sort((a, b) => {
const priorityDiff = data[a].priority - data[b].priority;
if (priorityDiff !== 0) {
return priorityDiff;
}
return a.localeCompare(b);
})
.map((c) => c.toUpperCase());
setCurrencies(["SATS", ...fiatCodes]);
} catch (err) {
console.error("Failed to load currencies", err);
}
}
fetchCurrencies();
}, []);
React.useEffect(() => {
// reset form on close
if (!open) {
@ -164,6 +139,10 @@ export function ZapPlanner() {
}, [open]);
React.useEffect(() => {
if (isCurrenciesLoading) {
return;
}
// If amount is empty, clear conversion output
if (!amount) {
setConvertedAmount("");
@ -200,7 +179,7 @@ export function ZapPlanner() {
};
convertCurrency();
}, [amount, currency, open]);
}, [amount, currency, open, isCurrenciesLoading]);
const appStoreApp = appStoreApps.find((app) => app.id === "zapplanner");
if (!appStoreApp) {
@ -428,14 +407,24 @@ export function ZapPlanner() {
)}
</div>
<Select value={currency} onValueChange={setCurrency}>
<Select
value={currency}
onValueChange={setCurrency}
disabled={isCurrenciesLoading}
>
<SelectTrigger className="w-1/2">
<SelectValue />
<SelectValue
placeholder={
isCurrenciesLoading
? "Loading currencies..."
: "Select a currency"
}
/>
</SelectTrigger>
<SelectContent>
{currencies.map((code) => (
{currencies.map(([code]) => (
<SelectItem key={code} value={code}>
{code === "BTC" ? "BTC (sats)" : code}
{code}
</SelectItem>
))}
</SelectContent>

View file

@ -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";
@ -23,6 +22,7 @@ import {
BITCOIN_DISPLAY_FORMAT_SATS,
} from "src/constants";
import { useAlbyMe } from "src/hooks/useAlbyMe";
import { useCurrencies } from "src/hooks/useCurrencies";
import { useInfo } from "src/hooks/useInfo";
import { cn } from "src/lib/utils";
import { handleRequestError } from "src/utils/handleRequestError";
@ -31,33 +31,10 @@ import { request } from "src/utils/request";
function Settings() {
const { data: albyMe } = useAlbyMe();
const { theme, darkMode, setTheme, setDarkMode } = useTheme();
const [fiatCurrencies, setFiatCurrencies] = useState<[string, string][]>([]);
const { currencies, isLoading: isCurrenciesLoading } = useCurrencies();
const { data: info, mutate: reloadInfo } = useInfo();
useEffect(() => {
async function fetchCurrencies() {
try {
const response = await fetch(`https://getalby.com/api/rates`);
const data: Record<string, { name: string }> = 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);
}
}
fetchCurrencies();
}, []);
async function updateSettings(
payload: Record<string, string | boolean>,
successMessage: string,
@ -206,12 +183,22 @@ function Settings() {
</div>
<div className="grid gap-1.5">
<Label htmlFor="currency">Fiat Currency</Label>
<Select value={info?.currency} onValueChange={updateCurrency}>
<Select
value={info?.currency}
onValueChange={updateCurrency}
disabled={isCurrenciesLoading}
>
<SelectTrigger className="w-full md:w-60">
<SelectValue placeholder="Select a currency" />
<SelectValue
placeholder={
isCurrenciesLoading
? "Loading currencies..."
: "Select a currency"
}
/>
</SelectTrigger>
<SelectContent>
{fiatCurrencies.map(([code, name]) => (
{currencies.map(([code, name]) => (
<SelectItem key={code} value={code}>
{name} ({code})
</SelectItem>

View file

@ -5,6 +5,8 @@ import (
b64 "encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
@ -105,8 +107,20 @@ 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 {
logger.Logger.WithFields(logrus.Fields{
"body": string(body),
"status_code": resp.StatusCode,
}).Error("phoenixd get balance endpoint returned non-success code")
return nil, fmt.Errorf("phoenixd get balance endpoint returned non-success code: %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 +159,20 @@ 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 {
logger.Logger.WithFields(logrus.Fields{
"body": string(body),
"status_code": resp.StatusCode,
}).Error("phoenixd get info endpoint returned non-success code")
return nil, fmt.Errorf("phoenixd get info endpoint returned non-success code: %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 +225,20 @@ 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 {
logger.Logger.WithFields(logrus.Fields{
"body": string(body),
"status_code": resp.StatusCode,
}).Error("phoenixd create invoice endpoint returned non-success code")
return nil, fmt.Errorf("phoenixd create invoice endpoint returned non-success code: %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 +276,20 @@ 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 {
logger.Logger.WithFields(logrus.Fields{
"body": string(body),
"status_code": resp.StatusCode,
}).Error("phoenixd incoming payments endpoint returned non-success code")
return nil, fmt.Errorf("phoenixd incoming payments endpoint returned non-success code: %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 +321,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 +370,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{

View file

@ -1415,6 +1415,15 @@ 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{
"endpoint": endpoint,
"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: %s", string(body))
}
jsonErr := json.Unmarshal(body, &result)
if jsonErr != nil {
logger.Logger.WithError(jsonErr).WithFields(logrus.Fields{
@ -1560,6 +1569,15 @@ func (svc *swapsService) getNextUnusedAddressFromXpub() (string, error) {
return nil, err
}
if res.StatusCode != http.StatusOK {
logger.Logger.WithFields(logrus.Fields{
"endpoint": endpoint,
"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: %s", string(body))
}
var jsonContent interface{}
err = json.Unmarshal(body, &jsonContent)
if err != nil {