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

mendersoftware / mender / 2378429566

11 Mar 2026 01:27PM UTC coverage: 81.7% (+0.02%) from 81.684%
2378429566

Pull #1913

gitlab-ci

mender-test-bot
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)
Pull Request #1913: [Cherry 5.1.x]: fix: Don't retry status update on 413

6 of 9 new or added lines in 3 files covered. (66.67%)

9005 of 11022 relevant lines covered (81.7%)

19911.0 hits per line

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

81.65
/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 {
33✔
60
        switch (code) {
33✔
61
        case NoError:
62
                return "Success";
×
63
        case InvalidDataError:
64
                return "Invalid data error";
×
65
        case BadResponseError:
66
                return "Bad response error";
4✔
67
        case DeploymentAbortedError:
68
                return "Deployment was aborted on the server";
3✔
69
        case TooManyRequestsError:
70
                return "Too many requests";
26✔
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) {
89✔
79
        return error::Error(error_condition(code, DeploymentsErrorCategory), msg);
104✔
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(
10✔
86
        context::MenderContext &ctx, api::Client &client, CheckUpdatesAPIResponseHandler api_handler) {
87
        auto ex_compatible_type = ctx.GetCompatibleType();
20✔
88
        if (!ex_compatible_type) {
10✔
89
                return ex_compatible_type.error();
4✔
90
        }
91
        string compatible_type = ex_compatible_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;
6✔
103
        ss << R"({"device_provides":{)";
6✔
104
        ss << R"("device_type":")";
6✔
105
        ss << json::EscapeString(compatible_type);
12✔
106

107
        for (const auto &kv : provides) {
14✔
108
                ss << "\",\"" + json::EscapeString(kv.first) + "\":\"";
8✔
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]() {
54✔
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"])
12✔
129
                                         + "&device_type=" + http::URLEncode(compatible_type);
18✔
130
        auto v1_req = make_shared<api::APIRequest>();
6✔
131
        v1_req->SetPath(check_updates_v1_uri + "?" + v1_args);
6✔
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(
×
144
                                        CheckUpdatesAPIResponseError {status, nullopt, ex_j.error()}));
×
145
                        }
146
                } else if (status == http::StatusNoContent) {
2✔
147
                        api_handler(CheckUpdatesAPIResponse {nullopt});
4✔
148
                } else {
149
                        log::Warning(
×
150
                                "DeploymentClient::CheckNewDeployments - received unhandled http response: "
151
                                + to_string(status));
×
152
                        api_handler(expected::unexpected(CheckUpdatesAPIResponseError {
×
153
                                status,
154
                                nullopt,
155
                                MakeError(
156
                                        DeploymentAbortedError,
157
                                        "received unhandled HTTP response: " + to_string(status))}));
×
158
                }
159
        };
10✔
160

161
        http::ResponseHandler header_handler =
162
                [this, received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
12✔
163
                        this->HeaderHandler(received_body, api_handler, exp_resp);
27✔
164
                };
15✔
165

166
        http::ResponseHandler v1_body_handler =
167
                [received_body, api_handler, handle_data](http::ExpectedIncomingResponsePtr exp_resp) {
15✔
168
                        if (!exp_resp) {
3✔
169
                                log::Error("Request to check new deployments failed: " + exp_resp.error().message);
×
170
                                CheckUpdatesAPIResponse response = expected::unexpected(
×
171
                                        CheckUpdatesAPIResponseError {nullopt, nullopt, exp_resp.error()});
×
172
                                api_handler(response);
×
173
                                return;
174
                        }
175
                        auto resp = exp_resp.value();
3✔
176
                        auto status = resp->GetStatusCode();
3✔
177

178
                        // StatusTooManyRequests must have been handled in HeaderHandler already
179
                        assert(status != http::StatusTooManyRequests);
180

181
                        if ((status == http::StatusOK) || (status == http::StatusNoContent)) {
3✔
182
                                handle_data(status);
2✔
183
                        } else {
184
                                auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
1✔
185
                                string err_str;
186
                                if (ex_err_msg) {
1✔
187
                                        err_str = ex_err_msg.value();
×
188
                                } else {
189
                                        err_str = resp->GetStatusMessage();
2✔
190
                                }
191
                                api_handler(expected::unexpected(CheckUpdatesAPIResponseError {
3✔
192
                                        status,
193
                                        nullopt,
194
                                        MakeError(
195
                                                BadResponseError,
196
                                                "Got unexpected response " + to_string(status) + ": " + err_str)}));
2✔
197
                        }
198
                };
