• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mendersoftware / mender / 2291732227

28 Jan 2026 01:31PM UTC coverage: 81.749% (+0.2%) from 81.518%
2291732227

push

gitlab-ci

web-flow
Merge pull request #1891 from michalkopczan/MEN-8850-graceful-handling-of-http-429-full

These are the remaining changes for HTTP 429 handling (MEN-8850) - inventory polling and pushing status/logs.

130 of 164 new or added lines in 3 files covered. (79.27%)

1 existing line in 1 file now uncovered.

8936 of 10931 relevant lines covered (81.75%)

20083.05 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.6
/src/mender-update/inventory.cpp
1
// Copyright 2023 Northern.tech AS
2
//
3
//    Licensed under the Apache License, Version 2.0 (the "License");
4
//    you may not use this file except in compliance with the License.
5
//    You may obtain a copy of the License at
6
//
7
//        http://www.apache.org/licenses/LICENSE-2.0
8
//
9
//    Unless required by applicable law or agreed to in writing, software
10
//    distributed under the License is distributed on an "AS IS" BASIS,
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
//    See the License for the specific language governing permissions and
13
//    limitations under the License.
14

15
#include <mender-update/inventory.hpp>
16

17
#include <functional>
18
#include <sstream>
19
#include <string>
20

21
#include <api/api.hpp>
22
#include <api/client.hpp>
23
#include <common/common.hpp>
24
#include <client_shared/conf.hpp>
25
#include <common/error.hpp>
26
#include <common/events.hpp>
27
#include <common/http.hpp>
28
#include <client_shared/inventory_parser.hpp>
29
#include <common/io.hpp>
30
#include <common/json.hpp>
31
#include <common/log.hpp>
32

