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

mendersoftware / mender / 2413180388

27 Mar 2026 10:33AM UTC coverage: 75.834% (+0.02%) from 75.818%
2413180388

push

gitlab-ci

danielskinstad
fix: Don't retry status update on 413

Changelog: Added an explicit check for 413 Request Body Too Large errors
when sending deployment logs in order to not go into an unnecessary retry
loop when the deployment logs are too big.

Ticket: ME-616

Signed-off-by: Daniel Skinstad Drabitzius <daniel.drabitzius@northern.tech>
(cherry picked from commit 682e48073)

8 of 12 new or added lines in 3 files covered. (66.67%)

7522 of 9919 relevant lines covered (75.83%)

13682.93 hits per line

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

79.49
/src/mender-update/deployments/deployments.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/deployments.hpp>
16

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

21
#include <api/api.hpp>
22
#include <api/client.hpp>
23
#include <common/common.hpp>
24
#include <common/error.hpp>
25
#include <common/events.hpp>
26
#include <common/expected.hpp>
27
#include <common/http.hpp>
28
#include <common/io.hpp>
29
#include <common/json.hpp>
30
#include <common/log.hpp>
31
#include <common/optional.hpp>
32
#include <common/path.hpp>
33
#include <mender-update/context.hpp>
34

35
namespace mender {
36
namespace update {
37
namespace deployments {
38

39
using namespace std;
40

41
namespace api = mender::api;
42
namespace common = mender::common;
43
namespace context = mender::update::context;
44
namespace error = mender::common::error;
45
namespace events = mender::common::events;
46
namespace expected = mender::common::expected;
47
namespace http = mender::common::http;
48
namespace io = mender::common::io;
49
namespace json = mender::common::json;
50
namespace log = mender::common::log;
51
namespace path = mender::common::path;
52

53
const DeploymentsErrorCategoryClass DeploymentsErrorCategory;
54

55
const char *DeploymentsErrorCategoryClass::name() const noexcept {
×
56
        return "DeploymentsErrorCategory";
×
57
}
58

59
string DeploymentsErrorCategoryClass::message(int code) const {
3✔
60
        switch (code) {
3✔
61
        case NoError:
62
                return "Success";
×
63
        case InvalidDataError:
64
                return "Invalid data error";
×
65
        case BadResponseError:
66
                return "Bad response error";
×
67
        case DeploymentAbortedError:
68
                return "Deployment was aborted on the server";
3✔
69
        case TooManyRequestsError:
NEW
70
                return "Too many requests";
×
71
        case RequestBodyTooLargeError:
NEW
72
                return "Request body too large";
×
73
        }
74
        assert(false);
75
        return "Unknown";
×
76
}
77

78
error::Error MakeError(DeploymentsErrorCode code, const string &msg) {
53✔
79
        return error::Error(error_condition(code, DeploymentsErrorCategory), msg);
59✔
80
}
81

82
static const string check_updates_v1_uri = "/api/devices/v1/deployments/device/deployments/next";
83
static const string check_updates_v2_uri = "/api/devices/v2/deployments/device/deployments/next";
84

85
error::Error DeploymentClient::CheckNewDeployments(
7✔
86
        context::MenderContext &ctx, api::Client &client, CheckUpdatesAPIResponseHandler api_handler) {
87
        auto ex_dev_type = ctx.GetDeviceType();
7✔
88
        if (!ex_dev_type) {
7✔
89
                return ex_dev_type.error();
1✔
90
        }
91
        string device_type = ex_dev_type.value();
6✔
92

93
        auto ex_provides = ctx.LoadProvides();
6✔
94
        if (!ex_provides) {
6✔
95
                return ex_provides.error();
×
96
        }
97
        auto provides = ex_provides.value();
6✔
98
        if (provides.find("artifact_name") == provides.end()) {
12✔
99
                return MakeError(InvalidDataError, "Missing artifact name data");
×
100
        }
101

102
        stringstream ss;
12✔
103
        ss << R"({"device_provides":{)";
6✔
104
        ss << R"("device_type":")";
6✔
105
        ss << json::EscapeString(device_type);
12✔
106

107
        for (const auto &kv : provides) {
14✔
108
                ss << "\",\"" + json::EscapeString(kv.first) + "\":\"";
16✔
109
                ss << json::EscapeString(kv.second);
16✔
110
        }
111

112
        ss << R"("}})";
6✔
113

114
        string v2_payload = ss.str();
115
        log::Debug("deployments/next v2 payload " + v2_payload);
6✔
116
        http::BodyGenerator payload_gen = [v2_payload]() {
48✔
117
                return make_shared<io::StringReader>(v2_payload);
6✔
118
        };
6✔
119

120
        auto v2_req = make_shared<api::APIRequest>();
6✔
121
        v2_req->SetPath(check_updates_v2_uri);
122
        v2_req->SetMethod(http::Method::POST);
6✔
123
        v2_req->SetHeader("Content-Type", "application/json");
12✔
124
        v2_req->SetHeader("Content-Length", to_string(v2_payload.size()));
12✔
125
        v2_req->SetHeader("Accept", "application/json");
12✔
126
        v2_req->SetBodyGenerator(payload_gen);
6✔
127

128
        string v1_args = "artifact_name=" + http::URLEncode(provides["artifact_name"])
18✔
129
                                         + "&device_type=" + http::URLEncode(device_type);
18✔
130
        auto v1_req = make_shared<api::APIRequest>();
6✔
131
        v1_req->SetPath(check_updates_v1_uri + "?" + v1_args);
12✔
132
        v1_req->SetMethod(http::Method::GET);
6✔
133
        v1_req->SetHeader("Accept", "application/json");
12✔
134

135
        auto received_body = make_shared<vector<uint8_t>>();
6✔
136
        auto handle_data = [received_body, api_handler](unsigned status) {
4✔
137
                if (status == http::StatusOK) {
4✔
138
                        auto ex_j = json::Load(common::StringFromByteVector(*received_body));
4✔
139
                        if (ex_j) {
2✔
140
                                CheckUpdatesAPIResponse response {optional<json::Json> {ex_j.value()}};
2✔
141
                                api_handler(response);
4✔
142
                        } else {
143
                                api_handler(expected::unexpected(ex_j.error()));
×
144
                        }
145
                } else if (status == http::StatusNoContent) {
2✔
146
                        api_handler(CheckUpdatesAPIResponse {nullopt});
4✔
147
                } else {
148
                        log::Warning(
×
149
                                "DeploymentClient::CheckNewDeployments - received unhandled http response: "
150
                                + to_string(status));
×
151
                        api_handler(expected::unexpected(MakeError(
×
152
                                DeploymentAbortedError, "received unhandled HTTP response: " + to_string(status))));
×
153
                }
154
        };
16✔
155

156
        http::ResponseHandler header_handler =
157
                [received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
9✔
158
                        if (!exp_resp) {
9✔
159
                                log::Error("Request to check new deployments failed: " + exp_resp.error().message);
×
160
                                CheckUpdatesAPIResponse response = expected::unexpected(exp_resp.error());
×
161
                                api_handler(response);
×
162
                                return;
163
                        }
164

165
                        auto resp = exp_resp.value();
9✔
166
                        received_body->clear();
9✔
167
                        auto body_writer = make_shared<io::ByteWriter>(received_body);
9✔
168
                        body_writer->SetUnlimited(true);
9✔
169
                        resp->SetBodyWriter(body_writer);
18✔
170
                };
12✔
171

172
        http::ResponseHandler v1_body_handler =
173
                [received_body, api_handler, handle_data](http::ExpectedIncomingResponsePtr exp_resp) {
3✔
174
                        if (!exp_resp) {
3✔
175
                                log::Error("Request to check new deployments failed: " + exp_resp.error().message);
×
176
                                CheckUpdatesAPIResponse response = expected::unexpected(exp_resp.error());
×
177
                                api_handler(response);
×
178
                                return;
179
                        }
180
                        auto resp = exp_resp.value();
3✔
181
                        auto status = resp->GetStatusCode();
3✔
182
                        if ((status == http::StatusOK) || (status == http::StatusNoContent)) {
3✔
183
                                handle_data(status);
2✔
184
                        } else {
185
                                auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
1✔
186
                                string err_str;
187
                                if (ex_err_msg) {
1✔
188
                                        err_str = ex_err_msg.value();
×
189
                                } else {
190
                                        err_str = resp->GetStatusMessage();
2✔
191
                                }
192
                                api_handler(expected::unexpected(MakeError(
2✔
193
                                        BadResponseError,
194
                                        "Got unexpected response " + to_string(status) + ": " + err_str)));
4✔
195
                        }
196
                };
12✔
197

198
        http::ResponseHandler v2_body_handler = [received_body,
6✔
199
                                                                                         v1_req,
200
                                                                                         header_handler,
201
                                                                                         v1_body_handler,
202
                                                                                         api_handler,
203
                                                                                         handle_data,
204
                                                                                         &client](http::ExpectedIncomingResponsePtr exp_resp) {
3✔
205
                if (!exp_resp) {
6✔
206
                        log::Error("Request to check new deployments failed: " + exp_resp.error().message);
×
207
                        CheckUpdatesAPIResponse response = expected::unexpected(exp_resp.error());
×
208
                        api_handler(response);
×
209
                        return;
210
                }
211
                auto resp = exp_resp.value();
6✔
212
                auto status = resp->GetStatusCode();
6✔
213
                if ((status == http::StatusOK) || (status == http::StatusNoContent)) {
6✔
214
                        handle_data(status);
2✔
215
                } else if (status == http::StatusNotFound) {
4✔
216
                        log::Debug(
3✔
217
                                "POST request to v2 version of the deployments API failed, falling back to v1 version and GET");
6✔
218
                        auto err = client.AsyncCall(v1_req, header_handler, v1_body_handler);
9✔
219
                        if (err != error::NoError) {
3✔
220
                                api_handler(expected::unexpected(err.WithContext("While calling v1 endpoint")));
×
221
                        }
222
                } else {
223
                        auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
1✔
224
                        string err_str;
225
                        if (ex_err_msg) {
1✔
226
                                err_str = ex_err_msg.value();
1✔
227
                        } else {
228
                                err_str = resp->GetStatusMessage();
×
229
                        }
230
                        api_handler(expected::unexpected(MakeError(
2✔
231
                                BadResponseError,
232
                                "Got unexpected response " + to_string(status) + ": " + err_str)));
4✔
233
                }
234
        };
6✔
235

236
        return client.AsyncCall(v2_req, header_handler, v2_body_handler);
18✔
237
}
238

239
static const string deployment_status_strings[static_cast<int>(DeploymentStatus::End_) + 1] = {
240
        "installing",
241
        "pause_before_installing",
242
        "downloading",
243
        "pause_before_rebooting",
244
        "rebooting",
245
        "pause_before_committing",
246
        "success",
247
        "failure",
248
        "already-installed"};
249

250
static const string deployments_uri_prefix = "/api/devices/v1/deployments/device/deployments";
251
static const string status_uri_suffix = "/status";
252

253
string DeploymentStatusString(DeploymentStatus status) {
501✔
254
        return deployment_status_strings[static_cast<int>(status)];
505✔
255
}
256

257
error::Error DeploymentClient::PushStatus(
4✔
258
        const string &deployment_id,
259
        DeploymentStatus status,
260
        const string &substate,
261
        api::Client &client,
262
        StatusAPIResponseHandler api_handler) {
263
        // Cannot push a status update without a deployment ID
264
        AssertOrReturnError(deployment_id != "");
4✔
265
        string payload = R"({"status":")" + DeploymentStatusString(status) + "\"";
8✔
266
        if (substate != "") {
4✔
267
                payload += R"(,"substate":")" + json::EscapeString(substate) + "\"}";
6✔
268
        } else {
269
                payload += "}";
1✔
270
        }
271
        http::BodyGenerator payload_gen = [payload]() {
32✔
272
                return make_shared<io::StringReader>(payload);
4✔
273
        };
4✔
274

275
        auto req = make_shared<api::APIRequest>();
4✔
276
        req->SetPath(http::JoinUrl(deployments_uri_prefix, deployment_id, status_uri_suffix));
8✔
277
        req->SetMethod(http::Method::PUT);
4✔
278
        req->SetHeader("Content-Type", "application/json");
8✔
279
        req->SetHeader("Content-Length", to_string(payload.size()));
8✔
280
        req->SetHeader("Accept", "application/json");
8✔
281
        req->SetBodyGenerator(payload_gen);
4✔
282

283
        auto received_body = make_shared<vector<uint8_t>>();
4✔
284
        return client.AsyncCall(
285
                req,
286
                [received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
4✔
287
                        if (!exp_resp) {
4✔
288
                                log::Error("Request to push status data failed: " + exp_resp.error().message);
×
289
                                api_handler(exp_resp.error());
×
290
                                return;
×
291
                        }
292

293
                        auto body_writer = make_shared<io::ByteWriter>(received_body);
4✔
294
                        auto resp = exp_resp.value();
4✔
295
                        auto content_length = resp->GetHeader("Content-Length");
8✔
296
                        if (!content_length) {
4✔
297
                                log::Debug(
×
298
                                        "Failed to get content length from the deployment status API response headers: "
299
                                        + content_length.error().String());
×
300
                                body_writer->SetUnlimited(true);
×
301
                        } else {
302
                                auto ex_len = common::StringTo<size_t>(content_length.value());
4✔
303
                                if (!ex_len) {
4✔
304
                                        log::Error(
×
305
                                                "Failed to convert the content length from the deployment status API response headers to an integer: "
306
                                                + ex_len.error().String());
×
307
                                        body_writer->SetUnlimited(true);
×
308
                                } else {
309
                                        received_body->resize(ex_len.value());
4✔
310
                                }
311
                        }
312
                        resp->SetBodyWriter(body_writer);
8✔
313
                },
314
                [received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
4✔
315
                        if (!exp_resp) {
4✔
316
                                log::Error("Request to push status data failed: " + exp_resp.error().message);
×
317
                                api_handler(exp_resp.error());
×
318
                                return;
×
319
                        }
320

321
                        auto resp = exp_resp.value();
4✔
322
                        auto status = resp->GetStatusCode();
4✔
323
                        if (status == http::StatusNoContent) {
4✔
324
                                api_handler(error::NoError);
4✔
325
                        } else if (status == http::StatusConflict) {
2✔
326
                                api_handler(
1✔
327
                                        MakeError(DeploymentAbortedError, "Could not send status update to server"));
2✔
328
                        } else {
329
                                auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
1✔
330
                                string err_str;
331
                                if (ex_err_msg) {
1✔
332
                                        err_str = ex_err_msg.value();
1✔
333
                                } else {
334
                                        err_str = resp->GetStatusMessage();
×
335
                                }
336
                                api_handler(MakeError(
2✔
337
                                        BadResponseError,
338
                                        "Got unexpected response " + to_string(status)
2✔
339
                                                + " from status API: " + err_str));
2✔
340
                        }
341
                });
16✔
342
}
343

344
using mender::common::expected::ExpectedSize;
345

346
static ExpectedSize GetLogFileDataSize(const string &path) {
22✔
347
        auto ex_istr = io::OpenIfstream(path);
22✔
348
        if (!ex_istr) {
22✔
349
                return expected::unexpected(ex_istr.error());
×
350
        }
351
        auto istr = std::move(ex_istr.value());
44✔
352

353
        // We want the size of the actual data without a potential trailing
354
        // comma. So let's seek one byte before the end of file, check if the last
355
        // byte is a comma and return the appropriate number.
356
        istr.seekg(-1, ios_base::end);
22✔
357
        int c = istr.get();
22✔
358
        if (c == ',') {
22✔
359
                return istr.tellg() - static_cast<ifstream::off_type>(1);
22✔
360
        } else {
361
                return istr.tellg();
×
362
        }
363
}
364

365
const vector<uint8_t> JsonLogMessagesReader::header_ = {
366
        '{', '"', 'm', 'e', 's', 's', 'a', 'g', 'e', 's', '"', ':', '['};
367
const vector<uint8_t> JsonLogMessagesReader::closing_ = {']', '}'};
368
const string JsonLogMessagesReader::default_tstamp_ = "1970-01-01T00:00:00.000000000Z";
369
const string JsonLogMessagesReader::bad_data_msg_tmpl_ =
370
        R"d({"timestamp": "1970-01-01T00:00:00.000000000Z", "level": "ERROR", "message": "(THE ORIGINAL LOGS CONTAINED INVALID ENTRIES)"},)d";
371

372
JsonLogMessagesReader::~JsonLogMessagesReader() {
51✔
373
        reader_.reset();
374
        if (!sanitized_fpath_.empty() && path::FileExists(sanitized_fpath_)) {
17✔
375
                auto del_err = path::FileDelete(sanitized_fpath_);
17✔
376
                if (del_err != error::NoError) {
17✔
377
                        log::Error("Failed to delete auxiliary logs file: " + del_err.String());
×
378
                }
379
        }
380
        sanitized_fpath_.erase();
17✔
381
}
17✔
382

383
static error::Error DoSanitizeLogs(
22✔
384
        const string &orig_path, const string &new_path, bool &all_valid, string &first_tstamp) {
385
        auto ex_ifs = io::OpenIfstream(orig_path);
22✔
386
        if (!ex_ifs) {
22✔
387
                return ex_ifs.error();
×
388
        }
389
        auto ex_ofs = io::OpenOfstream(new_path);
22✔
390
        if (!ex_ofs) {
22✔
391
                return ex_ofs.error();
×
392
        }
393
        auto &ifs = ex_ifs.value();
22✔
394
        auto &ofs = ex_ofs.value();
22✔
395

396
        string last_known_tstamp = first_tstamp;
22✔
397
        const string tstamp_prefix_data = R"d({"timestamp": ")d";
22✔
398
        const string corrupt_msg_suffix_data =
399
                R"d(", "level": "ERROR", "message": "(CORRUPTED LOG DATA)"},)d";
22✔
400

401
        string line;
402
        first_tstamp.erase();
22✔
403
        all_valid = true;
22✔
404
        error::Error err;
22✔
405
        while (!ifs.eof()) {
104✔
406
                getline(ifs, line);
82✔
407
                if (!ifs.eof() && !ifs) {
82✔
408
                        int io_errno = errno;
×
409
                        return error::Error(
410
                                generic_category().default_error_condition(io_errno),
×
411
                                "Failed to get line from deployment logs file '" + orig_path
×
412
                                        + "': " + strerror(io_errno));
×
413
                }
414
                if (line.empty()) {
82✔
415
                        // skip empty lines
416
                        continue;
22✔
417
                }
418
                auto ex_json = json::Load(line);
120✔
419
                if (ex_json) {
60✔
420
                        // valid JSON log line, just replace the newline after it with a comma and save the
421
                        // timestamp for later
422
                        auto ex_tstamp = ex_json.value().Get("timestamp").and_then(json::ToString);
94✔
423
                        if (ex_tstamp) {
47✔
424
                                if (first_tstamp.empty()) {
47✔
425
                                        first_tstamp = ex_tstamp.value();
21✔
426
                                }
427
                                last_known_tstamp = std::move(ex_tstamp.value());
47✔
428
                        }
429
                        line.append(1, ',');
47✔
430
                        err = io::WriteStringIntoOfstream(ofs, line);
47✔
431
                        if (err != error::NoError) {
47✔
432
                                return err.WithContext("Failed to write pre-processed deployment logs data");
×
433
                        }
434
                } else {
435
                        all_valid = false;
13✔
436
                        if (first_tstamp.empty()) {
13✔
437
                                // If we still don't have the first valid tstamp, we need to
438
                                // save the last known one (potentially pre-set) as the first
439
                                // one.
440
                                first_tstamp = last_known_tstamp;
441
                        }
442
                        err = io::WriteStringIntoOfstream(
13✔
443
                                ofs, tstamp_prefix_data + last_known_tstamp + corrupt_msg_suffix_data);
26✔
444
                        if (err != error::NoError) {
13✔
445
                                return err.WithContext("Failed to write pre-processed deployment logs data");
×
446
                        }
447
                }
448
        }
449
        return error::NoError;
22✔
450
}
451

452
error::Error JsonLogMessagesReader::SanitizeLogs() {
22✔
453
        if (!sanitized_fpath_.empty()) {
22✔
454
                return error::NoError;
×
455
        }
456

457
        string prep_fpath = log_fpath_ + ".sanitized";
22✔
458
        string first_tstamp = default_tstamp_;
22✔
459
        auto err = DoSanitizeLogs(log_fpath_, prep_fpath, clean_logs_, first_tstamp);
22✔
460
        if (err != error::NoError) {
22✔
461
                if (path::FileExists(prep_fpath)) {
×
462
                        auto del_err = path::FileDelete(prep_fpath);
×
463
                        if (del_err != error::NoError) {
×
464
                                log::Error("Failed to delete auxiliary logs file: " + del_err.String());
×
465
                        }
466
                }
467
        } else {
468
                sanitized_fpath_ = std::move(prep_fpath);
22✔
469
                reader_ = make_unique<io::FileReader>(sanitized_fpath_);
44✔
470
                auto ex_sz = GetLogFileDataSize(sanitized_fpath_);
22✔
471
                if (!ex_sz) {
22✔
472
                        return ex_sz.error().WithContext("Failed to determine deployment logs size");
×
473
                }
474
                raw_data_size_ = ex_sz.value();
22✔
475
                rem_raw_data_size_ = raw_data_size_;
22✔
476
                if (!clean_logs_) {
22✔
477
                        auto bad_data_msg_tstamp_start =
478
                                bad_data_msg_.begin() + 15; // len(R"({"timestamp": ")")
479

480
                        // The actual timestamp from logs can potential have a different
481
                        // (likely lower) time resolution and thus length than our default.
482
                        const auto first_tstamp_size = first_tstamp.size();
483
                        if (first_tstamp_size > default_tstamp_.size()) {
9✔
484
                                // In case the time resolution is higher and the timestamp
485
                                // longer (unlikely to happen)
486
                                if (first_tstamp[first_tstamp_size - 1] == 'Z') {
1✔
487
                                        first_tstamp[default_tstamp_.size() - 1] = 'Z';
1✔
488
                                }
489
                                first_tstamp.resize(default_tstamp_.size());
1✔
490
                        }
491
                        copy_n(first_tstamp.cbegin(), first_tstamp.size(), bad_data_msg_tstamp_start);
9✔
492
                        if (first_tstamp.size() < default_tstamp_.size()) {
9✔
493
                                // Add a closing '"' right after the timestamp and fill in the
494
                                // rest of the space in the template with spaces that have no
495
                                // effect in JSON.
496
                                bad_data_msg_tstamp_start[first_tstamp.size()] = '"';
1✔
497
                                for (auto it = bad_data_msg_tstamp_start + first_tstamp.size() + 1;
498
                                         it < bad_data_msg_tstamp_start + default_tstamp_.size() + 1;
4✔
499
                                         it++) {
500
                                        *it = ' ';
3✔
501
                                }
502
                        }
503
                }
504
        }
505
        return err;
22✔
506
}
507