6✔
199

200
        http::ResponseHandler v2_body_handler = [received_body,
18✔
201
                                                                                         v1_req,
202
                                                                                         header_handler,
203
                                                                                         v1_body_handler,
204
                                                                                         api_handler,
205
                                                                                         handle_data,
206
                                                                                         &client](http::ExpectedIncomingResponsePtr exp_resp) {
207
                if (!exp_resp) {
6✔
208
                        log::Error("Request to check new deployments failed: " + exp_resp.error().message);
×
209
                        CheckUpdatesAPIResponse response = expected::unexpected(
×
210
                                CheckUpdatesAPIResponseError {nullopt, nullopt, exp_resp.error()});
×
211
                        api_handler(response);
×
212
                        return;
213
                }
214
                auto resp = exp_resp.value();
6✔
215
                auto status = resp->GetStatusCode();
6✔
216

217
                // StatusTooManyRequests must have been handled in HeaderHandler already
218
                assert(status != http::StatusTooManyRequests);
219

220
                if ((status == http::StatusOK) || (status == http::StatusNoContent)) {
6✔
221
                        handle_data(status);
2✔
222
                } else if (status == http::StatusNotFound) {
4✔
223
                        log::Debug(
3✔
224
                                "POST request to v2 version of the deployments API failed, falling back to v1 version and GET");
225
                        auto err = client.AsyncCall(v1_req, header_handler, v1_body_handler);
9✔
226
                        if (err != error::NoError) {
3✔
227
                                api_handler(expected::unexpected(CheckUpdatesAPIResponseError {
×
228
                                        status, nullopt, err.WithContext("While calling v1 endpoint")}));
×
229
                        }
230
                } else {
231
                        auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
1✔
232
                        string err_str;
233
                        if (ex_err_msg) {
1✔
234
                                err_str = ex_err_msg.value();
1✔
235
                        } else {
236
                                err_str = resp->GetStatusMessage();
×
237
                        }
238
                        api_handler(expected::unexpected(CheckUpdatesAPIResponseError {
3✔
239
                                status,
240
                                nullopt,
241
                                MakeError(
242
                                        BadResponseError,
243
                                        "Got unexpected response " + to_string(status) + ": " + err_str)}));
2✔
244
                }
245
        };
6✔
246

247
        return client.AsyncCall(v2_req, header_handler, v2_body_handler);
18✔
248
}
12✔
249

250
void DeploymentClient::HeaderHandler(
12✔
251
        shared_ptr<vector<uint8_t>> received_body,
252
        CheckUpdatesAPIResponseHandler api_handler,
253
        http::ExpectedIncomingResponsePtr exp_resp) {
254
        if (!exp_resp) {
12✔
255
                log::Error("Request to check new deployments failed: " + exp_resp.error().message);
×
256
                CheckUpdatesAPIResponse response =
257
                        expected::unexpected(CheckUpdatesAPIResponseError {nullopt, nullopt, exp_resp.error()});
×
258
                api_handler(response);
×
259
                return;
260
        }
261

262
        auto resp = exp_resp.value();
12✔
263
        auto status = resp->GetStatusCode();
12✔
264
        if (status == http::StatusTooManyRequests) {
12✔
265
                CheckUpdatesAPIResponse response = expected::unexpected(CheckUpdatesAPIResponseError {
6✔
266
                        status, resp->GetHeaders(), MakeError(TooManyRequestsError, "Too many requests")});
9✔
267
                api_handler(response);
6✔
268
        }
269
        received_body->clear();
12✔
270
        auto body_writer = make_shared<io::ByteWriter>(received_body);
12✔
271
        body_writer->SetUnlimited(true);
12✔
272
        resp->SetBodyWriter(body_writer);
24✔
273
}
274

275
static const string deployment_status_strings[static_cast<int>(DeploymentStatus::End_) + 1] = {
276
        "installing",
277
        "pause_before_installing",
278
        "downloading",
279
        "pause_before_rebooting",
280
        "rebooting",
281
        "pause_before_committing",
282
        "success",
283
        "failure",
284
        "already-installed"};
285

286
static const string deployments_uri_prefix = "/api/devices/v1/deployments/device/deployments";
287
static const string status_uri_suffix = "/status";
288

289
string DeploymentStatusString(DeploymentStatus status) {
501✔
290
        return deployment_status_strings[static_cast<int>(status)];
505✔
291
}
292

293
error::Error DeploymentClient::PushStatus(
4✔
294
        const string &deployment_id,
295
        DeploymentStatus status,
296
        const string &substate,
297
        api::Client &client,
298
        StatusAPIResponseHandler api_handler) {
299
        // Cannot push a status update without a deployment ID
300
        AssertOrReturnError(deployment_id != "");
4✔
301
        string payload = R"({"status":")" + DeploymentStatusString(status) + "\"";
4✔
302
        if (substate != "") {
4✔
303
                payload += R"(,"substate":")" + json::EscapeString(substate) + "\"}";
6✔
304
        } else {
305
                payload += "}";
1✔
306
        }
307
        http::BodyGenerator payload_gen = [payload]() {
36✔
308
                return make_shared<io::StringReader>(payload);
4✔
309
        };
4✔
310

311
        auto req = make_shared<api::APIRequest>();
4✔
312
        req->SetPath(http::JoinUrl(deployments_uri_prefix, deployment_id, status_uri_suffix));
4✔
313
        req->SetMethod(http::Method::PUT);
4✔
314
        req->SetHeader("Content-Type", "application/json");
8✔
315
        req->SetHeader("Content-Length", to_string(payload.size()));
8✔
316
        req->SetHeader("Accept", "application/json");
8✔
317
        req->SetBodyGenerator(payload_gen);
4✔
318

319
        auto received_body = make_shared<vector<uint8_t>>();
4✔
320
        return client.AsyncCall(
16✔
321
                req,
322
                [this, received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
8✔
323
                        this->PushStatusHeaderHandler(received_body, api_handler, exp_resp);
12✔
324
                },
4✔
325
                [received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
12✔
326
                        if (!exp_resp) {
4✔
327
                                log::Error("Request to push status data failed: " + exp_resp.error().message);
×
328
                                api_handler(StatusAPIResponse {nullopt, nullopt, exp_resp.error()});
×
329
                                return;
×
330
                        }
331

332
                        auto resp = exp_resp.value();
4✔
333
                        auto status = resp->GetStatusCode();
4✔
334

335
                        // StatusTooManyRequests must have been handled in PushStatusHeaderHandler already
336
                        assert(status != http::StatusTooManyRequests);
337

338
                        if (status == http::StatusNoContent) {
4✔
339
                                api_handler(StatusAPIResponse {status, nullopt, error::NoError});
2✔
340
                        } else if (status == http::StatusConflict) {
2✔
341
                                api_handler(StatusAPIResponse {
2✔
342
                                        status,
343
                                        nullopt,
344
                                        MakeError(DeploymentAbortedError, "Could not send status update to server")});
2✔
345
                        } else {
346
                                auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
1✔
347
                                string err_str;
348
                                if (ex_err_msg) {
1✔
349
                                        err_str = ex_err_msg.value();
1✔
350
                                } else {
351
                                        err_str = resp->GetStatusMessage();
×
352
                                }
353
                                api_handler(StatusAPIResponse {
2✔
354
                                        status,
355
                                        nullopt,
356
                                        MakeError(
357
                                                BadResponseError,
358
                                                "Got unexpected response " + to_string(status)
1✔
359
                                                        + " from status API: " + err_str)});
2✔
360
                        }
361
                });
4✔
362
}
363

364
void DeploymentClient::PushStatusHeaderHandler(
7✔
365
        shared_ptr<vector<uint8_t>> received_body,
366
        StatusAPIResponseHandler api_handler,
367
        http::ExpectedIncomingResponsePtr exp_resp) {
368
        if (!exp_resp) {
7✔
369
                log::Error("Request to push status data failed: " + exp_resp.error().message);
×
370
                api_handler(StatusAPIResponse {nullopt, nullopt, exp_resp.error()});
×
371
                return;
×
372
        }
373

374
        auto body_writer = make_shared<io::ByteWriter>(received_body);
7✔
375
        auto resp = exp_resp.value();
7✔
376
        auto status = resp->GetStatusCode();
7✔
377
        if (status == http::StatusTooManyRequests) {
7✔
378
                StatusAPIResponse response = {
379
                        status, resp->GetHeaders(), MakeError(TooManyRequestsError, "Too many requests")};
3✔
380
                api_handler(response);
3✔
381
        }
3✔
382
        auto content_length = resp->GetHeader("Content-Length");
14✔
383
        if (!content_length) {
7✔
384
                log::Debug(
3✔
385
                        "Failed to get content length from the deployment status API response headers: "
386
                        + content_length.error().String());
6✔
387
                body_writer->SetUnlimited(true);
3✔
388
        } else {
389
                auto ex_len = common::StringTo<size_t>(content_length.value());
4✔
390
                if (!ex_len) {
4✔
391
                        log::Error(
×
392
                                "Failed to convert the content length from the deployment status API response headers to an integer: "
393
                                + ex_len.error().String());
×
394
                        body_writer->SetUnlimited(true);
×
395
                } else {
396
                        received_body->resize(ex_len.value());
4✔
397
                }
398
        }
399
        resp->SetBodyWriter(body_writer);
14✔
400
}
401

402
using mender::common::expected::ExpectedSize;
403

404
static ExpectedSize GetLogFileDataSize(const string &path) {
20✔
405
        auto ex_istr = io::OpenIfstream(path);
20✔
406
        if (!ex_istr) {
20✔
407
                return expected::unexpected(ex_istr.error());
×
408
        }
409
        auto istr = std::move(ex_istr.value());
20✔
410

411
        // We want the size of the actual data without a potential trailing
412
        // comma. So let's seek one byte before the end of file, check if the last
413
        // byte is a comma and return the appropriate number.
414
        istr.seekg(-1, ios_base::end);
20✔
415
        int c = istr.get();
20✔
416
        if (c == ',') {
20✔
417
                return istr.tellg() - static_cast<ifstream::off_type>(1);
20✔
418
        } else {
419
                return istr.tellg();
×
420
        }
421
}
20✔
422

423
const vector<uint8_t> JsonLogMessagesReader::header_ = {
424
        '{', '"', 'm', 'e', 's', 's', 'a', 'g', 'e', 's', '"', ':', '['};
425
const vector<uint8_t> JsonLogMessagesReader::closing_ = {']', '}'};
426
const string JsonLogMessagesReader::default_tstamp_ = "1970-01-01T00:00:00.000000000Z";
427
const string JsonLogMessagesReader::bad_data_msg_tmpl_ =
428
        R"d({"timestamp": "1970-01-01T00:00:00.000000000Z", "level": "ERROR", "message": "(THE ORIGINAL LOGS CONTAINED INVALID ENTRIES)"},)d";
429

430
JsonLogMessagesReader::~JsonLogMessagesReader() {
45✔
431
        reader_.reset();
432
        if (!sanitized_fpath_.empty() && path::FileExists(sanitized_fpath_)) {
15✔
433
                auto del_err = path::FileDelete(sanitized_fpath_);
15✔
434
                if (del_err != error::NoError) {
15✔
435
                        log::Error("Failed to delete auxiliary logs file: " + del_err.String());
×
436
                }
437
        }
438
        sanitized_fpath_.erase();
15✔
439
}
30✔
440

441
static error::Error DoSanitizeLogs(
20✔
442
        const string &orig_path, const string &new_path, bool &all_valid, string &first_tstamp) {
443
        auto ex_ifs = io::OpenIfstream(orig_path);
20✔
444
        if (!ex_ifs) {
20✔
445
                return ex_ifs.error();
×
446
        }
447
        auto ex_ofs = io::OpenOfstream(new_path);
20✔
448
        if (!ex_ofs) {
20✔
449
                return ex_ofs.error();
×
450
        }
451
        auto &ifs = ex_ifs.value();
20✔
452
        auto &ofs = ex_ofs.value();
20✔
453

454
        string last_known_tstamp = first_tstamp;
20✔
455
        const string tstamp_prefix_data = R"d({"timestamp": ")d";
20✔
456
        const string corrupt_msg_suffix_data =
457
                R"d(", "level": "ERROR", "message": "(CORRUPTED LOG DATA)"},)d";
20✔
458

459
        string line;
460
        first_tstamp.erase();
20✔
461
        all_valid = true;
20✔
462
        error::Error err;
