Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changes/account-link-errors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
kind: changed
---
Report specific account linking failure reasons instead of a generic internal error

## Release Note
More descriptive account linking errors

Account linking now reports the specific reason it failed (request failed, timed out, server error, invalid response, or failed to save the token) in both the captive portal and the device logs, instead of a generic "Internal error".
5 changes: 5 additions & 0 deletions frontend/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ const _accountLinkErrorMessages: Record<string, string> = {
NoInternetConnection: 'No internet connection',
InvalidCode: 'Invalid code',
RateLimited: 'Too many requests',
RequestFailed: 'Could not reach the server (check DNS/TLS/connection)',
RequestTimedOut: 'Request to the server timed out',
ServerError: 'Server returned an unexpected response',
InvalidResponse: 'Server sent an invalid response',
ConfigSaveFailed: 'Failed to save the auth token to the device',
InternalError: 'Internal error',
};

Expand Down
6 changes: 6 additions & 0 deletions include/AccountLinkResultCode.h
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#pragma once

Check warning on line 1 in include/AccountLinkResultCode.h

View workflow job for this annotation

GitHub Actions / C/C++ Linter

include/AccountLinkResultCode.h:1:1 [portability-avoid-pragma-once]

avoid 'pragma once' directive; use include guards instead

#include <cstdint>

Check failure on line 3 in include/AccountLinkResultCode.h

View workflow job for this annotation

GitHub Actions / C/C++ Linter

include/AccountLinkResultCode.h:3:10 [clang-diagnostic-error]

'cstdint' file not found

namespace OpenShock {
enum class AccountLinkResultCode : uint8_t {

Check warning on line 6 in include/AccountLinkResultCode.h

View workflow job for this annotation

GitHub Actions / C/C++ Linter

include/AccountLinkResultCode.h:6:14 [performance-enum-size]

enum 'AccountLinkResultCode' uses a larger base type ('int', size: 4 bytes) than necessary for its value set, consider using 'std::uint8_t' (1 byte) as the base type to reduce its size
Success = 0,
CodeRequired = 1,
InvalidCodeLength = 2,
Expand All @@ -11,5 +11,11 @@
InvalidCode = 4,
RateLimited = 5,
InternalError = 6,
// More descriptive failure causes (previously all reported as InternalError)
RequestFailed = 7, // Could not start/complete the HTTP request (DNS, TLS, connection refused, ...)
RequestTimedOut = 8, // The request to the backend timed out
ServerError = 9, // The backend returned an unexpected response code
InvalidResponse = 10, // The backend response could not be parsed or was missing the auth token
ConfigSaveFailed = 11, // Failed to persist the auth token to flash
};
} // namespace OpenShock
41 changes: 26 additions & 15 deletions src/GatewayConnectionManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,16 @@

const uint8_t LINK_CODE_LENGTH = 6;

static std::atomic<uint8_t> s_flags = 0;

Check warning on line 43 in src/GatewayConnectionManager.cpp

View workflow job for this annotation

GitHub Actions / C/C++ Linter

src/GatewayConnectionManager.cpp:43:29 [cppcoreguidelines-avoid-non-const-global-variables]

variable 's_flags' is non-const and globally accessible, consider making it const
static std::atomic<int64_t> s_lastAuthFailure = 0;

Check warning on line 44 in src/GatewayConnectionManager.cpp

View workflow job for this annotation

GitHub Actions / C/C++ Linter

src/GatewayConnectionManager.cpp:44:29 [cppcoreguidelines-avoid-non-const-global-variables]

variable 's_lastAuthFailure' is non-const and globally accessible, consider making it const
static std::atomic<int64_t> s_lastConnectionAttempt = 0;

Check warning on line 45 in src/GatewayConnectionManager.cpp

View workflow job for this annotation

GitHub Actions / C/C++ Linter

src/GatewayConnectionManager.cpp:45:29 [cppcoreguidelines-avoid-non-const-global-variables]

variable 's_lastConnectionAttempt' is non-const and globally accessible, consider making it const
static std::atomic_flag s_isInitializing = ATOMIC_FLAG_INIT;

Check warning on line 46 in src/GatewayConnectionManager.cpp

View workflow job for this annotation

GitHub Actions / C/C++ Linter

src/GatewayConnectionManager.cpp:46:25 [cppcoreguidelines-avoid-non-const-global-variables]

variable 's_isInitializing' is non-const and globally accessible, consider making it const
static OpenShock::SimpleMutex s_clientMutex;

Check warning on line 47 in src/GatewayConnectionManager.cpp

View workflow job for this annotation

GitHub Actions / C/C++ Linter

src/GatewayConnectionManager.cpp:47:31 [cppcoreguidelines-avoid-non-const-global-variables]

variable 's_clientMutex' is non-const and globally accessible, consider making it const
static std::shared_ptr<OpenShock::GatewayClient> s_wsClient = nullptr;

