MockWebServer.cpp (25565B)
1 // Copyright (c) Microsoft Corporation. 2 // Licensed under the MIT License. 3 4 #include "MockWebServer.h" 5 6 #include "../util/TestHelper.h" 7 #include "ErrorHandling.h" 8 #include "ServerCommon.h" 9 #include "Util.h" 10 #include "connection/HttpHeader.h" 11 12 #ifdef _WIN32 13 #define WIN32_LEAN_AND_MEAN 14 #include <windows.h> 15 #endif 16 17 #include <nlohmann/json.hpp> 18 19 #include <chrono> 20 #include <mutex> 21 #include <optional> 22 #include <thread> 23 #include <unordered_map> 24 25 using namespace SFS; 26 using namespace SFS::details; 27 using namespace SFS::details::util; 28 using namespace SFS::test; 29 using namespace SFS::test::details; 30 using json = nlohmann::json; 31 32 namespace 33 { 34 35 struct App 36 { 37 std::string version; 38 std::vector<MockPrerequisite> prerequisites; 39 }; 40 41 struct AppCmp 42 { 43 bool operator()(App a, App b) const 44 { 45 return a.version < b.version; 46 } 47 }; 48 49 json GenerateContentIdJsonObject(const std::string& name, const std::string& latestVersion, const std::string& ns) 50 { 51 // { 52 // "ContentId": { 53 // "Namespace": <ns>, 54 // "Name": <name>, 55 // "Version": <version> 56 // } 57 // } 58 59 return {{"ContentId", {{"Namespace", ns}, {"Name", name}, {"Version", latestVersion}}}}; 60 } 61 62 json GenerateGetAppVersionJsonObject(const std::string& name, const App& app, const std::string& ns) 63 { 64 // { 65 // "ContentId": { 66 // "Namespace": <ns>, 67 // "Name": <name>, 68 // "Version": <version> 69 // }, 70 // "UpdateId": "<id>", 71 // "Prerequisites": [ 72 // { 73 // "Namespace": "<ns>", 74 // "Name": "<name>", 75 // "Version": "<version>" 76 // } 77 // ] 78 // } 79 80 json prereqs = json::array(); 81 for (const auto& prereq : app.prerequisites) 82 { 83 prereqs.push_back({{"Namespace", ns}, {"Name", prereq.name}, {"Version", prereq.version}}); 84 } 85 return {{"ContentId", {{"Namespace", ns}, {"Name", name}, {"Version", app.version}}}, 86 {"UpdateId", "123"}, 87 {"Prerequisites", prereqs}}; 88 } 89 90 json GeneratePostDownloadInfo(const std::string& name) 91 { 92 // [ 93 // { 94 // "Url": <url>, 95 // "FileId": <fileid>, 96 // "SizeInBytes": <size>, 97 // "Hashes": { 98 // "Sha1": <sha1>, 99 // "Sha256": <sha2> 100 // }, 101 // "DeliveryOptimization": { 102 // "CatalogId": <catalogid>, 103 // "Properties": { 104 // "IntegrityCheckInfo": { 105 // "PiecesHashFileUrl": <url>, 106 // "HashOfHashes": <hash> 107 // } 108 // } 109 // } 110 // }, 111 // ... 112 // ] 113 114 // Generating DeliveryOptimizationData to simulate the server response, but it's not being parsed by the Client 115 116 json response; 117 response = json::array(); 118 response.push_back({{"Url", "http://localhost/1.json"}, 119 {"FileId", name + ".json"}, 120 {"SizeInBytes", 100}, 121 {"Hashes", {{"Sha1", "123"}, {"Sha256", "456"}}}}); 122 response[0]["DeliveryOptimization"] = {{"CatalogId", "789"}}; 123 response[0]["DeliveryOptimization"]["Properties"] = { 124 {"IntegrityCheckInfo", {{"PiecesHashFileUrl", "http://localhost/1.json"}, {"HashOfHashes", "abc"}}}}; 125 126 response.push_back({{"Url", "http://localhost/2.bin"}, 127 {"FileId", name + ".bin"}, 128 {"SizeInBytes", 200}, 129 {"Hashes", {{"Sha1", "421"}, {"Sha256", "132"}}}}); 130 response[1]["DeliveryOptimization"] = {{"CatalogId", "14"}}; 131 response[1]["DeliveryOptimization"]["Properties"] = { 132 {"IntegrityCheckInfo", {{"PiecesHashFileUrl", "http://localhost/2.bin"}, {"HashOfHashes", "abcd"}}}}; 133 return response; 134 } 135 136 json GeneratePostAppDownloadInfo(const std::string& name) 137 { 138 // [ 139 // { 140 // "Url": <url>, 141 // "FileId": <fileid>, 142 // "SizeInBytes": <size>, 143 // "Hashes": { 144 // "Sha1": <sha1>, 145 // "Sha256": <sha2> 146 // }, 147 // "DeliveryOptimization": { 148 // "CatalogId": <catalogid>, 149 // "Properties": { 150 // "IntegrityCheckInfo": { 151 // "PiecesHashFileUrl": <url>, 152 // "HashOfHashes": <hash> 153 // } 154 // } 155 // }, 156 // "ApplicabilityDetails": { 157 // "Architectures": [ 158 // "<arch>" 159 // ], 160 // "PlatformApplicabilityForPackage": [ 161 // "<app>" 162 // ] 163 // }, 164 // "FileMoniker": "<moniker>" 165 // } 166 // ] 167 168 // Generating DeliveryOptimizationData to simulate the server response, but it's not being parsed by the Client 169 170 json response; 171 response = json::array(); 172 response.push_back({{"Url", "http://localhost/1.json"}, 173 {"FileId", name + ".json"}, 174 {"SizeInBytes", 100}, 175 {"Hashes", {{"Sha1", "123"}, {"Sha256", "456"}}}}); 176 response[0]["DeliveryOptimization"] = {{"CatalogId", "789"}}; 177 response[0]["DeliveryOptimization"]["Properties"] = { 178 {"IntegrityCheckInfo", {{"PiecesHashFileUrl", "http://localhost/1.json"}, {"HashOfHashes", "abc"}}}}; 179 response[0]["ApplicabilityDetails"] = {{"Architectures", {"x86"}}, 180 {"PlatformApplicabilityForPackage", {"Windows"}}}; 181 response[0]["FileMoniker"] = "1.json"; 182 183 response.push_back({{"Url", "http://localhost/2.bin"}, 184 {"FileId", name + ".bin"}, 185 {"SizeInBytes", 200}, 186 {"Hashes", {{"Sha1", "421"}, {"Sha256", "132"}}}}); 187 response[1]["DeliveryOptimization"] = {{"CatalogId", "14"}}; 188 response[1]["DeliveryOptimization"]["Properties"] = { 189 {"IntegrityCheckInfo", {{"PiecesHashFileUrl", "http://localhost/2.bin"}, {"HashOfHashes", "abcd"}}}}; 190 response[1]["ApplicabilityDetails"] = {{"Architectures", {"amd64"}}, 191 {"PlatformApplicabilityForPackage", {"Linux"}}}; 192 response[1]["FileMoniker"] = "2.bin"; 193 194 return response; 195 } 196 197 void CheckApiVersion(const httplib::Request& req, std::string_view apiVersion) 198 { 199 if (util::AreNotEqualI(req.path_params.at("apiVersion"), apiVersion)) 200 { 201 throw StatusCodeException(httplib::StatusCode::NotFound_404); 202 } 203 } 204 } // namespace 205 206 namespace SFS::test::details 207 { 208 class MockWebServerImpl : public BaseServerImpl 209 { 210 public: 211 MockWebServerImpl() = default; 212 ~MockWebServerImpl() = default; 213 214 MockWebServerImpl(const MockWebServerImpl&) = delete; 215 MockWebServerImpl& operator=(const MockWebServerImpl&) = delete; 216 217 void RegisterProduct(std::string&& name, std::string&& version); 218 void RegisterAppProduct(std::string&& name, std::string&& version, std::vector<MockPrerequisite>&& prerequisites); 219 void RegisterExpectedRequestHeader(std::string&& header, std::string&& value); 220 void SetForcedHttpErrors(std::queue<HttpCode> forcedErrors); 221 void SetResponseHeaders(std::unordered_map<HttpCode, HeaderMap> headersByCode); 222 223 private: 224 void ConfigureRequestHandlers() override; 225 std::string GetLogIdentifier() override; 226 227 void ConfigurePostLatestVersion(); 228 void ConfigurePostLatestVersionBatch(); 229 void ConfigureGetSpecificVersion(); 230 void ConfigurePostDownloadInfo(); 231 232 void RunHttpCallback(const httplib::Request& req, 233 httplib::Response& res, 234 const std::string& methodName, 235 const std::string& apiVersion, 236 const std::function<void(const httplib::Request, httplib::Response&)>& callback); 237 void CheckRequestHeaders(const httplib::Request& req); 238 239 using VersionList = std::set<std::string>; 240 std::unordered_map<std::string, VersionList> m_products; 241 242 using AppList = std::set<App, AppCmp>; 243 std::unordered_map<std::string, AppList> m_appProducts; 244 245 std::unordered_map<std::string, std::string> m_expectedRequestHeaders; 246 std::queue<HttpCode> m_forcedHttpErrors; 247 std::unordered_map<HttpCode, HeaderMap> m_headersByCode; 248 }; 249 } // namespace SFS::test::details 250 251 MockWebServer::MockWebServer() 252 { 253 m_impl = std::make_unique<MockWebServerImpl>(); 254 m_impl->Start(); 255 } 256 257 MockWebServer::~MockWebServer() 258 { 259 const auto ret = Stop(); 260 if (!ret) 261 { 262 TEST_UNSCOPED_INFO("Failed to stop: " + std::string(ToString(ret.GetCode()))); 263 } 264 } 265 266 Result MockWebServer::Stop() 267 { 268 return m_impl->Stop(); 269 } 270 271 std::string MockWebServer::GetBaseUrl() const 272 { 273 return m_impl->GetUrl(); 274 } 275 276 void MockWebServer::RegisterProduct(std::string name, std::string version) 277 { 278 m_impl->RegisterProduct(std::move(name), std::move(version)); 279 } 280 281 void MockWebServer::RegisterAppProduct(std::string name, 282 std::string version, 283 std::vector<MockPrerequisite> prerequisites) 284 { 285 m_impl->RegisterAppProduct(std::move(name), std::move(version), std::move(prerequisites)); 286 } 287 288 void MockWebServer::RegisterExpectedRequestHeader(HttpHeader header, std::string value) 289 { 290 std::string headerName = ToString(header); 291 m_impl->RegisterExpectedRequestHeader(std::move(headerName), std::move(value)); 292 } 293 294 void MockWebServer::SetForcedHttpErrors(std::queue<HttpCode> forcedErrors) 295 { 296 m_impl->SetForcedHttpErrors(std::move(forcedErrors)); 297 } 298 299 void MockWebServer::SetResponseHeaders(std::unordered_map<HttpCode, HeaderMap> headersByCode) 300 { 301 m_impl->SetResponseHeaders(std::move(headersByCode)); 302 } 303 304 void MockWebServerImpl::ConfigureRequestHandlers() 305 { 306 ConfigurePostLatestVersion(); 307 ConfigurePostLatestVersionBatch(); 308 ConfigureGetSpecificVersion(); 309 ConfigurePostDownloadInfo(); 310 } 311 312 std::string MockWebServerImpl::GetLogIdentifier() 313 { 314 return "MockWebServer"; 315 } 316 317 void MockWebServerImpl::ConfigurePostLatestVersion() 318 { 319 // Path: /api/<apiVersion:v2>/contents/<instanceId>/namespaces/<ns>/names/<name>/versions/latest?action=select 320 const std::string pattern = "/api/:apiVersion/contents/:instanceId/namespaces/:ns/names/:name/versions/latest"; 321 m_server.Post(pattern, [&](const httplib::Request& req, httplib::Response& res) { 322 RunHttpCallback(req, res, "PostLatestVersion", "v2", [&](const httplib::Request& req, httplib::Response& res) { 323 // TODO: Ignoring instanceId for now 324 325 if (!req.has_param("action") || util::AreNotEqualI(req.get_param_value("action"), "select")) 326 { 327 // TODO: SFS might throw a different error when the query string is unexpected 328 throw StatusCodeException(httplib::StatusCode::NotFound_404); 329 } 330 331 // Checking body has expected format, but won't use it for the response 332 { 333 if (req.body.empty()) 334 { 335 throw StatusCodeException(httplib::StatusCode::BadRequest_400); 336 } 337 338 json body; 339 try 340 { 341 body = json::parse(req.body); 342 } 343 catch (const json::parse_error& ex) 344 { 345 BUFFER_LOG("JSON parse error: " + std::string(ex.what())); 346 throw StatusCodeException(httplib::StatusCode::BadRequest_400); 347 } 348 349 // The GetLatestVersion API expects an object as a body, with a "TargetingAttributes" object element. 350 if (!body.is_object() || !body.contains("TargetingAttributes") || 351 !body["TargetingAttributes"].is_object()) 352 { 353 throw StatusCodeException(httplib::StatusCode::BadRequest_400); 354 } 355 } 356 357 const std::string ns = req.path_params.at("ns"); 358 359 json response; 360 const std::string& name = req.path_params.at("name"); 361 if (auto it = m_products.find(name); it != m_products.end()) 362 { 363 const auto& versions = it->second; 364 if (versions.empty()) 365 { 366 throw StatusCodeException(httplib::StatusCode::InternalServerError_500); 367 } 368 369 const auto& latestVersion = *versions.rbegin(); 370 response = GenerateContentIdJsonObject(name, latestVersion, ns); 371 } 372 else if (auto appIt = m_appProducts.find(name); appIt != m_appProducts.end()) 373 { 374 const auto& appList = appIt->second; 375 if (appList.empty()) 376 { 377 throw StatusCodeException(httplib::StatusCode::InternalServerError_500); 378 } 379 380 const auto& latestApp = *appList.rbegin(); 381 response = GenerateGetAppVersionJsonObject(name, latestApp, ns); 382 } 383 else 384 { 385 throw StatusCodeException(httplib::StatusCode::NotFound_404); 386 } 387 388 res.set_content(response.dump(), "application/json"); 389 }); 390 }); 391 } 392 393 void MockWebServerImpl::ConfigurePostLatestVersionBatch() 394 { 395 // Path: /api/<apiVersion:v2>/contents/<instanceId>/namespaces/<ns>/names?action=BatchUpdates 396 const std::string pattern = "/api/:apiVersion/contents/:instanceId/namespaces/:ns/names"; 397 m_server.Post(pattern, [&](const httplib::Request& req, httplib::Response& res) { 398 RunHttpCallback( 399 req, 400 res, 401 "PostLatestVersionBatch", 402 "v2", 403 [&](const httplib::Request& req, httplib::Response& res) { 404 // TODO: Ignoring instanceId for now 405 406 if (!req.has_param("action") || util::AreNotEqualI(req.get_param_value("action"), "BatchUpdates")) 407 { 408 // TODO: SFS might throw a different error when the query string is unexpected 409 throw StatusCodeException(httplib::StatusCode::NotFound_404); 410 } 411 412 if (req.body.empty()) 413 { 414 throw StatusCodeException(httplib::StatusCode::BadRequest_400); 415 } 416 417 json body; 418 try 419 { 420 body = json::parse(req.body); 421 } 422 catch (const json::parse_error& ex) 423 { 424 BUFFER_LOG("JSON parse error: " + std::string(ex.what())); 425 throw StatusCodeException(httplib::StatusCode::BadRequest_400); 426 } 427 428 // The BatchUpdates API returns an array of objects, each with a "Product" key. 429 // If repeated, the same product is only returned once. 430 // TODO: We are ignoring the TargetingAttributes for now. 431 if (!body.is_array()) 432 { 433 throw StatusCodeException(httplib::StatusCode::BadRequest_400); 434 } 435 436 // Iterate over the array and collect the unique products 437 std::unordered_map<std::string, json> requestedProducts; 438 for (const auto& productRequest : body) 439 { 440 if (!productRequest.is_object() || !productRequest.contains("Product") || 441 !productRequest["Product"].is_string() || !productRequest.contains("TargetingAttributes")) 442 { 443 throw StatusCodeException(httplib::StatusCode::BadRequest_400); 444 } 445 if (requestedProducts.count(productRequest["Product"])) 446 { 447 continue; 448 } 449 requestedProducts.emplace(productRequest["Product"], productRequest["TargetingAttributes"]); 450 } 451 452 // If at least one product exists, we will return a 200 OK with that. Non-existing products are ignored. 453 // Otherwise, a 404 is sent. 454 json response = json::array(); 455 for (const auto& [name, _] : requestedProducts) 456 { 457 auto it = m_products.find(name); 458 if (it == m_products.end()) 459 { 460 continue; 461 } 462 463 const VersionList& versions = it->second; 464 if (versions.empty()) 465 { 466 throw StatusCodeException(httplib::StatusCode::InternalServerError_500); 467 } 468 469 const std::string ns = req.path_params.at("ns"); 470 const auto& latestVersion = *versions.rbegin(); 471 472 response.push_back(GenerateContentIdJsonObject(name, latestVersion, ns)); 473 } 474 475 if (response.empty()) 476 { 477 throw StatusCodeException(httplib::StatusCode::NotFound_404); 478 } 479 480 res.set_content(response.dump(), "application/json"); 481 }); 482 }); 483 } 484 485 void MockWebServerImpl::ConfigureGetSpecificVersion() 486 { 487 // Path: /api/<apiVersion:v2>/contents/<instanceId>/namespaces/<ns>/names/<name>/versions/<version> 488 const std::string pattern = "/api/:apiVersion/contents/:instanceId/namespaces/:ns/names/:name/versions/:version"; 489 m_server.Get(pattern, [&](const httplib::Request& req, httplib::Response& res) { 490 RunHttpCallback(req, res, "GetSpecificVersion", "v2", [&](const httplib::Request& req, httplib::Response& res) { 491 // TODO: Ignoring instanceId for now 492 493 const std::string& name = req.path_params.at("name"); 494 auto it = m_products.find(name); 495 if (it == m_products.end()) 496 { 497 throw StatusCodeException(httplib::StatusCode::NotFound_404); 498 } 499 500 const VersionList& versions = it->second; 501 if (versions.empty()) 502 { 503 throw StatusCodeException(httplib::StatusCode::InternalServerError_500); 504 } 505 506 // TODO: Are apps suppported? 507 508 const std::string& version = req.path_params.at("version"); 509 if (version.empty() || !versions.count(version)) 510 { 511 throw StatusCodeException(httplib::StatusCode::NotFound_404); 512 } 513 514 const std::string ns = req.path_params.at("ns"); 515 516 res.set_content(GenerateContentIdJsonObject(name, version, ns).dump(), "application/json"); 517 }); 518 }); 519 } 520 521 void MockWebServerImpl::ConfigurePostDownloadInfo() 522 { 523 // Path: 524 // /api/<apiVersion:v2>/contents/<instanceId>/namespaces/<ns>/names/<name>/versions/<version>/files?action=GenerateDownloadInfo 525 const std::string pattern = 526 "/api/:apiVersion/contents/:instanceId/namespaces/:ns/names/:name/versions/:version/files"; 527 m_server.Post(pattern, [&](const httplib::Request& req, httplib::Response& res) { 528 RunHttpCallback(req, res, "PostDownloadInfo", "v2", [&](const httplib::Request& req, httplib::Response& res) { 529 // TODO: Ignoring instanceId and ns for now 530 531 if (!req.has_param("action") || util::AreNotEqualI(req.get_param_value("action"), "GenerateDownloadInfo")) 532 { 533 // TODO: SFS might throw a different error when the query string is unexpected 534 throw StatusCodeException(httplib::StatusCode::NotFound_404); 535 } 536 537 const std::string& version = req.path_params.at("version"); 538 if (version.empty()) 539 { 540 throw StatusCodeException(httplib::StatusCode::NotFound_404); 541 } 542 543 json response; 544 const std::string& name = req.path_params.at("name"); 545 if (auto it = m_products.find(name); it != m_products.end()) 546 { 547 const auto& versions = it->second; 548 if (versions.empty()) 549 { 550 throw StatusCodeException(httplib::StatusCode::InternalServerError_500); 551 } 552 553 if (!versions.count(version)) 554 { 555 throw StatusCodeException(httplib::StatusCode::NotFound_404); 556 } 557 558 // Response is a dummy, doesn't use the version above 559 response = GeneratePostDownloadInfo(name); 560 } 561 else if (auto appIt = m_appProducts.find(name); appIt != m_appProducts.end()) 562 { 563 const auto& appList = appIt->second; 564 if (appList.empty()) 565 { 566 throw StatusCodeException(httplib::StatusCode::InternalServerError_500); 567 } 568 569 auto app = std::find_if(appList.begin(), appList.end(), [&](const App& app) { 570 return app.version == version; 571 }); 572 if (app == appList.end()) 573 { 574 throw StatusCodeException(httplib::StatusCode::NotFound_404); 575 } 576 577 // Response is a dummy, doesn't use the version above 578 response = GeneratePostAppDownloadInfo(name); 579 } 580 else 581 { 582 throw StatusCodeException(httplib::StatusCode::NotFound_404); 583 } 584 585 res.set_content(response.dump(), "application/json"); 586 }); 587 }); 588 } 589 590 void MockWebServerImpl::RunHttpCallback(const httplib::Request& req, 591 httplib::Response& res, 592 const std::string& methodName, 593 const std::string& apiVersion, 594 const std::function<void(const httplib::Request, httplib::Response&)>& callback) 595 { 596 if (m_forcedHttpErrors.size() > 0) 597 { 598 res.status = m_forcedHttpErrors.front(); 599 m_forcedHttpErrors.pop(); 600 601 BUFFER_LOG("Forcing HTTP error: " + std::to_string(res.status)); 602 } 603 else 604 { 605 try 606 { 607 BUFFER_LOG("Matched " + methodName); 608 CheckApiVersion(req, apiVersion); 609 CheckRequestHeaders(req); 610 callback(req, res); 611 res.status = httplib::StatusCode::OK_200; 612 } 613 catch (const StatusCodeException& ex) 614 { 615 res.status = ex.GetStatusCode(); 616 } 617 catch (const std::exception&) 618 { 619 res.status = httplib::StatusCode::InternalServerError_500; 620 } 621 catch (...) 622 { 623 res.status = httplib::StatusCode::InternalServerError_500; 624 } 625 } 626 627 if (m_headersByCode.count(res.status) > 0) 628 { 629 BUFFER_LOG("HTTP code " + std::to_string(res.status) + " has response headers to be sent"); 630 for (const auto& header : m_headersByCode[res.status]) 631 { 632 BUFFER_LOG("Adding header [" + header.first + "] with value [" + header.second + "]"); 633 res.set_header(header.first, header.second); 634 } 635 } 636 } 637 638 void MockWebServerImpl::CheckRequestHeaders(const httplib::Request& req) 639 { 640 for (const auto& header : m_expectedRequestHeaders) 641 { 642 std::optional<std::string> errorMessage; 643 if (!req.has_header(header.first)) 644 { 645 errorMessage = "Expected header [" + header.first + "] not found"; 646 } 647 else if (util::AreNotEqualI(req.get_header_value(header.first), header.second)) 648 { 649 errorMessage = "Header [" + header.first + "] with value [" + req.get_header_value(header.first) + 650 "] does not match the expected value [" + header.second + "]"; 651 } 652 653 if (errorMessage) 654 { 655 BUFFER_LOG(*errorMessage); 656 throw std::runtime_error(errorMessage->c_str()); 657 } 658 } 659 } 660 661 void MockWebServerImpl::RegisterProduct(std::string&& name, std::string&& version) 662 { 663 m_products[std::move(name)].emplace(std::move(version)); 664 } 665 666 void MockWebServerImpl::RegisterAppProduct(std::string&& name, 667 std::string&& version, 668 std::vector<MockPrerequisite>&& prerequisites) 669 { 670 for (const auto& prereq : prerequisites) 671 { 672 m_appProducts[prereq.name].emplace(App{prereq.version, {}}); 673 } 674 m_appProducts[std::move(name)].emplace(App{std::move(version), std::move(prerequisites)}); 675 } 676 677 void MockWebServerImpl::RegisterExpectedRequestHeader(std::string&& header, std::string&& value) 678 { 679 if (auto it = m_expectedRequestHeaders.find(header); it != m_expectedRequestHeaders.end()) 680 { 681 it->second = std::move(value); 682 return; 683 } 684 else 685 { 686 m_expectedRequestHeaders.emplace(std::move(header), std::move(value)); 687 } 688 } 689 690 void MockWebServerImpl::SetForcedHttpErrors(std::queue<int> forcedErrors) 691 { 692 m_forcedHttpErrors = std::move(forcedErrors); 693 } 694 695 void MockWebServerImpl::SetResponseHeaders(std::unordered_map<HttpCode, HeaderMap> headersByCode) 696 { 697 m_headersByCode = std::move(headersByCode); 698 }