33
namespace mender {
34
namespace update {
35
namespace inventory {
36

37
using namespace std;
38

39
namespace api = mender::api;
40
namespace common = mender::common;
41
namespace conf = mender::client_shared::conf;
42
namespace error = mender::common::error;
43
namespace events = mender::common::events;
44
namespace expected = mender::common::expected;
45
namespace http = mender::common::http;
46
namespace inv_parser = mender::client_shared::inventory_parser;
47
namespace io = mender::common::io;
48
namespace json = mender::common::json;
49
namespace log = mender::common::log;
50

51
const InventoryErrorCategoryClass InventoryErrorCategory;
52

53
const char *InventoryErrorCategoryClass::name() const noexcept {
×
54
        return "InventoryErrorCategory";
×
55
}
56

57
string InventoryErrorCategoryClass::message(int code) const {
12✔
58
        switch (code) {
12✔
59
        case NoError:
60
                return "Success";
×
61
        case BadResponseError:
62
                return "Bad response error";
8✔
63
        case TooManyRequestsError:
64
                return "Too many requests";
4✔
65
        }
66
        assert(false);
67
        return "Unknown";
×
68
}
69

70
error::Error MakeError(InventoryErrorCode code, const string &msg) {
4✔
71
        return error::Error(error_condition(code, InventoryErrorCategory), msg);
8✔
72
}
73

74
const string uri = "/api/devices/v1/inventory/device/attributes";
75

76
error::Error InventoryClient::PushInventoryData(
5✔
77
        const string &inventory_generators_dir,
78
        events::EventLoop &loop,
79
        api::Client &client,
80
        size_t &last_data_hash,
81
        APIResponseHandler api_handler) {
82
        auto ex_inv_data = inv_parser::GetInventoryData(inventory_generators_dir);
5✔
83
        if (!ex_inv_data) {
5✔
84
                return ex_inv_data.error();
×
85
        }
86
        auto &inv_data = ex_inv_data.value();
5✔
87

88
        // The Mender Client version attribute is owned by
89
        // mender-client-version-inventory-script; mender-update adds the built-in
90
        // version only when not present, marking the provider accordingly.
91
        // See MEN-9016
92
        if (inv_data.count("mender_client_version") != 0) {
10✔
93
                inv_data["mender_client_version_provider"] = {"external"};
8✔
94
        } else {
95
                inv_data["mender_client_version"] = {conf::kMenderVersion};
12✔
96
                inv_data["mender_client_version_provider"] = {"internal"};
12✔
97
        }
98

99
        stringstream top_ss;
5✔
100
        top_ss << "[";
5✔
101
        auto key_vector = common::GetMapKeyVector(inv_data);
5✔
102
        std::sort(key_vector.begin(), key_vector.end());
5✔
103
        for (const auto &key : key_vector) {
27✔
104
                top_ss << R"({"name":")";
22✔
105
                top_ss << json::EscapeString(key);
22✔
106
                top_ss << R"(","value":)";
22✔
107
                if (inv_data[key].size() == 1) {
22✔
108
                        top_ss << "\"" + json::EscapeString(inv_data[key][0]) + "\"";
34✔
109
                } else {
110
                        stringstream items_ss;
5✔
111
                        items_ss << "[";
5✔
112
                        for (const auto &str : inv_data[key]) {
15✔
113
                                items_ss << "\"" + json::EscapeString(str) + "\",";
20✔
114
                        }
115
                        auto items_str = items_ss.str();
116
                        // replace the trailing comma with the closing square bracket
117
                        items_str[items_str.size() - 1] = ']';
5✔
118
                        top_ss << items_str;
5✔
119
                }
5✔
120
                top_ss << R"(},)";
22✔
121
        }
122
        auto payload = top_ss.str();
123
        if (payload[payload.size() - 1] == ',') {
5✔
124
                // replace the trailing comma with the closing square bracket
125
                payload.pop_back();
5✔
126
        }
127
        payload.push_back(']');
5✔
128

129
        size_t payload_hash = std::hash<string> {}(payload);
5✔
130
        if (payload_hash == last_data_hash) {
5✔
131
                log::Info("Inventory data unchanged, not submitting");
1✔
132
                loop.Post([api_handler]() { api_handler(APIResponse {nullopt, nullopt, error::NoError}); });
6✔
133
                return error::NoError;
1✔
134
        }
135

136
        http::BodyGenerator payload_gen = [payload]() {
36✔
137
                return make_shared<io::StringReader>(payload);
4✔
138
        };
4✔
139

140
        auto req = make_shared<api::APIRequest>();
4✔
141
        req->SetPath(uri);
142
        req->SetMethod(http::Method::PUT);
4✔
143
        req->SetHeader("Content-Type", "application/json");
8✔
144
        req->SetHeader("Content-Length", to_string(payload.size()));
8✔
145
        req->SetHeader("Accept", "application/json");
8✔
146
        req->SetBodyGenerator(payload_gen);
4✔
147

148
        auto received_body = make_shared<vector<uint8_t>>();
4✔
149
        return client.AsyncCall(
12✔
150
                req,
151
                [this, received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
8✔
152
                        this->HeaderHandler(received_body, api_handler, exp_resp);
12✔
153
                },
4✔
154
                [received_body, api_handler, payload_hash, &last_data_hash](
12✔
155
                        http::ExpectedIncomingResponsePtr exp_resp) {
156
                        if (!exp_resp) {
4✔
157
                                log::Error("Request to push inventory data failed: " + exp_resp.error().message);
×
NEW
158
                                api_handler(APIResponse {nullopt, nullopt, exp_resp.error()});
×
159
                                return;
×
160
                        }
161

162
                        auto resp = exp_resp.value();
4✔
163
                        auto status = resp->GetStatusCode();
4✔
164

165
                        // StatusTooManyRequests must have been handled in HeaderHandler already
166
                        assert(status != http::StatusTooManyRequests);
167

168
                        if (status == http::StatusOK) {
4✔
169
                                log::Info("Inventory data submitted successfully");
3✔
170
                                last_data_hash = payload_hash;
3✔
171
                                api_handler(APIResponse {status, nullopt, error::NoError});
3✔
172
                        } else {
173
                                auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
1✔
174
                                string err_str;
175
                                if (ex_err_msg) {
1✔
176
                                        err_str = ex_err_msg.value();
1✔
177
                                } else {
178
                                        err_str = resp->GetStatusMessage();
×
179
                                }
180
                                api_handler(APIResponse {
2✔
181
                                        status,
182
                                        nullopt,
183
                                        MakeError(
184
                                                BadResponseError,
185
                                                "Got unexpected response " + to_string(status)
1✔
186
                                                        + " from inventory API: " + err_str)});
2✔
187
                        }
188
                });
4✔
189
}
13✔
190

191
void InventoryClient::HeaderHandler(
7✔
192
        shared_ptr<vector<uint8_t>> received_body,
193
        APIResponseHandler api_handler,
194
        http::ExpectedIncomingResponsePtr exp_resp) {
195
        if (!exp_resp) {
7✔
NEW
196
                log::Error("Request to push inventory data failed: " + exp_resp.error().message);
×
NEW
197
                api_handler(APIResponse {nullopt, nullopt, exp_resp.error()});
×
198
                return;
3✔
199
        }
200

201
        auto body_writer = make_shared<io::ByteWriter>(received_body);
7✔
202
        auto resp = exp_resp.value();
7✔
203
        auto status = resp->GetStatusCode();
7✔
204
        if (status == http::StatusTooManyRequests) {
7✔
205
                api_handler(APIResponse {
6✔
206
                        status, resp->GetHeaders(), MakeError(TooManyRequestsError, "Too many requests")});
6✔
207
                return;
208
        }
209
        auto content_length = resp->GetHeader("Content-Length");
4✔
210
        auto ex_len = common::StringTo<size_t>(content_length.value());
4✔
211
        if (!ex_len) {
4✔
NEW
212
                log::Error("Failed to get content length from the inventory API response headers");
×
NEW
213
                body_writer->SetUnlimited(true);
×
214
        } else {
215
                received_body->resize(ex_len.value());
4✔
216
        }
217
        resp->SetBodyWriter(body_writer);
8✔
218
}
219

220
} // namespace inventory
221
} // namespace update
222
} // namespace mender
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc