Add TOO_MANY_REQUESTS to BinaryUploadService::Result

This adds a new enum value to handle 429 errors so they can be used to
throttle multi-file uploads. The following changes are made in this CL
to achieve this:

- BinaryUploadService and MultipartUploadRequest are updated to handle
  the new result.
- ContentAnalysisDelegate is update so no more files are uploaded in
  a multi-file upload case.
- Histograms are updated so data about this new case can be collected.

Bug: 1191062
Change-Id: Ie6794d2a48739a9216ca7493e038e714ca70b822
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2785163
Reviewed-by: Steven Holte <holte@chromium.org>
Reviewed-by: Roger Tawa <rogerta@chromium.org>
Reviewed-by: Daniel Rubery <drubery@chromium.org>
Auto-Submit: Dominique Fauteux-Chapleau <domfc@chromium.org>
Commit-Queue: Dominique Fauteux-Chapleau <domfc@chromium.org>
Cr-Commit-Position: refs/heads/master@{#869534}
diff --git a/chrome/browser/enterprise/connectors/analysis/content_analysis_delegate.cc b/chrome/browser/enterprise/connectors/analysis/content_analysis_delegate.cc
index b087f138..fe88383 100644
--- a/chrome/browser/enterprise/connectors/analysis/content_analysis_delegate.cc
+++ b/chrome/browser/enterprise/connectors/analysis/content_analysis_delegate.cc
@@ -236,6 +236,7 @@
     case BinaryUploadService::Result::UPLOAD_FAILURE:
     case BinaryUploadService::Result::TIMEOUT:
     case BinaryUploadService::Result::FAILED_TO_GET_TOKEN:
+    case BinaryUploadService::Result::TOO_MANY_REQUESTS:
     // UNAUTHORIZED allows data usage since it's a result only obtained if the
     // browser is not authorized to perform deep scanning. It does not make
     // sense to block data in this situation since no actual scanning of the
@@ -447,6 +448,9 @@
     base::FilePath path,
     BinaryUploadService::Result result,
     enterprise_connectors::ContentAnalysisResponse response) {
+  if (result == BinaryUploadService::Result::TOO_MANY_REQUESTS)
+    throttled_ = true;
+
   // Find the path in the set of files that are being scanned.
   auto it = std::find(data_.paths.begin(), data_.paths.end(), path);
   DCHECK(it != data_.paths.end());
@@ -611,6 +615,14 @@
     return;
   }
 
+  // If |throttled_| is true, then the file shouldn't be upload since the server
+  // is receiving too many requests.
+  if (throttled_) {
+    request->FinishRequest(BinaryUploadService::Result::TOO_MANY_REQUESTS,
+                           enterprise_connectors::ContentAnalysisResponse());
+    return;
+  }
+
   UploadFileForDeepScanning(result, data_.paths[index], std::move(request));
 }
 
diff --git a/chrome/browser/enterprise/connectors/analysis/content_analysis_delegate.h b/chrome/browser/enterprise/connectors/analysis/content_analysis_delegate.h
index fbab5d6..a6b257e 100644
--- a/chrome/browser/enterprise/connectors/analysis/content_analysis_delegate.h
+++ b/chrome/browser/enterprise/connectors/analysis/content_analysis_delegate.h
@@ -360,6 +360,10 @@
   // for every file/text. This is read to ensure |this| isn't deleted too early.
   bool data_uploaded_ = false;
 
+  // This is set to true as soon as a TOO_MANY_REQUESTS response is obtained. No
+  // more data should be upload for |this| at that point.
+  bool throttled_ = false;
+
   base::TimeTicks upload_start_time_;
 
   base::WeakPtrFactory<ContentAnalysisDelegate> weak_ptr_factory_{this};
diff --git a/chrome/browser/enterprise/connectors/analysis/content_analysis_delegate_browsertest.cc b/chrome/browser/enterprise/connectors/analysis/content_analysis_delegate_browsertest.cc
index cd17970..25ca4be 100644
--- a/chrome/browser/enterprise/connectors/analysis/content_analysis_delegate_browsertest.cc
+++ b/chrome/browser/enterprise/connectors/analysis/content_analysis_delegate_browsertest.cc
@@ -511,6 +511,84 @@
   ASSERT_EQ(FakeBinaryUploadServiceStorage()->requests_count(), 2);
 }
 
+IN_PROC_BROWSER_TEST_P(ContentAnalysisDelegateBrowserTest, Throttled) {
+  base::ScopedAllowBlockingForTesting allow_blocking;
+
+  // Set up delegate and upload service.
+  EnableUploadsScanningAndReporting();
+
+  ContentAnalysisDelegate::SetFactoryForTesting(
+      base::BindRepeating(&MinimalFakeContentAnalysisDelegate::Create));
+
+  FakeBinaryUploadServiceStorage()->SetAuthorized(true);
+  FakeBinaryUploadServiceStorage()->SetShouldAutomaticallyAuthorize(true);
+
+  // Create the files to be opened and scanned.
+  ContentAnalysisDelegate::Data data;
+  CreateFilesForTest({"a.exe", "b.exe", "c.exe"},
+                     {"a content", "b content", "c content"}, &data);
+  ASSERT_TRUE(ContentAnalysisDelegate::IsEnabled(
+      browser()->profile(), GURL(kTestUrl), &data, FILE_ATTACHED));
+
+  // The malware verdict means an event should be reported.
+  safe_browsing::EventReportValidator validator(client());
+  validator.ExpectUnscannedFileEvents(
+      /*url*/ "about:blank",
+      {
+          created_file_paths()[0].AsUTF8Unsafe(),
+          created_file_paths()[1].AsUTF8Unsafe(),
+          created_file_paths()[2].AsUTF8Unsafe(),
+      },
+      {
+          // printf "a content" | sha256sum | tr '[:lower:]' '[:upper:]'
+          "D2D2ACF640179223BF9E1EB43C5FBF854C4E50FFB6733BC3A9279D3FF7DE9BE1",
+          // printf "b content" | sha256sum | tr '[:lower:]' '[:upper:]'
+          "93CB3641ADD6A9A6619D7E2F304EBCF5160B2DB016B27C6E3D641C5306897224",
+          // printf "c content" | sha256sum | tr '[:lower:]' '[:upper:]'
+          "2E6D1C4A1F39A02562BF1505AD775C0323D7A04C0C37C9B29D25F532B9972080",
+      },
+      /*trigger*/ SafeBrowsingPrivateEventRouter::kTriggerFileUpload,
+      // TODO(crbug.com/1191060): Update this string when the event is supported
+      /*reason*/ "SERVICE_UNAVAILABLE",
+      /*mimetypes*/ ExeMimeTypes(),
+      /*size*/ 9,
+      /*result*/
+      safe_browsing::EventResultToString(safe_browsing::EventResult::ALLOWED),
+      /*username*/ kUserName);
+
+  // Only the first file should be uploaded since the other ones will be
+  // throttled.
+  FakeBinaryUploadServiceStorage()->SetResponseForFile(
+      "a.exe", BinaryUploadService::Result::TOO_MANY_REQUESTS,
+      ContentAnalysisResponse());
+
+  bool called = false;
+  base::RunLoop run_loop;
+  SetQuitClosure(run_loop.QuitClosure());
+
+  // Start test.
+  ContentAnalysisDelegate::CreateForWebContents(
+      browser()->tab_strip_model()->GetActiveWebContents(), std::move(data),
+      base::BindLambdaForTesting(
+          [&called](const ContentAnalysisDelegate::Data& data,
+                    const ContentAnalysisDelegate::Result& result) {
+            ASSERT_TRUE(result.text_results.empty());
+            ASSERT_EQ(result.paths_results.size(), 3u);
+            for (bool result : result.paths_results)
+              ASSERT_TRUE(result);
+            called = true;
+          }),
+      safe_browsing::DeepScanAccessPoint::UPLOAD);
+
+  run_loop.Run();
+
+  EXPECT_TRUE(called);
+
+  // There should have been 1 request for the first file and 1 for
+  // authentication.
+  ASSERT_EQ(FakeBinaryUploadServiceStorage()->requests_count(), 2);
+}
+
 // This class tests each of the blocking settings used in Connector policies:
 // - block_until_verdict
 // - block_password_protected