Check warning on line 48 in src/GatewayConnectionManager.cpp

View workflow job for this annotation

GitHub Actions / C/C++ Linter

src/GatewayConnectionManager.cpp:48:50 [cppcoreguidelines-avoid-non-const-global-variables]

variable 's_wsClient' is non-const and globally accessible, consider making it const

static std::shared_ptr<OpenShock::GatewayClient> GetClient()

Check warning on line 50 in src/GatewayConnectionManager.cpp

View workflow job for this annotation

GitHub Actions / C/C++ Linter

src/GatewayConnectionManager.cpp:50:50 [modernize-use-trailing-return-type]

use a trailing return type for this function
{
OpenShock::ScopedLock lock__(&s_clientMutex);

Check warning on line 52 in src/GatewayConnectionManager.cpp

View workflow job for this annotation

GitHub Actions / C/C++ Linter

src/GatewayConnectionManager.cpp:52:25 [bugprone-reserved-identifier]

declaration uses identifier 'lock__', which is a reserved identifier
return s_wsClient;
}
static void CreateClient(const std::string& authToken)
Expand Down Expand Up @@ -127,39 +127,50 @@
OS_LOGD(TAG, "Attempting to link to account using code %.*s", linkCode.length(), linkCode.data());

if (linkCode.length() != LINK_CODE_LENGTH) {
OS_LOGE(TAG, "Invalid link code length");
return AccountLinkResultCode::InvalidCode;
OS_LOGE(TAG, "Invalid link code length: expected %zu, got %zu", static_cast<size_t>(LINK_CODE_LENGTH), linkCode.length());
return AccountLinkResultCode::InvalidCodeLength;
}

auto response = HTTP::JsonAPI::LinkAccount(linkCode);

if (response.code == 404) {
OS_LOGW(TAG, "Account link failed: backend rejected the link code as invalid (404)");
return AccountLinkResultCode::InvalidCode;
}

if (response.result == HTTP::RequestResult::RateLimited) {
OS_LOGW(TAG, "Account Link request got ratelimited");
return AccountLinkResultCode::RateLimited;
}
if (response.result != HTTP::RequestResult::Success) {
OS_LOGE(TAG, "Error while getting auth token: %s %d", response.ResultToString(), response.code);

return AccountLinkResultCode::InternalError;
OS_LOGE(TAG, "Account link request failed: %s (result=%hhu, http=%d)", response.ResultToString(), static_cast<uint8_t>(response.result), response.code);

switch (response.result) {
case HTTP::RequestResult::RateLimited:
return AccountLinkResultCode::RateLimited;
case HTTP::RequestResult::TimedOut:
return AccountLinkResultCode::RequestTimedOut;
case HTTP::RequestResult::InvalidURL:
case HTTP::RequestResult::RequestFailed:
return AccountLinkResultCode::RequestFailed;
case HTTP::RequestResult::CodeRejected:
return AccountLinkResultCode::ServerError;
case HTTP::RequestResult::ParseFailed:
return AccountLinkResultCode::InvalidResponse;
default: // InternalError (e.g. no backend domain configured, URI truncated), Cancelled, ...
return AccountLinkResultCode::InternalError;
}
}

if (response.code != 200) {
OS_LOGE(TAG, "Unexpected response code: %d", response.code);
return AccountLinkResultCode::InternalError;
OS_LOGE(TAG, "Account link failed: unexpected response code %d from backend", response.code);
return AccountLinkResultCode::ServerError;
}

if (response.data.authToken.empty()) {
OS_LOGE(TAG, "Received empty auth token");
return AccountLinkResultCode::InternalError;
OS_LOGE(TAG, "Account link failed: backend returned an empty auth token");
return AccountLinkResultCode::InvalidResponse;
}

if (!Config::SetBackendAuthToken(std::move(response.data.authToken))) {
OS_LOGE(TAG, "Failed to save auth token");
return AccountLinkResultCode::InternalError;
OS_LOGE(TAG, "Account link failed: could not persist auth token to flash");
return AccountLinkResultCode::ConfigSaveFailed;
}

s_flags.fetch_or(FLAG_LINKED, std::memory_order_relaxed);
Expand Down
15 changes: 15 additions & 0 deletions src/captiveportal/CaptivePortalInstance.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#include <freertos/FreeRTOS.h>

#include "captiveportal/CaptivePortalInstance.h"
Expand Down Expand Up @@ -206,6 +206,21 @@
case ResultCode::RateLimited:
error = "RateLimited";
break;
case ResultCode::RequestFailed:
error = "RequestFailed";
break;
case ResultCode::RequestTimedOut:
error = "RequestTimedOut";
break;
case ResultCode::ServerError:
error = "ServerError";
break;
case ResultCode::InvalidResponse:
error = "InvalidResponse";
break;
case ResultCode::ConfigSaveFailed:
error = "ConfigSaveFailed";
break;
default:
error = "InternalError";
break;
Expand Down
Loading