Fix/orca auth network glitch logout (#14110)

* fix(auth): don't log out of Orca cloud on transient token-refresh failures
This commit is contained in:
SoftFever
2026-06-09 02:34:25 +08:00
committed by GitHub
parent ff51abc69b
commit 445bcdfa00
2 changed files with 118 additions and 89 deletions

View File

@@ -1564,21 +1564,24 @@ bool OrcaCloudServiceAgent::decode_jwt_expiry(const std::string& token, std::chr
return false; return false;
} }
bool OrcaCloudServiceAgent::refresh_now(const std::string& refresh_token, const std::string& reason, bool async) RefreshResult OrcaCloudServiceAgent::refresh_now(const std::string& refresh_token, const std::string& reason, bool async)
{ {
if (refresh_token.empty()) return false; if (refresh_token.empty()) return RefreshResult::AuthRejected; // nothing to refresh
bool expected = false; bool expected = false;
if (!refresh_running.compare_exchange_strong(expected, true)) { if (!refresh_running.compare_exchange_strong(expected, true)) {
BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: refresh already running, skip (reason=" << reason << ")"; BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: refresh already running, skip (reason=" << reason << ")";
return false; // Another refresh is already in flight. Treat as transient so we keep the session
// instead of logging out: that in-flight refresh surfaces its own Success/AuthRejected,
// so a genuine rejection is only deferred to the next request, never lost.
return RefreshResult::Transient;
} }
auto worker = [this, refresh_token, reason]() { auto worker = [this, refresh_token, reason]() {
(void) reason; (void) reason;
bool ok = refresh_session_with_token(refresh_token); RefreshResult r = refresh_session_with_token(refresh_token);
refresh_running.store(false); refresh_running.store(false);
return ok; return r;
}; };
if (async) { if (async) {
@@ -1586,13 +1589,15 @@ bool OrcaCloudServiceAgent::refresh_now(const std::string& refresh_token, const
refresh_thread.join(); refresh_thread.join();
} }
refresh_thread = std::thread([worker]() { worker(); }); refresh_thread = std::thread([worker]() { worker(); });
return true; // Fire-and-forget: the outcome isn't known yet and no current caller consumes it.
// Return Transient (indeterminate) rather than implying a completed, successful refresh.
return RefreshResult::Transient;
} }
return worker(); return worker();
} }
bool OrcaCloudServiceAgent::refresh_from_storage(const std::string& reason, bool async) RefreshResult OrcaCloudServiceAgent::refresh_from_storage(const std::string& reason, bool async)
{ {
std::string refresh_token = get_refresh_token(); std::string refresh_token = get_refresh_token();
if (refresh_token.empty()) { if (refresh_token.empty()) {
@@ -1600,7 +1605,7 @@ bool OrcaCloudServiceAgent::refresh_from_storage(const std::string& reason, bool
} }
if (refresh_token.empty()) { if (refresh_token.empty()) {
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: no refresh token available for refresh (reason=" << reason << ")"; BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: no refresh token available for refresh (reason=" << reason << ")";
return false; return RefreshResult::AuthRejected; // no persisted token: nothing to preserve
} }
return refresh_now(refresh_token, reason, async); return refresh_now(refresh_token, reason, async);
@@ -1616,37 +1621,55 @@ bool OrcaCloudServiceAgent::refresh_if_expiring(std::chrono::seconds skew, const
if (!needs_refresh) return true; if (!needs_refresh) return true;
if (refresh_from_storage(reason, false)) return true; if (refresh_from_storage(reason, false) == RefreshResult::Success) return true;
std::this_thread::sleep_for(std::chrono::milliseconds(750)); std::this_thread::sleep_for(std::chrono::milliseconds(750));
return refresh_from_storage(reason + "_retry", false); return refresh_from_storage(reason + "_retry", false) == RefreshResult::Success;
} }
bool OrcaCloudServiceAgent::refresh_session_with_token(const std::string& refresh_token) // Maps a token-refresh HTTP outcome to a RefreshResult. http_code == 0 means the
// server could not be reached; session_established is only meaningful for a 2xx body.
static RefreshResult classify_refresh_result(unsigned http_code, bool session_established)
{
if (http_code == 0)
return RefreshResult::Transient; // no response: network/transport failure
if (http_code == 400 || http_code == 401 || http_code == 403)
return RefreshResult::AuthRejected; // refresh token rejected
if (http_code >= 400)
return RefreshResult::Transient; // rate-limit (429), server error (5xx) or other 4xx: keep the session
return session_established ? RefreshResult::Success // 2xx with a usable session
: RefreshResult::Transient; // 2xx but unusable body
}
RefreshResult OrcaCloudServiceAgent::refresh_session_with_token(const std::string& refresh_token)
{ {
std::string body = "{\"refresh_token\":\"" + refresh_token + "\"}"; std::string body = "{\"refresh_token\":\"" + refresh_token + "\"}";
std::string url = auth_base_url + auth_constants::TOKEN_PATH + "?grant_type=refresh_token"; std::string url = auth_base_url + auth_constants::TOKEN_PATH + "?grant_type=refresh_token";
std::string response; std::string response;
unsigned int http_code = 0; unsigned int http_code = 0;
if (!http_post_token(body, &response, &http_code, url) || http_code >= 400) { // http_post_token sets http_code to 0 when the server could not be reached.
http_post_token(body, &response, &http_code, url);
bool established = false;
if (http_code >= 200 && http_code < 300) {
if (session_handler) {
established = session_handler(response);
} else {
// No session handler set - parse the token response directly and establish the
// session, so OrcaCloudServiceAgent is self-contained without external setup.
try {
established = set_user_session(json::parse(response));
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: token refresh parse exception - " << e.what();
}
}
} else {
std::string truncated_response = response.size() > 200 ? response.substr(0, 200) + "..." : response; std::string truncated_response = response.size() > 200 ? response.substr(0, 200) + "..." : response;
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: token refresh failed - http_code=" << http_code BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: token refresh failed - http_code=" << http_code
<< ", response_body=" << truncated_response; << ", response_body=" << truncated_response;
return false;
} }
if (session_handler) { return classify_refresh_result(http_code, established);
return session_handler(response);
}
// No session handler set - parse the token response directly and establish session
// This makes OrcaCloudServiceAgent self-contained without requiring external setup
try {
return set_user_session(json::parse(response));
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: token refresh parse exception - " << e.what();
return false;
}
} }
// ============================================================================ // ============================================================================
@@ -1759,15 +1782,20 @@ void OrcaCloudServiceAgent::clear_session()
// HTTP Helpers // HTTP Helpers
// ============================================================================ // ============================================================================
bool OrcaCloudServiceAgent::attempt_refresh_after_unauthorized(const std::string& reason) RefreshResult OrcaCloudServiceAgent::attempt_refresh_after_unauthorized(const std::string& reason)
{ {
if (refresh_from_storage(reason, false)) return true; RefreshResult r = refresh_from_storage(reason, false);
if (r != RefreshResult::Transient) return r; // Success or AuthRejected: decided, no retry
// Only a transient (network/server) failure is worth retrying.
std::this_thread::sleep_for(std::chrono::milliseconds(500)); std::this_thread::sleep_for(std::chrono::milliseconds(500));
if (refresh_from_storage(reason + "_retry", false)) return true; r = refresh_from_storage(reason + "_retry", false);
BOOST_LOG_TRIVIAL(warning) << "[auth] event=refresh result=failure source=" << reason << " action=logout"; if (r == RefreshResult::Transient)
return false; BOOST_LOG_TRIVIAL(warning) << "[auth] event=refresh result=transient source=" << reason << " action=keep_session";
else if (r == RefreshResult::AuthRejected)
BOOST_LOG_TRIVIAL(warning) << "[auth] event=refresh result=rejected source=" << reason << " action=logout";
return r;
} }
std::map<std::string, std::string> OrcaCloudServiceAgent::data_headers() std::map<std::string, std::string> OrcaCloudServiceAgent::data_headers()
@@ -1780,6 +1808,24 @@ std::map<std::string, std::string> OrcaCloudServiceAgent::data_headers()
return headers; return headers;
} }
bool OrcaCloudServiceAgent::resolve_unauthorized(HttpResult& res,
const std::function<HttpResult()>& perform, const std::string& reason)
{
if (res.status != 401)
return false;
RefreshResult rr = attempt_refresh_after_unauthorized(reason);
if (rr == RefreshResult::Success) {
res = perform(); // refreshed: retry the original request with the new token
return false;
}
// Transient (no connection / 5xx / 429 / ambiguous): keep the session and token,
// suppress the auth error so the GUI does not log the user out.
// AuthRejected (refresh token genuinely rejected): let the 401 surface -> logout.
return rr == RefreshResult::Transient;
}
int OrcaCloudServiceAgent::http_get(const std::string& path, std::string* response_body, unsigned int* http_code) int OrcaCloudServiceAgent::http_get(const std::string& path, std::string* response_body, unsigned int* http_code)
{ {
std::string url = api_base_url + path; std::string url = api_base_url + path;
@@ -1788,12 +1834,6 @@ int OrcaCloudServiceAgent::http_get(const std::string& path, std::string* respon
if (!ensure_token_fresh("http_get_" + path)) if (!ensure_token_fresh("http_get_" + path))
BOOST_LOG_TRIVIAL(warning) << "ensure_token_fresh returned false"; BOOST_LOG_TRIVIAL(warning) << "ensure_token_fresh returned false";
struct HttpResult {
bool success{false};
unsigned int status{0};
std::string body;
};
auto perform = [&]() { auto perform = [&]() {
HttpResult result; HttpResult result;
try { try {
@@ -1832,20 +1872,16 @@ int OrcaCloudServiceAgent::http_get(const std::string& path, std::string* respon
}; };
HttpResult res = perform(); HttpResult res = perform();
bool suppress = resolve_unauthorized(res, perform, "http_get_" + path);
// Single retry on 401 - no recursion
if (res.status == 401 && attempt_refresh_after_unauthorized("http_get_" + path)) {
res = perform();
}
if (response_body) *response_body = res.body; if (response_body) *response_body = res.body;
if (http_code) *http_code = res.status; if (http_code) *http_code = res.status;
if (!res.success || res.status >= 400) { if (!suppress && (!res.success || res.status >= 400)) {
invoke_http_error_callback(res.status, res.body); invoke_http_error_callback(res.status, res.body);
} }
return res.success ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED; return (res.success && !suppress) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED;
} }
int OrcaCloudServiceAgent::http_post(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code) int OrcaCloudServiceAgent::http_post(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code)
@@ -1855,12 +1891,6 @@ int OrcaCloudServiceAgent::http_post(const std::string& path, const std::string&
ensure_token_fresh("http_post_" + path); ensure_token_fresh("http_post_" + path);
struct HttpResult {
bool success{false};
unsigned int status{0};
std::string body;
};
auto perform = [&]() { auto perform = [&]() {
HttpResult result; HttpResult result;
try { try {
@@ -1902,20 +1932,16 @@ int OrcaCloudServiceAgent::http_post(const std::string& path, const std::string&
}; };
HttpResult res = perform(); HttpResult res = perform();
bool suppress = resolve_unauthorized(res, perform, "http_post_" + path);
// Single retry on 401 - no recursion
if (res.status == 401 && attempt_refresh_after_unauthorized("http_post_" + path)) {
res = perform();
}
if (response_body) *response_body = res.body; if (response_body) *response_body = res.body;
if (http_code) *http_code = res.status; if (http_code) *http_code = res.status;
if (!res.success || res.status >= 400) { if (!suppress && (!res.success || res.status >= 400)) {
invoke_http_error_callback(res.status, res.body); invoke_http_error_callback(res.status, res.body);
} }
return res.success ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED; return (res.success && !suppress) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED;
} }
int OrcaCloudServiceAgent::http_put(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code) int OrcaCloudServiceAgent::http_put(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code)
@@ -1925,12 +1951,6 @@ int OrcaCloudServiceAgent::http_put(const std::string& path, const std::string&
ensure_token_fresh("http_put_" + path); ensure_token_fresh("http_put_" + path);
struct HttpResult {
bool success{false};
unsigned int status{0};
std::string body;
};
auto perform = [&]() { auto perform = [&]() {
HttpResult result; HttpResult result;
try { try {
@@ -1959,7 +1979,7 @@ int OrcaCloudServiceAgent::http_put(const std::string& path, const std::string&
.on_error([&](std::string resp_body, std::string error, unsigned resp_status) { .on_error([&](std::string resp_body, std::string error, unsigned resp_status) {
result.success = false; result.success = false;
result.status = resp_status == 0 ? 404 : resp_status; result.status = resp_status == 0 ? 404 : resp_status;
result.body = body; result.body = resp_body;
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: HTTP error - " << error; BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: HTTP error - " << error;
}) })
.timeout_max(30) .timeout_max(30)
@@ -1972,20 +1992,16 @@ int OrcaCloudServiceAgent::http_put(const std::string& path, const std::string&
}; };
HttpResult res = perform(); HttpResult res = perform();
bool suppress = resolve_unauthorized(res, perform, "http_put_" + path);
// Single retry on 401 - no recursion
if (res.status == 401 && attempt_refresh_after_unauthorized("http_put_" + path)) {
res = perform();
}
if (response_body) *response_body = res.body; if (response_body) *response_body = res.body;
if (http_code) *http_code = res.status; if (http_code) *http_code = res.status;
if (!res.success || res.status >= 400) { if (!suppress && (!res.success || res.status >= 400)) {
invoke_http_error_callback(res.status, res.body); invoke_http_error_callback(res.status, res.body);
} }
return res.success ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED; return (res.success && !suppress) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED;
} }
int OrcaCloudServiceAgent::http_delete(const std::string& path, std::string* response_body, unsigned int* http_code) int OrcaCloudServiceAgent::http_delete(const std::string& path, std::string* response_body, unsigned int* http_code)
@@ -1995,12 +2011,6 @@ int OrcaCloudServiceAgent::http_delete(const std::string& path, std::string* res
ensure_token_fresh("http_delete_" + path); ensure_token_fresh("http_delete_" + path);
struct HttpResult {
bool success{false};
unsigned int status{0};
std::string body;
};
auto perform = [&]() { auto perform = [&]() {
HttpResult result; HttpResult result;
try { try {
@@ -2039,20 +2049,16 @@ int OrcaCloudServiceAgent::http_delete(const std::string& path, std::string* res
}; };
HttpResult res = perform(); HttpResult res = perform();
bool suppress = resolve_unauthorized(res, perform, "http_delete_" + path);
// Single retry on 401 - no recursion
if (res.status == 401 && attempt_refresh_after_unauthorized("http_delete_" + path)) {
res = perform();
}
if (response_body) *response_body = res.body; if (response_body) *response_body = res.body;
if (http_code) *http_code = res.status; if (http_code) *http_code = res.status;
if (!res.success || res.status >= 400) { if (!suppress && (!res.success || res.status >= 400)) {
invoke_http_error_callback(res.status, res.body); invoke_http_error_callback(res.status, res.body);
} }
return res.success ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED; return (res.success && !suppress) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED;
} }
bool OrcaCloudServiceAgent::http_post_token(const std::string& body, std::string* response_body, unsigned int* http_code, const std::string& custom_url) bool OrcaCloudServiceAgent::http_post_token(const std::string& body, std::string* response_body, unsigned int* http_code, const std::string& custom_url)
@@ -2108,7 +2114,7 @@ bool OrcaCloudServiceAgent::http_post_token(const std::string& body, std::string
}) })
.on_error([&](std::string body, std::string error, unsigned resp_status) { .on_error([&](std::string body, std::string error, unsigned resp_status) {
success = false; success = false;
status = resp_status == 0 ? 404 : resp_status; status = resp_status; // keep 0 for "no response" so the refresh classifier sees a transport failure
resp_body = body; resp_body = body;
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: HTTP error - " << error; BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: HTTP error - " << error;
}) })

View File

@@ -20,6 +20,14 @@ namespace Slic3r {
class AppConfig; class AppConfig;
struct BundleMetadata; struct BundleMetadata;
// Outcome of a token-refresh attempt: decides whether a 401 should log the user
// out (AuthRejected) or be treated as a recoverable condition (Transient).
enum class RefreshResult {
Success, // new tokens obtained
AuthRejected, // server definitively rejected the refresh token -> logout is correct
Transient // network/server problem -> keep the session and retry later
};
// Constants for OAuth loopback server // Constants for OAuth loopback server
namespace auth_constants { namespace auth_constants {
constexpr int LOOPBACK_PORT = 41172; constexpr int LOOPBACK_PORT = 41172;
@@ -276,10 +284,10 @@ public:
void clear_refresh_token(); void clear_refresh_token();
// Token refresh helpers // Token refresh helpers
bool refresh_if_expiring(std::chrono::seconds skew, const std::string& reason); bool refresh_if_expiring(std::chrono::seconds skew, const std::string& reason);
bool refresh_from_storage(const std::string& reason, bool async = false); RefreshResult refresh_from_storage(const std::string& reason, bool async = false);
bool refresh_now(const std::string& refresh_token, const std::string& reason, bool async = false); RefreshResult refresh_now(const std::string& refresh_token, const std::string& reason, bool async = false);
bool refresh_session_with_token(const std::string& refresh_token); RefreshResult refresh_session_with_token(const std::string& refresh_token);
// Session state helpers. nickname is the human-facing UI label after provider fallback resolution. // Session state helpers. nickname is the human-facing UI label after provider fallback resolution.
bool set_user_session(const std::string& token, bool set_user_session(const std::string& token,
@@ -299,13 +307,28 @@ private:
std::function<void(int http_code, const std::string& error)> on_error std::function<void(int http_code, const std::string& error)> on_error
); );
// Shared result of one HTTP attempt by the data methods (get/post/put/delete).
struct HttpResult {
bool success{false};
unsigned int status{0};
std::string body;
};
// Applies the "retry once on 401" policy for the data HTTP methods.
// `res` holds the first response; `perform` re-issues the request after a
// successful refresh. Returns true if the auth error should be SUPPRESSED
// (i.e. the session must be kept rather than logged out).
bool resolve_unauthorized(HttpResult& res,
const std::function<HttpResult()>& perform,
const std::string& reason);
// HTTP request helpers // HTTP request helpers
int http_get(const std::string& path, std::string* response_body, unsigned int* http_code); int http_get(const std::string& path, std::string* response_body, unsigned int* http_code);
int http_post(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code); int http_post(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code);
int http_put(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code); int http_put(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code);
int http_delete(const std::string& path, std::string* response_body, unsigned int* http_code); int http_delete(const std::string& path, std::string* response_body, unsigned int* http_code);
std::map<std::string, std::string> data_headers(); std::map<std::string, std::string> data_headers();
bool attempt_refresh_after_unauthorized(const std::string& reason); RefreshResult attempt_refresh_after_unauthorized(const std::string& reason);
// Auth HTTP helpers // Auth HTTP helpers
bool http_post_token(const std::string& body, std::string* response_body, unsigned int* http_code, const std::string& url = ""); bool http_post_token(const std::string& body, std::string* response_body, unsigned int* http_code, const std::string& url = "");