diff --git a/chrome/browser/safe_browsing/cloud_content_scanning/binary_upload_service.cc b/chrome/browser/safe_browsing/cloud_content_scanning/binary_upload_service.cc
index b46abd7f..0263c51 100644
--- a/chrome/browser/safe_browsing/cloud_content_scanning/binary_upload_service.cc
+++ b/chrome/browser/safe_browsing/cloud_content_scanning/binary_upload_service.cc
@@ -83,6 +83,8 @@
       return "FILE_ENCRYPTED";
     case BinaryUploadService::Result::DLP_SCAN_UNSUPPORTED_FILE_TYPE:
       return "DLP_SCAN_UNSUPPORTED_FILE_TYPE";
+    case BinaryUploadService::Result::TOO_MANY_REQUESTS:
+      return "TOO_MANY_REQUESTS";
   }
 }
 
@@ -364,10 +366,17 @@
 
 void BinaryUploadService::OnUploadComplete(Request* request,
                                            bool success,
+                                           int http_status,
                                            const std::string& response_data) {
   if (!IsActive(request))
     return;
 
+  if (http_status == net::HTTP_TOO_MANY_REQUESTS) {
+    FinishRequest(request, Result::TOO_MANY_REQUESTS,
+                  enterprise_connectors::ContentAnalysisResponse());
+    return;
+  }
+
   if (!success) {
     FinishRequest(request, Result::UPLOAD_FAILURE,
                   enterprise_connectors::ContentAnalysisResponse());
diff --git a/chrome/browser/safe_browsing/cloud_content_scanning/binary_upload_service.h b/chrome/browser/safe_browsing/cloud_content_scanning/binary_upload_service.h
index ac4de13..ef9e17a 100644
--- a/chrome/browser/safe_browsing/cloud_content_scanning/binary_upload_service.h
+++ b/chrome/browser/safe_browsing/cloud_content_scanning/binary_upload_service.h
@@ -81,7 +81,11 @@
     // The file's type is not supported and the file was not uploaded.
     DLP_SCAN_UNSUPPORTED_FILE_TYPE = 8,
 
-    kMaxValue = DLP_SCAN_UNSUPPORTED_FILE_TYPE,
+    // The server returned a 429 HTTP status indicating too many requests are
+    // being sent.
+    TOO_MANY_REQUESTS = 9,
+
+    kMaxValue = TOO_MANY_REQUESTS,
   };
 
   // Callbacks used to pass along the results of scanning. The response protos
@@ -244,6 +248,7 @@
 
   void OnUploadComplete(Request* request,
                         bool success,
+                        int http_status,
                         const std::string& response_data);
 
   void OnGetResponse(Request* request,
diff --git a/chrome/browser/safe_browsing/cloud_content_scanning/binary_upload_service_unittest.cc b/chrome/browser/safe_browsing/cloud_content_scanning/binary_upload_service_unittest.cc
index 33d5c7b..85dea56 100644
--- a/chrome/browser/safe_browsing/cloud_content_scanning/binary_upload_service_unittest.cc
+++ b/chrome/browser/safe_browsing/cloud_content_scanning/binary_upload_service_unittest.cc
@@ -63,7 +63,8 @@
   void Start() override {
     std::string serialized_response;
     response_.SerializeToString(&serialized_response);
-    std::move(callback_).Run(should_succeed_, serialized_response);
+    std::move(callback_).Run(should_succeed_, should_succeed_ ? 200 : 401,
+                             serialized_response);
   }
 
  private:
@@ -161,7 +162,7 @@
   void ReceiveResponseFromUpload(BinaryUploadService::Request* request,
                                  bool success,
                                  const std::string& response) {
-    service_->OnUploadComplete(request, success, response);
+    service_->OnUploadComplete(request, success, success ? 200 : 401, response);
   }
 
   void ServiceWithNoFCMConnection() {
diff --git a/chrome/browser/safe_browsing/cloud_content_scanning/deep_scanning_test_utils.cc b/chrome/browser/safe_browsing/cloud_content_scanning/deep_scanning_test_utils.cc
index 76424db..e3a0c8b 100644
--- a/chrome/browser/safe_browsing/cloud_content_scanning/deep_scanning_test_utils.cc
+++ b/chrome/browser/safe_browsing/cloud_content_scanning/deep_scanning_test_utils.cc
@@ -4,6 +4,7 @@
 
 #include "chrome/browser/safe_browsing/cloud_content_scanning/deep_scanning_test_utils.h"
 
+#include "base/containers/flat_map.h"
 #include "base/json/json_reader.h"
 #include "base/strings/string_number_conversions.h"
 #include "base/values.h"
@@ -48,8 +49,7 @@
     const std::string& expected_username) {
   event_key_ = SafeBrowsingPrivateEventRouter::kKeyUnscannedFileEvent;
   url_ = expected_url;
-  filename_ = expected_filename;
-  sha256_ = expected_sha256;
+  filenames_and_hashes_[expected_filename] = expected_sha256;
   mimetypes_ = expected_mimetypes;
   trigger_ = expected_trigger;
   unscanned_reason_ = expected_reason;
@@ -66,6 +66,37 @@
       });
 }
 
+void EventReportValidator::ExpectUnscannedFileEvents(
+    const std::string& expected_url,
+    const std::vector<const std::string>& expected_filenames,
+    const std::vector<const std::string>& expected_sha256s,
+    const std::string& expected_trigger,
+    const std::string& expected_reason,
+    const std::set<std::string>* expected_mimetypes,
+    int expected_content_size,
+    const std::string& expected_result,
+    const std::string& expected_username) {
+  DCHECK_EQ(expected_filenames.size(), expected_sha256s.size());
+  for (size_t i = 0; i < expected_filenames.size(); ++i)
+    filenames_and_hashes_[expected_filenames[i]] = expected_sha256s[i];
+
+  event_key_ = SafeBrowsingPrivateEventRouter::kKeyUnscannedFileEvent;
+  url_ = expected_url;
+  mimetypes_ = expected_mimetypes;
+  trigger_ = expected_trigger;
+  unscanned_reason_ = expected_reason;
+  content_size_ = expected_content_size;
+  result_ = expected_result;
+  username_ = expected_username;
+  EXPECT_CALL(*client_, UploadSecurityEventReport_(_, _, _, _))
+      .Times(expected_filenames.size())
+      .WillRepeatedly([this](content::BrowserContext* context,
+                             bool include_device_info, base::Value& report,
+                             base::OnceCallback<void(bool)>& callback) {
+        ValidateReport(&report);
+      });
+}
+
 void EventReportValidator::ExpectDangerousDeepScanningResult(
     const std::string& expected_url,
     const std::string& expected_filename,
@@ -78,8 +109,7 @@
     const std::string& expected_username) {
   event_key_ = SafeBrowsingPrivateEventRouter::kKeyDangerousDownloadEvent;
   url_ = expected_url;
-  filename_ = expected_filename;
-  sha256_ = expected_sha256;
+  filenames_and_hashes_[expected_filename] = expected_sha256;
   threat_type_ = expected_threat_type;
   mimetypes_ = expected_mimetypes;
   trigger_ = expected_trigger;
@@ -110,8 +140,7 @@
   event_key_ = SafeBrowsingPrivateEventRouter::kKeySensitiveDataEvent;
   url_ = expected_url;
   dlp_verdict_ = expected_dlp_verdict;
-  filename_ = expected_filename;
-  sha256_ = expected_sha256;
+  filenames_and_hashes_[expected_filename] = expected_sha256;
   mimetypes_ = expected_mimetypes;
   trigger_ = expected_trigger;
   content_size_ = expected_content_size;
@@ -142,8 +171,7 @@
         const std::string& expected_username) {
   event_key_ = SafeBrowsingPrivateEventRouter::kKeyDangerousDownloadEvent;
   url_ = expected_url;
-  filename_ = expected_filename;
-  sha256_ = expected_sha256;
+  filenames_and_hashes_[expected_filename] = expected_sha256;
   threat_type_ = expected_threat_type;
   trigger_ = expected_trigger;
   mimetypes_ = expected_mimetypes;
@@ -184,8 +212,7 @@
         const std::string& expected_username) {
   event_key_ = SafeBrowsingPrivateEventRouter::kKeySensitiveDataEvent;
   url_ = expected_url;
-  filename_ = expected_filename;
-  sha256_ = expected_sha256;
+  filenames_and_hashes_[expected_filename] = expected_sha256;
   trigger_ = expected_trigger;
   mimetypes_ = expected_mimetypes;
   content_size_ = expected_content_size;
@@ -223,8 +250,7 @@
     const std::string& expected_username) {
   event_key_ = SafeBrowsingPrivateEventRouter::kKeyDangerousDownloadEvent;
   url_ = expected_url;
-  filename_ = expected_filename;
-  sha256_ = expected_sha256;
+  filenames_and_hashes_[expected_filename] = expected_sha256;
   threat_type_ = expected_threat_type;
   mimetypes_ = expected_mimetypes;
   trigger_ = expected_trigger;
@@ -261,9 +287,7 @@
 
   // The event should match the expected values.
   ValidateField(event, SafeBrowsingPrivateEventRouter::kKeyUrl, url_);
-  ValidateField(event, SafeBrowsingPrivateEventRouter::kKeyFileName, filename_);
-  ValidateField(event, SafeBrowsingPrivateEventRouter::kKeyDownloadDigestSha256,
-                sha256_);
+  ValidateFilenameAndHash(event);
   ValidateField(event, SafeBrowsingPrivateEventRouter::kKeyTrigger, trigger_);
   ValidateField(event, SafeBrowsingPrivateEventRouter::kKeyContentSize,
                 content_size_);
@@ -316,6 +340,16 @@
                 expected_rule.rule_id());
 }
 
+void EventReportValidator::ValidateFilenameAndHash(base::Value* value) {
+  const std::string* filename =
+      value->FindStringKey(SafeBrowsingPrivateEventRouter::kKeyFileName);
+  ASSERT_TRUE(filename);
+  ASSERT_TRUE(filenames_and_hashes_.count(*filename))
+      << "Mismatch in field " << SafeBrowsingPrivateEventRouter::kKeyFileName;
+  ValidateField(value, SafeBrowsingPrivateEventRouter::kKeyDownloadDigestSha256,
+                filenames_and_hashes_[*filename]);
+}
+
 void EventReportValidator::ValidateField(
     base::Value* value,
     const std::string& field_key,
diff --git a/chrome/browser/safe_browsing/cloud_content_scanning/deep_scanning_test_utils.h b/chrome/browser/safe_browsing/cloud_content_scanning/deep_scanning_test_utils.h
index 5e1898e..56bfd50 100644
--- a/chrome/browser/safe_browsing/cloud_content_scanning/deep_scanning_test_utils.h
+++ b/chrome/browser/safe_browsing/cloud_content_scanning/deep_scanning_test_utils.h
@@ -9,6 +9,7 @@
 #include <string>
 
 #include "base/callback.h"
+#include "base/containers/flat_map.h"
 #include "base/optional.h"
 #include "chrome/browser/safe_browsing/cloud_content_scanning/deep_scanning_utils.h"
 #include "components/enterprise/common/proto/connectors.pb.h"
@@ -93,6 +94,17 @@
                                 const std::string& expected_result,
                                 const std::string& expected_username);
 
+  void ExpectUnscannedFileEvents(
+      const std::string& expected_url,
+      const std::vector<const std::string>& expected_filenames,
+      const std::vector<const std::string>& expected_sha256s,
+      const std::string& expected_trigger,
+      const std::string& expected_reason,
+      const std::set<std::string>* expected_mimetypes,
+      int expected_content_size,
+      const std::string& expected_result,
+      const std::string& expected_username);
+
   void ExpectDangerousDownloadEvent(
       const std::string& expected_url,
       const std::string& expected_filename,
@@ -116,6 +128,7 @@
   void ValidateDlpRule(base::Value* value,
                        const enterprise_connectors::ContentAnalysisResponse::
                            Result::TriggeredRule& expected_rule);
+  void ValidateFilenameAndHash(base::Value* value);
   void ValidateField(base::Value* value,
                      const std::string& field_key,
                      const base::Optional<std::string>& expected_value);
@@ -130,8 +143,6 @@
 
   std::string event_key_;
   std::string url_;
-  std::string filename_;
-  std::string sha256_;
   std::string trigger_;
   base::Optional<enterprise_connectors::ContentAnalysisResponse::Result>
       dlp_verdict_ = base::nullopt;
@@ -142,6 +153,11 @@
   base::Optional<std::string> result_ = base::nullopt;
   std::string username_;
 
+  // When multiple files generate events, we don't necessarily know in which
+  // order they will be reported. As such, we use a map to ensure all of them
+  // are called as expected.
+  base::flat_map<std::string, std::string> filenames_and_hashes_;
+
   base::RepeatingClosure done_closure_;
 };
 
diff --git a/chrome/browser/safe_browsing/cloud_content_scanning/deep_scanning_utils.cc b/chrome/browser/safe_browsing/cloud_content_scanning/deep_scanning_utils.cc
index 7e33556..104a5e5 100644
--- a/chrome/browser/safe_browsing/cloud_content_scanning/deep_scanning_utils.cc
+++ b/chrome/browser/safe_browsing/cloud_content_scanning/deep_scanning_utils.cc
@@ -36,6 +36,8 @@
     case BinaryUploadService::Result::UNKNOWN:
     case BinaryUploadService::Result::UPLOAD_FAILURE:
     case BinaryUploadService::Result::FAILED_TO_GET_TOKEN:
+    // TODO(crbug.com/1191060): Update this string when the event is supported.
+    case BinaryUploadService::Result::TOO_MANY_REQUESTS:
       unscanned_reason = "SERVICE_UNAVAILABLE";
       break;
     case BinaryUploadService::Result::FILE_ENCRYPTED:
@@ -324,6 +326,8 @@
       return "FileEncrypted";
     case BinaryUploadService::Result::DLP_SCAN_UNSUPPORTED_FILE_TYPE:
       return "DlpScanUnsupportedFileType";
+    case BinaryUploadService::Result::TOO_MANY_REQUESTS:
+      return "TooManyRequests";
   }
 }
 
diff --git a/chrome/browser/safe_browsing/cloud_content_scanning/multipart_uploader.cc b/chrome/browser/safe_browsing/cloud_content_scanning/multipart_uploader.cc
index a0f4ac7..04bfd04 100644
--- a/chrome/browser/safe_browsing/cloud_content_scanning/multipart_uploader.cc
+++ b/chrome/browser/safe_browsing/cloud_content_scanning/multipart_uploader.cc
@@ -133,13 +133,15 @@
     base::UmaHistogramMediumTimes(
         "SBMultipartUploader.SuccessfulUploadDuration",
         base::Time::Now() - start_time_);
-    std::move(callback_).Run(/*success=*/true, *response_body.get());
+    std::move(callback_).Run(/*success=*/true, response_code,
+                             *response_body.get());
   } else {
     if (response_code < 500 || retry_count_ >= kMaxRetryAttempts) {
       RecordUploadSuccessHistogram(/*success=*/false);
       base::UmaHistogramMediumTimes("SBMultipartUploader.FailedUploadDuration",
                                     base::Time::Now() - start_time_);
-      std::move(callback_).Run(/*success=*/false, *response_body.get());
+      std::move(callback_).Run(/*success=*/false, response_code,
+                               *response_body.get());
     } else {
       content::GetUIThreadTaskRunner({})->PostDelayedTask(
           FROM_HERE,
diff --git a/chrome/browser/safe_browsing/cloud_content_scanning/multipart_uploader.h b/chrome/browser/safe_browsing/cloud_content_scanning/multipart_uploader.h
index b6b9690..d8e3a8c 100644
--- a/chrome/browser/safe_browsing/cloud_content_scanning/multipart_uploader.h
+++ b/chrome/browser/safe_browsing/cloud_content_scanning/multipart_uploader.h
@@ -29,8 +29,8 @@
 // multipart protocol. This class is neither movable nor copyable.
 class MultipartUploadRequest {
  public:
-  using Callback =
-      base::OnceCallback<void(bool success, const std::string& response_data)>;
+  using Callback = base::OnceCallback<
+      void(bool success, int http_status, const std::string& response_data)>;
 
   // Creates a MultipartUploadRequest, which will upload |data| to the given
   // |base_url| with |metadata| attached.
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index ebc2ed8..c573b12 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -67848,6 +67848,7 @@
   <int value="6" label="Unauthorized"/>
   <int value="7" label="File encrypted"/>
   <int value="8" label="Unsupported file type"/>
+  <int value="9" label="Too many requests"/>
 </enum>
 
 <enum name="SafeBrowsingDelayedWarningEvent">
diff --git a/tools/metrics/histograms/histograms_xml/safe_browsing/histograms.xml b/tools/metrics/histograms/histograms_xml/safe_browsing/histograms.xml
index ecb7c2f..381277f 100644
--- a/tools/metrics/histograms/histograms_xml/safe_browsing/histograms.xml
+++ b/tools/metrics/histograms/histograms_xml/safe_browsing/histograms.xml
@@ -304,6 +304,17 @@
   </summary>
 </histogram>
 
+<histogram name="SafeBrowsing.DeepScan.Download.TooManyRequests.Duration"
+    units="ms" expires_after="2022-02-01">
+  <owner>domfc@chromium.org</owner>
+  <owner>webprotect-team@google.com</owner>
+  <summary>
+    This records the deep scanning duration of a user download request with a
+    TooManyRequests result. It is logged once for each binary upload with that
+    result.
+  </summary>
+</histogram>
+
 <histogram name="SafeBrowsing.DeepScan.Download.Unknown.Duration" units="ms"
     expires_after="2022-02-01">
   <owner>domfc@chromium.org</owner>
@@ -425,6 +436,17 @@
   </summary>
 </histogram>
 
+<histogram name="SafeBrowsing.DeepScan.DragAndDrop.TooManyRequests.Duration"
+    units="ms" expires_after="2022-02-01">
+  <owner>domfc@chromium.org</owner>
+  <owner>webprotect-team@google.com</owner>
+  <summary>
+    This records the deep scanning duration of a user &quot;drag and drop&quot;
+    request with a TooManyRequests result. It is logged once for each binary
+    upload with that result.
+  </summary>
+</histogram>
+
 <histogram name="SafeBrowsing.DeepScan.DragAndDrop.Unknown.Duration" units="ms"
     expires_after="2022-02-01">
   <owner>domfc@chromium.org</owner>
@@ -533,6 +555,17 @@
   </summary>
 </histogram>
 
+<histogram name="SafeBrowsing.DeepScan.Paste.TooManyRequests.Duration"
+    units="ms" expires_after="2022-02-01">
+  <owner>domfc@chromium.org</owner>
+  <owner>webprotect-team@google.com</owner>
+  <summary>
+    This records the deep scanning duration of a user paste request with a
+    TooManyRequests result. It is logged once for each binary upload with that
+    result.
+  </summary>
+</histogram>
+
 <histogram name="SafeBrowsing.DeepScan.Paste.Unknown.Duration" units="ms"
     expires_after="2022-02-01">
   <owner>domfc@chromium.org</owner>
@@ -650,6 +683,17 @@
   </summary>
 </histogram>
 
+<histogram name="SafeBrowsing.DeepScan.Upload.TooManyRequests.Duration"
+    units="ms" expires_after="2022-02-01">
+  <owner>domfc@chromium.org</owner>
+  <owner>webprotect-team@google.com</owner>
+  <summary>
+    This records the deep scanning duration of a user upload request with a
+    TooManyRequests result. It is logged once for each binary upload with that
+    result.
+  </summary>
+</histogram>
+
 <histogram name="SafeBrowsing.DeepScan.Upload.Unknown.Duration" units="ms"
     expires_after="2022-02-01">
   <owner>domfc@chromium.org</owner>