20✔
463
        while (!ifs.eof()) {
92✔
464
                getline(ifs, line);
72✔
465
                if (!ifs.eof() && !ifs) {
72✔
466
                        int io_errno = errno;
×
467
                        return error::Error(
468
                                generic_category().default_error_condition(io_errno),
×
469
                                "Failed to get line from deployment logs file '" + orig_path
×
470
                                        + "': " + strerror(io_errno));
×
471
                }
472
                if (line.empty()) {
72✔
473
                        // skip empty lines
474
                        continue;
20✔
475
                }
476
                auto ex_json = json::Load(line);
104✔
477
                if (ex_json) {
52✔
478
                        // valid JSON log line, just replace the newline after it with a comma and save the
479
                        // timestamp for later
480
                        auto ex_tstamp = ex_json.value().Get("timestamp").and_then(json::ToString);
86✔
481
                        if (ex_tstamp) {
43✔
482
                                if (first_tstamp.empty()) {
43✔
483
                                        first_tstamp = ex_tstamp.value();
19✔
484
                                }
485
                                last_known_tstamp = std::move(ex_tstamp.value());
43✔
486
                        }
487
                        line.append(1, ',');
43✔
488
                        err = io::WriteStringIntoOfstream(ofs, line);
43✔
489
                        if (err != error::NoError) {
43✔
490
                                return err.WithContext("Failed to write pre-processed deployment logs data");
×
491
                        }
492
                } else {
493
                        all_valid = false;
9✔
494
                        if (first_tstamp.empty()) {
9✔
495
                                // If we still don't have the first valid tstamp, we need to
496
                                // save the last known one (potentially pre-set) as the first
497
                                // one.
498
                                first_tstamp = last_known_tstamp;
499
                        }
500
                        err = io::WriteStringIntoOfstream(
9✔
501
                                ofs, tstamp_prefix_data + last_known_tstamp + corrupt_msg_suffix_data);
18✔
502
                        if (err != error::NoError) {
9✔
503
                                return err.WithContext("Failed to write pre-processed deployment logs data");
×
504
                        }
505
                }
506
        }
507
        return error::NoError;
20✔
508
}
509

510
error::Error JsonLogMessagesReader::SanitizeLogs() {
20✔
511
        if (!sanitized_fpath_.empty()) {
20✔
512
                return error::NoError;
×
513
        }
514

515
        string prep_fpath = log_fpath_ + ".sanitized";
20✔
516
        string first_tstamp = default_tstamp_;
20✔
517
        auto err = DoSanitizeLogs(log_fpath_, prep_fpath, clean_logs_, first_tstamp);
20✔
518
        if (err != error::NoError) {
20✔
519
                if (path::FileExists(prep_fpath)) {
×
520
                        auto del_err = path::FileDelete(prep_fpath);
×
521
                        if (del_err != error::NoError) {
×
522
                                log::Error("Failed to delete auxiliary logs file: " + del_err.String());
×
523
                        }
524
                }
525
        } else {
526
                sanitized_fpath_ = std::move(prep_fpath);
20✔
527
                reader_ = make_unique<io::FileReader>(sanitized_fpath_);
40✔
528
                auto ex_sz = GetLogFileDataSize(sanitized_fpath_);
20✔
529
                if (!ex_sz) {
20✔
530
                        return ex_sz.error().WithContext("Failed to determine deployment logs size");
×
531
                }
532
                raw_data_size_ = ex_sz.value();
20✔
533
                rem_raw_data_size_ = raw_data_size_;
20✔
534
                if (!clean_logs_) {
20✔
535
                        auto bad_data_msg_tstamp_start =
536
                                bad_data_msg_.begin() + 15; // len(R"({"timestamp": ")")
537
                        copy_n(first_tstamp.cbegin(), first_tstamp.size(), bad_data_msg_tstamp_start);
7✔
538
                }
539
        }
540
        return err;
20✔
541
}
542

543
error::Error JsonLogMessagesReader::Rewind() {
5✔
544
        AssertOrReturnError(!sanitized_fpath_.empty());
5✔
545
        header_rem_ = header_.size();
5✔
546
        closing_rem_ = closing_.size();
5✔
547
        bad_data_msg_rem_ = bad_data_msg_.size();
5✔
548

549
        // release/close the file first so that the FileDelete() below can actually
550
        // delete it and free space up
551
        reader_.reset();
552
        auto del_err = path::FileDelete(sanitized_fpath_);
5✔
553
        if (del_err != error::NoError) {
5✔
554
                log::Error("Failed to delete auxiliary logs file: " + del_err.String());
2✔
555
        }
556
        sanitized_fpath_.erase();
5✔
557
        return SanitizeLogs();
5✔
558
}
559

