Skip to content

Commit 65a8c9d

Browse files
committed
Merge branch 'dev' into 'main'
fix(exchange): distinguish exchange outage from real errors See merge request AndreyPopov/spot-trading-bot!44
2 parents 5f64383 + 46eae69 commit 65a8c9d

8 files changed

Lines changed: 94 additions & 15 deletions

File tree

src/app.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,10 +159,11 @@ app.use(function (err, req, res, next) {
159159
// set locals, only providing error in development
160160
res.locals.message = err.message;
161161
res.locals.error = req.app.get('env') === 'development' ? err : {};
162+
res.locals.retryable = Boolean(err.retryable);
162163

163164
// render the error page
164165
res.status(err.status || 500);
165-
res.render('error');
166+
res.render('error', { title: 'Error' });
166167
});
167168

168169
module.exports = app;

src/lib/checkKeys.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ async function checkKeys(env) {
4444
if (!key || !secret) return { success: false, message: 'Keys are not set' };
4545

4646
try {
47-
const client = new Spot(key, secret, { baseURL: cfg.baseURL });
47+
const client = new Spot(key, secret, { baseURL: cfg.baseURL, timeout: 5000 });
4848
const res = await client.account({ omitZeroBalances: true });
4949
return {
5050
success: true,
@@ -53,7 +53,20 @@ async function checkKeys(env) {
5353
};
5454
} catch (err) {
5555
const data = err.response?.data;
56-
const message = [data?.code, data?.msg || err.message].filter(Boolean).join(' ');
56+
// A rejected key comes back from Binance as a parsed JSON body {code, msg}.
57+
// Anything else — no response at all (DNS failure, refused connection, our
58+
// own timeout), or a response that isn't that shape (503/502/HTML "under
59+
// maintenance" page) — means the request never reached the account check,
60+
// so it must not be reported as an invalid key.
61+
if (!data || typeof data !== 'object' || data.code === undefined) {
62+
// The body is a gateway/maintenance page (HTML, plain text), not something
63+
// worth showing raw — the status code already says everything useful.
64+
const reason = err.response
65+
? `HTTP ${err.response.status} ${err.response.statusText || ''}`.trim()
66+
: err.code || err.message || 'unknown error';
67+
return { success: false, offline: true, message: `Exchange unreachable (${reason})` };
68+
}
69+
const message = [data.code, data.msg || err.message].filter(Boolean).join(' ');
5770
return { success: false, message: message || 'Request failed' };
5871
}
5972
}

src/lib/invokeAPI.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ class InvokeApi {
341341
const message = this.#getCatchMsg(err);
342342

343343
this.getConsoleMsg(message, false);
344-
return { success: false, message };
344+
return { success: false, message, unavailable: this.#isUnavailable(err) };
345345
}
346346
}
347347

@@ -427,6 +427,16 @@ class InvokeApi {
427427

428428
return [err.message, data?.code, data?.msg || data?.message].filter(Boolean).join(' | ');
429429
}
430+
431+
// A rejected request comes back from Binance as a parsed JSON body {code, msg}.
432+
// Anything else — no response (DNS failure, connection refused, our own
433+
// timeout), or a gateway/maintenance page (502/503, HTML, plain text) —
434+
// means the exchange itself is unreachable, not that the request was
435+
// rejected. The two must read differently to whoever sees the message.
436+
#isUnavailable(err) {
437+
const data = err.response?.data;
438+
return !data || typeof data !== 'object' || data.code === undefined;
439+
}
430440
}
431441

432442
module.exports = { InvokeApi };

src/public/javascripts/indexMain.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ document.querySelectorAll('[data-check]').forEach((button) => {
116116
const status = button.closest('.api-keys__group').querySelector('.api-keys__status');
117117

118118
button.disabled = true;
119-
button.classList.remove('success', 'danger');
119+
button.classList.remove('success', 'danger', 'warning');
120120
if (status) status.textContent = 'checking…';
121121

122122
try {
@@ -127,11 +127,13 @@ document.querySelectorAll('[data-check]').forEach((button) => {
127127
});
128128
const data = await res.json();
129129

130-
button.classList.add(data.success ? 'success' : 'danger');
130+
button.classList.add(data.success ? 'success' : data.offline ? 'warning' : 'danger');
131131
if (status) {
132132
status.textContent = data.success
133133
? `✅ valid${data.canTrade === false ? ' (canTrade: no)' : ''}`
134-
: `❌ ${data.message || 'invalid'}`;
134+
: data.offline
135+
? `⚠️ ${data.message}`
136+
: `❌ ${data.message || 'invalid'}`;
135137
}
136138
} catch (err) {
137139
button.classList.add('danger');

src/public/stylesheets/style.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/routes/spotbot.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,18 @@ router.get('/:currency', async function (req, res, next) {
2727
const exchangeInfo = await API.exchangeInfo({ symbol: currency });
2828

2929
if (!exchangeInfo.success) {
30+
// Distinguish the exchange being down (maintenance, gateway errors — retry
31+
// in a few minutes and it's fine) from a real problem the user needs to
32+
// act on. Otherwise both look like the identical raw stack trace, and the
33+
// temporary case reads as the app being broken.
34+
if (exchangeInfo.unavailable) {
35+
const err = new Error(
36+
'The exchange is temporarily unavailable (maintenance or network issue). Please retry in a few minutes.'
37+
);
38+
err.status = 503;
39+
err.retryable = true;
40+
return next(err);
41+
}
3042
const err = new Error(`Binance API error: ${exchangeInfo.message}`);
3143
err.status = 502;
3244
return next(err);

src/scss/_button.scss

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,25 @@
5656
}
5757
}
5858

59+
&.warning {
60+
background-color: $c-w-700;
61+
border-color: $c-w-500;
62+
color: $c-w-50;
63+
64+
&:hover {
65+
background-color: $c-w-300;
66+
color: $c-w-950;
67+
border-color: $c-w-700;
68+
}
69+
70+
&.disabled {
71+
background-color: $c-w-100;
72+
border-color: $c-w-50;
73+
color: $c-w-300;
74+
cursor: not-allowed;
75+
}
76+
}
77+
5978
&.sm {
6079
font-size: $font-size-base - 0.1;
6180
padding: 0.125rem 0.25rem;

src/views/error.ejs

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,29 @@
1-
<h1>
2-
<%= message %>
3-
</h1>
4-
<h2>
5-
<%= typeof error !=='undefined' ? error.status : '' %>
6-
</h2>
7-
<pre><%= typeof error !== 'undefined' ? error.stack : '' %></pre>
1+
<!DOCTYPE html>
2+
<html data-theme="">
3+
<%- include('../views/head'); %>
4+
5+
<body>
6+
<div class="wrapper">
7+
<header class="">
8+
<%- include('../views/navbar'); %>
9+
</header>
10+
<main class="container-adaptive content">
11+
<section class="error-page">
12+
<% if (typeof retryable !=='undefined' && retryable) { %>
13+
<h1>Exchange temporarily unavailable</h1>
14+
<p><%= message %></p>
15+
<% } else { %>
16+
<h1>Something went wrong</h1>
17+
<p><%= message %></p>
18+
<% } %>
19+
<p><a href="/">Back to main page</a></p>
20+
<% if (!(typeof retryable !=='undefined' && retryable) && typeof error !=='undefined' && error.stack) { %>
21+
<pre><%= error.stack %></pre>
22+
<% } %>
23+
</section>
24+
</main>
25+
</div>
26+
<%- include('../views/footer'); %>
27+
</body>
28+
29+
</html>

0 commit comments

Comments
 (0)