508
error::Error JsonLogMessagesReader::Rewind() {
5✔
509
        AssertOrReturnError(!sanitized_fpath_.empty());
5✔
510
        header_rem_ = header_.size();
5✔
511
        closing_rem_ = closing_.size();
5✔
512
        bad_data_msg_rem_ = bad_data_msg_.size();
5✔
513

514
        // release/close the file first so that the FileDelete() below can actually
515
        // delete it and free space up
516
        reader_.reset();
517
        auto del_err = path::FileDelete(sanitized_fpath_);
5✔
518
        if (del_err != error::NoError) {
5✔
519
                log::Error("Failed to delete auxiliary logs file: " + del_err.String());
2✔
520
        }
521
        sanitized_fpath_.erase();
5✔
522
        return SanitizeLogs();
5✔
523
}
524

525
int64_t JsonLogMessagesReader::TotalDataSize() {
17✔
526
        assert(!sanitized_fpath_.empty());
527

528
        auto ret = raw_data_size_ + header_.size() + closing_.size();
17✔
529
        if (!clean_logs_) {
17✔
530
                ret += bad_data_msg_.size();
9✔
531
        }
532
        return ret;
17✔
533
}
534

535
ExpectedSize JsonLogMessagesReader::Read(
161✔
536
        vector<uint8_t>::iterator start, vector<uint8_t>::iterator end) {
537
        AssertOrReturnUnexpected(!sanitized_fpath_.empty());
161✔
538

539
        if (header_rem_ > 0) {
161✔
540
                io::Vsize target_size = end - start;
19✔
541
                auto copy_end = copy_n(
542
                        header_.begin() + (header_.size() - header_rem_), min(header_rem_, target_size), start);
20✔
543
                auto n_copied = copy_end - start;
544
                header_rem_ -= n_copied;
38✔
545
                return static_cast<size_t>(n_copied);
546
        } else if (!clean_logs_ && (bad_data_msg_rem_ > 0)) {
142✔
547
                io::Vsize target_size = end - start;
16✔
548
                auto copy_end = copy_n(
549
                        bad_data_msg_.begin() + (bad_data_msg_.size() - bad_data_msg_rem_),
16✔
550
                        min(bad_data_msg_rem_, target_size),
16✔
551
                        start);
16✔
552
                auto n_copied = copy_end - start;
553
                bad_data_msg_rem_ -= n_copied;
16✔
554
                return static_cast<size_t>(n_copied);
555
        } else if (rem_raw_data_size_ > 0) {
126✔
556
                if (end - start > rem_raw_data_size_) {
90✔
557
                        end = start + static_cast<size_t>(rem_raw_data_size_);
558
                }
559
                auto ex_sz = reader_->Read(start, end);
90✔
560
                if (!ex_sz) {
90✔
561
                        return ex_sz;
562
                }
563
                auto n_read = ex_sz.value();
90✔
564
                rem_raw_data_size_ -= n_read;
90✔
565

566
                // We control how much we read from the file so we should never read
567
                // 0 bytes (meaning EOF reached). If we do, it means the file is
568
                // smaller than what we were told.
569
                assert(n_read > 0);
570
                if (n_read == 0) {
90✔
571
                        return expected::unexpected(
×
572
                                MakeError(InvalidDataError, "Unexpected EOF when reading logs file"));
×
573
                }
574
                return n_read;
575
        } else if (closing_rem_ > 0) {
36✔
576
                io::Vsize target_size = end - start;
18✔
577
                auto copy_end = copy_n(
578
                        closing_.begin() + (closing_.size() - closing_rem_),
18✔
579
                        min(closing_rem_, target_size),
18✔
580
                        start);
18✔
581
                auto n_copied = copy_end - start;
582
                closing_rem_ -= n_copied;
18✔
583
                return static_cast<size_t>(copy_end - start);
584
        } else {
585
                return 0;
586
        }
587
};
588