560
int64_t JsonLogMessagesReader::TotalDataSize() {
15✔
561
        assert(!sanitized_fpath_.empty());
562

563
        auto ret = raw_data_size_ + header_.size() + closing_.size();
15✔
564
        if (!clean_logs_) {
15✔
565
                ret += bad_data_msg_.size();
7✔
566
        }
567
        return ret;
15✔
568
}
569

570
ExpectedSize JsonLogMessagesReader::Read(
151✔
571
        vector<uint8_t>::iterator start, vector<uint8_t>::iterator end) {
572
        AssertOrReturnUnexpected(!sanitized_fpath_.empty());
151✔
573

574
        if (header_rem_ > 0) {
151✔
575
                io::Vsize target_size = end - start;
17✔
576
                auto copy_end = copy_n(
17✔
577
                        header_.begin() + (header_.size() - header_rem_), min(header_rem_, target_size), start);
17✔
578
                auto n_copied = copy_end - start;
579
                header_rem_ -= n_copied;
17✔
580
                return static_cast<size_t>(n_copied);
581
        } else if (!clean_logs_ && (bad_data_msg_rem_ > 0)) {
134✔
582
                io::Vsize target_size = end - start;
14✔
583
                auto copy_end = copy_n(
14✔
584
                        bad_data_msg_.begin() + (bad_data_msg_.size() - bad_data_msg_rem_),
14✔
585
                        min(bad_data_msg_rem_, target_size),
586
                        start);
587
                auto n_copied = copy_end - start;
588
                bad_data_msg_rem_ -= n_copied;
14✔
589
                return static_cast<size_t>(n_copied);
590
        } else if (rem_raw_data_size_ > 0) {
120✔
591
                if (end - start > rem_raw_data_size_) {
88✔
592
                        end = start + static_cast<size_t>(rem_raw_data_size_);
593
                }
594
                auto ex_sz = reader_->Read(start, end);
88✔
595
                if (!ex_sz) {
88✔
596
                        return ex_sz;
597
                }
598
                auto n_read = ex_sz.value();
88✔
599
                rem_raw_data_size_ -= n_read;
88✔
600

601
                // We control how much we read from the file so we should never read
602
                // 0 bytes (meaning EOF reached). If we do, it means the file is
603
                // smaller than what we were told.
604
                assert(n_read > 0);
605
                if (n_read == 0) {
88✔
606
                        return expected::unexpected(
×
607
                                MakeError(InvalidDataError, "Unexpected EOF when reading logs file"));
×
608
                }
609
                return n_read;
610
        } else if (closing_rem_ > 0) {
32✔
611
                io::Vsize target_size = end - start;
16✔
612
                auto copy_end = copy_n(
16✔
613
                        closing_.begin() + (closing_.size() - closing_rem_),
16✔
614
                        min(closing_rem_, target_size),
615
                        start);
616
                auto n_copied = copy_end - start;
617
                closing_rem_ -= n_copied;
16✔
618
                return static_cast<size_t>(copy_end - start);
619
        } else {
620
                return 0;
621
        }
622
};
623

624
static const string logs_uri_suffix = "/log";
625

