Skip to content

Commit f7b157e

Browse files
committed
Error handling docs, switch to APIException, and add function for more fun stuff
1 parent 361f689 commit f7b157e

File tree

6 files changed

+166
-5
lines changed

6 files changed

+166
-5
lines changed

docs/Error-handling.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Error handling
2+
3+
For the most part, the library tries to deal with most of the error handling, as well as backoff.
4+
5+
It's worth noting that not all errors are handled at this time, but that the amount of handled issues will grow as the library does.
6+
7+
The `APIConfig` Struct contains two config variables for error handling; `autoHandleBackoff` and `autoHandleDowntime`. If these are set to false, stackapi.cpp throws when these types of errors are encountered.
8+
9+
If they're set to true, the library sleeps for a few minutes, and then re-tries the last invoked request. **This means the main thread will be blocked** if either of these options are true, and the corresponding type of error occurs. If you run the API calls on the main thread, alongside a user interface, this means the UI will be blocked, and you'll likely want to set these options to false. In turn, you need to handle errors manually.
10+
11+
## Automatic error handling
12+
13+
If you don't disable config variables, many errors are automatically handled. At the time of writing, this is with the exception of specific errors returned by the API, caused by incorrect API calls (mainly programmer errors), as these primarily occur during development. Dealing with these kinds of errors are documented in the manual error handling section.
14+
15+
For automatic error handling, there's naturally not much to do. Don't set the config variables to false, and let the library do its thing.
16+
17+
However, in some cases, you may still want to do extra handling of these errors. For this, there's a configurable callback; `APIConfig::errorCallback`.
18+
19+
This callback is invoked for several reasons:
20+
21+
1. Proper errors; non-200 status codes (Cloudflare blocks the request, backoff violations, downtime, internet outage, etc.)
22+
2. Other waits. At the time of writing, this only applies to respecting backoff before calling an API endpoint. These aren't hard errors, but if you're developing a user-oriented program, this can be used to let the user know they need to wait before the request can be made.
23+
3. Service restauration after the previous points; used to signal that whatever was previously reported has been resolved.
24+
25+
\#3 is a very specific use-case for particularly bots that keep track of whether or not a new error has happened to avoid double-reporting errors. It can also be used to do event-driven updates for the user to avoid having handling on every request made. However, the exact handling is up to each consumer application.
26+
27+
## Manual error handling
28+
29+
If automatic error handling is either disabled or unavailable for a given type of error, you'll get exceptions. For the most part, you'll get an `stackapi::APIException`[^1], which contains a few fields that can be used for manual error handling:
30+
* message: A custom message supplied by stackchat
31+
* errorMessage: the full response from the API endpoint. May or may not be JSON, may or may not say something useful
32+
* statusCode: Either the HTTP status code, a modified HTTP status code (HTTP 400 becomes HTTP 500 if the respons isn't JSON; this is the only modified HTTP status code), or `error_id` parsed from the JSON response.
33+
* isHTTPStatus: Whether or not the statusCode is an HTTP status code, or an error_id corresponding to one of the API error codes.
34+
35+
See the [/errors](https://api.stackexchange.com/docs/errors) endpoint for documentation on values for `error_id`. Note that if you're using automatic error handling, 500, 502, and 503 are automatically handled, and don't require any custom logic.
36+
37+
The use of exceptions does require the use of try-catches in the code, but it also (in my biased opinion) simplifies certain parts of the handling. For example, if you're using an endpoint that requires an access_token, and you want to handle access token errors, you can use:
38+
```cpp
39+
try {
40+
// Note that the NullStruct used here is purely because I don't care enough to look up the return value of this endpoint
41+
// In real code, you probably want to check the return value
42+
api.post<stackapi::NullStruct>("questions/1234/upvote");
43+
} catch (APIException e) {
44+
if (!e.isHTTPStatus && e.statusCode >= 401 && e.statusCode <= 403) {
45+
// Notify the user that their token is bad
46+
// Note that you can parse e.errorMessage as JSON to get error_message from it, and append it to the message
47+
48+
} else {
49+
// Unhandled; either handle it or just throw, or ignore it if you prefer
50+
throw;
51+
}
52+
}
53+
```
54+
55+
[^1]: At the time of writing, this is the only exception thrown. Some parts of the library throw std::runtime_error, but these are not in response to API calls, and are more often than not programmer errors.

docs/Getting-started.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,4 @@ Due to SSL being used, Linux users have to install libssl-dev (Debian and deriva
5454
## After installing
5555

5656
You should now have access to the library. See the [general use document](General-use.md) for getting started with the code, or take a look at the demos.
57+

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ Because GitHub, this is a pseudo-chapter list:
55
1. [Getting started](Getting-started.md)
66
2. [General use](General-use.md)
77
3. [Auth](Auth.md)
8+
4. [Error handling](Error-handling.md)

src/stackapi/StackAPI.cpp

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include "StackAPI.hpp"
2+
#include "stackapi/errors/APIException.hpp"
23

34
#include <stdexcept>
45

@@ -18,6 +19,7 @@ nlohmann::json StackAPI::postRaw(const std::string& dest,
1819
if (dryRun) {
1920
return {};
2021
}
22+
bool faulted = false;
2123
do {
2224
if (opt.autoHandleBackoff.value_or(conf.autoHandleBackoff)) {
2325
checkBackoff();
@@ -44,8 +46,12 @@ nlohmann::json StackAPI::postRaw(const std::string& dest,
4446
auto res = sess.Post(url, body, conf.userAgent);
4547

4648
if (auto jsonOpt = checkErrors(res, opt)) {
49+
if (faulted) {
50+
conf.reportError(FaultType::RESTORED, "");
51+
}
4752
return *jsonOpt;
4853
}
54+
faulted = true;
4955

5056
} while (true);
5157
}
@@ -54,6 +60,8 @@ nlohmann::json StackAPI::getRaw(const std::string &dest,
5460
const std::map<std::string, std::string>& extraParams,
5561
const APIConfigOpt& opt) {
5662

63+
bool faulted = false;
64+
5765
do {
5866

5967
if (opt.autoHandleBackoff.value_or(conf.autoHandleBackoff)) {
@@ -81,8 +89,12 @@ nlohmann::json StackAPI::getRaw(const std::string &dest,
8189
auto res = sess.Get(url, body, conf.userAgent);
8290

8391
if (auto jsonOpt = checkErrors(res, opt)) {
92+
if (faulted) {
93+
conf.reportError(FaultType::RESTORED, "");
94+
}
8495
return *jsonOpt;
8596
}
97+
faulted = true;
8698
} while (true);
8799

88100
}
@@ -100,6 +112,7 @@ void StackAPI::checkBackoff() {
100112

101113
auto wait = backoff.secs - consumed + 2 * conf.backoffStrictnessMultiplier;
102114
if (wait > 0) {
115+
this->conf.reportError(FaultType::RATE_LIMIT, "Backoff instructed; waiting " + std::to_string(wait) + " seconds before sending request...");
103116
std::this_thread::sleep_for(std::chrono::seconds(wait));
104117
}
105118

@@ -109,9 +122,11 @@ void StackAPI::checkBackoff() {
109122

110123
std::optional<nlohmann::json> StackAPI::checkErrors(cpr::Response& res, const APIConfigOpt& opt) {
111124
auto status_code = res.status_code;
125+
bool isHTTPStatus = true;
112126
if (status_code == 400) {
113127
try {
114128
status_code = nlohmann::json::parse(res.text).at("error_id");
129+
isHTTPStatus = false;
115130
} catch (...) {
116131
// "json" is HTML
117132
status_code = 500;
@@ -122,34 +137,38 @@ std::optional<nlohmann::json> StackAPI::checkErrors(cpr::Response& res, const AP
122137
case 0: {
123138
if (opt.autoHandleDowntime.value_or(conf.autoHandleDowntime)) {
124139
spdlog::warn("Host disconnected from the internet. Sleeping 5 minutes...");
140+
conf.reportError(FaultType::DOWNTIME, "Host seems to be disconnected from the internet. Retrying in 5 minutes");
125141
std::this_thread::sleep_for(std::chrono::minutes(5));
126142
} else {
127-
throw std::runtime_error("Connection failed or internet dead (probably the latter): " + res.text + "; " + res.error.message);
143+
throw APIException("Connection failed, or your internet connection is down", res.text, res.url.str(), res.status_code, isHTTPStatus);
128144
}
129145
} break;
130146
case 429:
131147
// Cloudflare bullshit
132148
if (opt.autoHandleBackoff.value_or(conf.autoHandleBackoff)) {
133149
spdlog::warn("Cloudflare says no. Sleeping for 5 minutes...");
150+
conf.reportError(FaultType::CLOUDFLARE, "Cloudflare has blocked your IP. Retrying in 5 minutes.");
134151
std::this_thread::sleep_for(std::chrono::minutes(5));
135152
} else {
136-
throw std::runtime_error("Cloudflare backoff");
153+
throw APIException("Cloudflare blocked the request", res.text, res.url.str(), res.status_code, isHTTPStatus);
137154
}
138155
case 500:
139156
case 503: {
140157
if (opt.autoHandleDowntime.value_or(conf.autoHandleDowntime)) {
141158
spdlog::warn("Stack is down. Sleeping 5 minutes...");
159+
conf.reportError(FaultType::DOWNTIME, "Stack's API appears to be down. Retrying in 5 minutes");
142160
std::this_thread::sleep_for(std::chrono::minutes(5));
143161
} else {
144-
throw std::runtime_error("Stack's servers are down: " + res.text + "; " + res.error.message);
162+
throw APIException("Stack's servers appear to be down", res.text, res.url.str(), res.status_code, isHTTPStatus);
145163
}
146164
} break;
147165
case 502: {
148166
if (opt.autoHandleBackoff.value_or(conf.autoHandleBackoff)) {
149167
spdlog::warn("Backoff violated. Sleeping for 5 minutes...");
168+
conf.reportError(FaultType::RATE_LIMIT, "Backoff violated - sleeping before retrying.");
150169
std::this_thread::sleep_for(std::chrono::minutes(5));
151170
} else {
152-
throw std::runtime_error("Backoff violated. " + res.text + "; " + res.error.message);
171+
throw APIException("Backoff violated", res.text, res.url.str(), res.status_code, isHTTPStatus);
153172
}
154173
} break;
155174
case 200: {
@@ -158,7 +177,7 @@ std::optional<nlohmann::json> StackAPI::checkErrors(cpr::Response& res, const AP
158177
return json;
159178
} break;
160179
default:
161-
throw std::runtime_error("Unhandled status code: " + std::to_string(res.status_code) + "; " + res.text + "; " + res.error.message + " on URL " + res.url.str());
180+
throw APIException("Likely API error; see errorMessage and statusCode for the type", res.text, res.url.str(), res.status_code, isHTTPStatus);
162181
}
163182
return {};
164183
}

src/stackapi/StackAPI.hpp

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,18 @@
1616

1717
namespace stackapi {
1818

19+
enum class FaultType {
20+
DOWNTIME,
21+
CLOUDFLARE,
22+
RATE_LIMIT,
23+
OTHER,
24+
/**
25+
* Special code that doesn't denote a fault, but notes that whatever fault was previously reported has been fixed.
26+
* Note that if FaultType == RESTORED, then message == "", so it shouldn't be displayed directly.
27+
*/
28+
RESTORED
29+
};
30+
1931
struct APIConfig {
2032
std::string apiKey = "";
2133
std::string auth = "";
@@ -53,7 +65,43 @@ struct APIConfig {
5365

5466
cpr::UserAgent userAgent = "StackAPIUnannouncedUser/git";
5567

68+
/**
69+
* Special callback invoked when dealing with backoff or downtime.
70+
* Note that this is only invoked when autoHandleBackoff and/or autoHandleDowntime is true.
71+
* If, for example, automatically handling backoff, but autoHandleBackoff is false, an
72+
* exception is thrown instead. This callback is meant to be coupled with the built-in
73+
* handling systems.
74+
*
75+
* Note that FaultType doesn't 1:1 correspond with the different config options. Notably,
76+
* Cloudflare is treated as backoff, while RATE_LIMIT just refers to the standard backoff.
77+
* The categories are meant to be used to separate issues you do or don't care about handling.
78+
*
79+
* This function is primarily aimed at two things:
80+
* 1. Supplying fault notification systems with information; for the most part, this applies to
81+
* CLOUDFLARE, as cloudflare blocking is downtime, while rate limit-based backoff is more expected.
82+
*
83+
* This is primarily aimed at bot and bot management, and to make it easier for me to know when to
84+
* poke SE with another complaint about Cloudflare being dumb again
85+
* 2. Supplying information to an end-user in, for example, a desktop application, where the auto-handling
86+
* is still desirable behaviour, but where the user also should be notified
87+
*
88+
* Note that this function is not required. If you don't care about handling any of these, set the value to
89+
* nullptr.
90+
*
91+
* Also note that not everything reported is an error. This function is also invoked when doing preventative
92+
* backoff waiting, and not only when SE blocks the request. This is also reported under FaultType::RATE_LIMIT,
93+
* with a message explaining the problem. I have not decided if it makes sense to separate these or not.
94+
*/
95+
std::function<void(FaultType, const std::string& message)> errorCallback = nullptr;
96+
5697
Backoff backoff = { 0, std::chrono::system_clock::now() };
98+
99+
void reportError(FaultType type, const std::string& message) const {
100+
if (errorCallback) {
101+
errorCallback(type, message);
102+
}
103+
}
104+
57105
};
58106

59107
struct APIConfigOpt {
@@ -67,6 +115,7 @@ struct APIConfigOpt {
67115
std::optional<bool> autoHandleBackoff;
68116
std::optional<bool> autoHandleDowntime;
69117
std::optional<bool> treat404AsDowntime;
118+
70119
};
71120

72121
class StackAPI {
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#pragma once
2+
3+
#include "spdlog/spdlog.h"
4+
#include <exception>
5+
#include <string>
6+
7+
#include <stackapi/data/structs/Types.hpp>
8+
9+
namespace stackapi {
10+
11+
class APIException : public std::exception {
12+
public:
13+
std::string message;
14+
/**
15+
* message usually contains API-provided messages, while errorMessage contains a message straight from the API.
16+
*
17+
* Note that this may or may not be useful. Not all faults return JSON
18+
*/
19+
std::string errorMessage;
20+
std::string url;
21+
API_INT statusCode;
22+
bool isHTTPStatus;
23+
24+
APIException(const std::string& message, const std::string& errorMessage, const std::string& url, API_INT statusCode,
25+
bool isHTTPStatus)
26+
: message(message), url(url), errorMessage(errorMessage), statusCode(statusCode), isHTTPStatus(isHTTPStatus) {
27+
// TODO: Does this make sense?
28+
spdlog::error("{}: {} (status code: {} on invoked URL {})", message, errorMessage, statusCode, url);
29+
}
30+
31+
char* what() {
32+
return message.data();
33+
}
34+
};
35+
36+
}

0 commit comments

Comments
 (0)