589
static const string logs_uri_suffix = "/log";
590

591
error::Error DeploymentClient::PushLogs(
4✔
592
        const string &deployment_id,
593
        const string &log_file_path,
594
        api::Client &client,
595
        LogsAPIResponseHandler api_handler) {
596
        auto logs_reader = make_shared<JsonLogMessagesReader>(log_file_path);
4✔
597
        auto err = logs_reader->SanitizeLogs();
4✔
598
        if (err != error::NoError) {
4✔
599
                return err;
×
600
        }
601

602
        auto req = make_shared<api::APIRequest>();
4✔
603
        req->SetPath(http::JoinUrl(deployments_uri_prefix, deployment_id, logs_uri_suffix));
8✔
604
        req->SetMethod(http::Method::PUT);
4✔
605
        req->SetHeader("Content-Type", "application/json");
8✔
606
        req->SetHeader("Content-Length", to_string(logs_reader->TotalDataSize()));
8✔
607
        req->SetHeader("Accept", "application/json");
8✔
608
        req->SetBodyGenerator([logs_reader]() {
24✔
609
                logs_reader->Rewind();
8✔
610
                return logs_reader;
4✔
611
        });
8✔
612

613
        auto received_body = make_shared<vector<uint8_t>>();
4✔
614
        return client.AsyncCall(
615
                req,
616
                [received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
4✔
617
                        if (!exp_resp) {
4✔
618
                                log::Error("Request to push logs data failed: " + exp_resp.error().message);
×
619
                                api_handler(exp_resp.error());
×
620
                                return;
×
621
                        }
622

623
                        auto body_writer = make_shared<io::ByteWriter>(received_body);
4✔
624
                        auto resp = exp_resp.value();
4✔
625
                        auto content_length = resp->GetHeader("Content-Length");
8✔
626
                        if (!content_length) {
4✔
627
                                log::Debug(
×
628
                                        "Failed to get content length from the deployment log API response headers: "
629
                                        + content_length.error().String());
×
630
                                body_writer->SetUnlimited(true);
×
631
                        } else {
632
                                auto ex_len = common::StringTo<size_t>(content_length.value());
4✔
633
                                if (!ex_len) {
4✔
634
                                        log::Error(
×
635
                                                "Failed to convert the content length from the deployment log API response headers to an integer: "
636
                                                + ex_len.error().String());
×
637
                                        body_writer->SetUnlimited(true);
×
638
                                } else {
639
                                        received_body->resize(ex_len.value());
4✔
640
                                }
641
                        }
642
                        resp->SetBodyWriter(body_writer);
8✔
643
                },
644
                [received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
4✔
645
                        if (!exp_resp) {
4✔
646
                                log::Error("Request to push logs data failed: " + exp_resp.error().message);
×
647
                                api_handler(exp_resp.error());
×
648
                                return;
×
649
                        }
650

651
                        auto resp = exp_resp.value();
4✔
652
                        auto status = resp->GetStatusCode();
4✔
653

654
                        if (status == http::StatusNoContent) {
4✔
655
                                api_handler(error::NoError);
4✔
656
                        } else if (status == http::StatusRequestBodyTooLarge) {
2✔
657
                                // Don't retry if the request body is too large
658
                                api_handler(MakeError(
3✔
659
                                        RequestBodyTooLargeError,
660
                                        "Could not send logs to server: request body too large"));
2✔
661
                        } else {
662
                                auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
1✔
663
                                string err_str;
664
                                if (ex_err_msg) {
1✔
665
                                        err_str = ex_err_msg.value();
1✔
666
                                } else {
667
                                        err_str = resp->GetStatusMessage();
×
668
                                }
669
                                api_handler(MakeError(
2✔
670
                                        BadResponseError,
671
                                        "Got unexpected response " + to_string(status) + " from logs API: " + err_str));
2✔
672
                        }
673
                });
16✔
674
}
675
} // namespace deployments
676
} // namespace update
677
} // 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