626
error::Error DeploymentClient::PushLogs(
4✔
627
        const string &deployment_id,
628
        const string &log_file_path,
629
        api::Client &client,
630
        LogsAPIResponseHandler api_handler) {
631
        auto logs_reader = make_shared<JsonLogMessagesReader>(log_file_path);
4✔
632
        auto err = logs_reader->SanitizeLogs();
4✔
633
        if (err != error::NoError) {
4✔
634
                return err;
×
635
        }
636

637
        auto req = make_shared<api::APIRequest>();
4✔
638
        req->SetPath(http::JoinUrl(deployments_uri_prefix, deployment_id, logs_uri_suffix));
4✔
639
        req->SetMethod(http::Method::PUT);
4✔
640
        req->SetHeader("Content-Type", "application/json");
8✔
641
        req->SetHeader("Content-Length", to_string(logs_reader->TotalDataSize()));
8✔
642
        req->SetHeader("Accept", "application/json");
8✔
643
        req->SetBodyGenerator([logs_reader]() {
20✔
644
                logs_reader->Rewind();
8✔
645
                return logs_reader;
4✔
646
        });
647

648
        auto received_body = make_shared<vector<uint8_t>>();
4✔
649
        return client.AsyncCall(
16✔
650
                req,
651
                [this, received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
8✔
652
                        this->PushLogsHeaderHandler(received_body, api_handler, exp_resp);
12✔
653
                },
4✔
654
                [received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
11✔
655
                        if (!exp_resp) {
3✔
656
                                log::Error("Request to push logs data failed: " + exp_resp.error().message);
×
657
                                api_handler(LogsAPIResponse {nullopt, nullopt, exp_resp.error()});
×
658
                                return;
×
659
                        }
660

661
                        auto resp = exp_resp.value();
3✔
662
                        auto status = resp->GetStatusCode();
3✔
663

664
                        // StatusTooManyRequests must have been handled in PushLogsHeaderHandler already
665
                        assert(status != http::StatusTooManyRequests);
666
                        // StatusRequestBodyTooLarge must have been handled in PushLogsHeaderHandler already
667
                        assert(status != http::StatusRequestBodyTooLarge);
668

669
                        if (status == http::StatusNoContent) {
3✔
670
                                api_handler(LogsAPIResponse {status, nullopt, error::NoError});
2✔
671
                        } else {
672
                                auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
1✔
673
                                string err_str;
674
                                if (ex_err_msg) {
1✔
675
                                        err_str = ex_err_msg.value();
1✔
676
                                } else {
677
                                        err_str = resp->GetStatusMessage();
×
678
                                }
679
                                api_handler(LogsAPIResponse {
2✔
680
                                        status,
681
                                        nullopt,
682
                                        MakeError(
683
                                                BadResponseError,
684
                                                "Got unexpected response " + to_string(status)
1✔
685
                                                        + " from logs API: " + err_str)});
2✔
686
                        }
687
                });
4✔
688
}
689

690
void DeploymentClient::PushLogsHeaderHandler(
7✔
691
        shared_ptr<vector<uint8_t>> received_body,
692
        LogsAPIResponseHandler api_handler,
693
        http::ExpectedIncomingResponsePtr exp_resp) {
694
        if (!exp_resp) {
7✔
695
                log::Error("Request to push logs data failed: " + exp_resp.error().message);
×
696
                api_handler(LogsAPIResponse {nullopt, nullopt, exp_resp.error()});
×
697
                return;
×
698
        }
699

700
        auto body_writer = make_shared<io::ByteWriter>(received_body);
7✔
701
        auto resp = exp_resp.value();
7✔
702
        auto status = resp->GetStatusCode();
7✔
703
        if (status == http::StatusTooManyRequests) {
7✔
704
                LogsAPIResponse response = {
705
                        status, resp->GetHeaders(), MakeError(TooManyRequestsError, "Too many requests")};
3✔
706
                api_handler(response);
3✔
707
        } else if (status == http::StatusRequestBodyTooLarge) {
7✔
708
                LogsAPIResponse response = {
709
                        status,
710
                        resp->GetHeaders(),
711
                        MakeError(RequestBodyTooLargeError, "Could not send logs to server")};
1✔
712
                api_handler(response);
1✔
713
        }
1✔
714
        auto content_length = resp->GetHeader("Content-Length");
14✔
715
        if (!content_length) {
7✔
716
                log::Debug(
3✔
717
                        "Failed to get content length from the deployment log API response headers: "
718
                        + content_length.error().String());
6✔
719
                body_writer->SetUnlimited(true);
3✔
720
        } else {
721
                auto ex_len = common::StringTo<size_t>(content_length.value());
4✔
722
                if (!ex_len) {
4✔
723
                        log::Error(
×
724
                                "Failed to convert the content length from the deployment log API response headers to an integer: "
725
                                + ex_len.error().String());
×
726
                        body_writer->SetUnlimited(true);
×
727
                } else {
728
                        received_body->resize(ex_len.value());
4✔
729
                }
730
        }
731
        resp->SetBodyWriter(body_writer);
14✔
732
}
733

734
} // namespace deployments
735
} // namespace update
736
} // 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