Commit c6677df1 authored by Vincent Boisselle's avatar Vincent Boisselle Committed by Commit Bot

Added handling of large requests to Autofill API.

Increased the limit of Query GET requests, added a POST Query fallback, and
added UMA metrics to keep track of URL length and associated events.

This change might be needed when metadata is enabled.

Bug: 948862
Change-Id: If0934a685758ab8999ceea6ce22a27499e4f772f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1549615
Commit-Queue: Vincent Boisselle <vincb@google.com>
Reviewed-by: default avatarRoger McFarlane <rogerm@chromium.org>
Cr-Commit-Position: refs/heads/master@{#656080}
parent ae825014
...@@ -629,6 +629,7 @@ source_set("unit_tests") { ...@@ -629,6 +629,7 @@ source_set("unit_tests") {
"//third_party/libaddressinput:test_support", "//third_party/libaddressinput:test_support",
"//third_party/libaddressinput:util", "//third_party/libaddressinput:util",
"//third_party/libphonenumber", "//third_party/libphonenumber",
"//third_party/re2:re2",
"//ui/base", "//ui/base",
"//url", "//url",
] ]
......
...@@ -60,7 +60,7 @@ constexpr std::pair<int, int> kAutofillExperimentRanges[] = { ...@@ -60,7 +60,7 @@ constexpr std::pair<int, int> kAutofillExperimentRanges[] = {
{3314445, 3314448}, {3314854, 3314883}, {3314445, 3314448}, {3314854, 3314883},
}; };
const size_t kMaxQueryGetSize = 1400; // 1.25KB const size_t kMaxQueryGetSize = 1400; // 1.25 KiB
const size_t kAutofillDownloadManagerMaxFormCacheSize = 16; const size_t kAutofillDownloadManagerMaxFormCacheSize = 16;
const size_t kMaxFieldsPerQueryRequest = 100; const size_t kMaxFieldsPerQueryRequest = 100;
...@@ -448,16 +448,22 @@ bool GetUploadPayloadForApi(const AutofillUploadContents& upload, ...@@ -448,16 +448,22 @@ bool GetUploadPayloadForApi(const AutofillUploadContents& upload,
return upload_request.SerializeToString(payload); return upload_request.SerializeToString(payload);
} }
// Gets an API method URL given its type (query or upload) and an optional // Gets an API method URL given its type (query or upload), an optional
// resource ID. // resource ID, and the HTTP method to be used.
// Example usage: // Example usage:
// * GetAPIMethodUrl(REQUEST_QUERY, "1234") will return "/v1/pages/1234". // * GetAPIMethodUrl(REQUEST_QUERY, "1234", "GET") will return "/v1/pages/1234".
// * GetAPIMethodUrl(REQUEST_UPLOAD, "") will return "/v1/forms:vote". // * GetAPIMethodUrl(REQUEST_QUERY, "1234", "POST") will return "/v1/pages:get".
// * GetAPIMethodUrl(REQUEST_UPLOAD, "", "POST") will return "/v1/forms:vote".
std::string GetAPIMethodUrl(AutofillDownloadManager::RequestType type, std::string GetAPIMethodUrl(AutofillDownloadManager::RequestType type,
base::StringPiece resource_id) { base::StringPiece resource_id,
base::StringPiece method) {
const char* api_method_url; const char* api_method_url;
if (type == AutofillDownloadManager::REQUEST_QUERY) { if (type == AutofillDownloadManager::REQUEST_QUERY) {
api_method_url = "/v1/pages"; if (method == "POST") {
api_method_url = "/v1/pages:get";
} else {
api_method_url = "/v1/pages";
}
} else if (type == AutofillDownloadManager::REQUEST_UPLOAD) { } else if (type == AutofillDownloadManager::REQUEST_UPLOAD) {
api_method_url = "/v1/forms:vote"; api_method_url = "/v1/forms:vote";
} else { } else {
...@@ -471,6 +477,35 @@ std::string GetAPIMethodUrl(AutofillDownloadManager::RequestType type, ...@@ -471,6 +477,35 @@ std::string GetAPIMethodUrl(AutofillDownloadManager::RequestType type,
return base::StrCat({api_method_url, "/", resource_id}); return base::StrCat({api_method_url, "/", resource_id});
} }
// Gets HTTP body payload for API POST request.
std::string GetAPIBodyPayload(const std::string& payload,
AutofillDownloadManager::RequestType type) {
// Don't do anything for payloads not related to Query.
if (type != AutofillDownloadManager::REQUEST_QUERY) {
return payload;
}
// Wrap query payload in a request proto to interface with API Query method.
AutofillPageResourceQueryRequest request;
request.set_serialized_request(payload);
std::string new_payload;
DCHECK(request.SerializeToString(&new_payload))
<< "could not serialize AutofillPageResourceQueryRequest payload";
return new_payload;
}
// Gets the data payload for API Query (POST and GET).
bool GetAPIQueryPayload(const AutofillQueryContents& query,
std::string* payload) {
std::string serialized_query;
if (!CreateApiRequestFromLegacyRequest(query).SerializeToString(
&serialized_query)) {
return false;
}
base::Base64UrlEncode(serialized_query,
base::Base64UrlEncodePolicy::INCLUDE_PADDING, payload);
return true;
}
} // namespace } // namespace
struct AutofillDownloadManager::FormRequestData { struct AutofillDownloadManager::FormRequestData {
...@@ -542,10 +577,8 @@ bool AutofillDownloadManager::StartQueryRequest( ...@@ -542,10 +577,8 @@ bool AutofillDownloadManager::StartQueryRequest(
// Get the query request payload. // Get the query request payload.
std::string payload; std::string payload;
bool is_payload_serialized = bool is_payload_serialized = UseApi() ? GetAPIQueryPayload(query, &payload)
UseApi() : query.SerializeToString(&payload);
? CreateApiRequestFromLegacyRequest(query).SerializeToString(&payload)
: query.SerializeToString(&payload);
if (!is_payload_serialized) { if (!is_payload_serialized) {
return false; return false;
} }
...@@ -674,31 +707,27 @@ AutofillDownloadManager::GetRequestURLAndMethodForApi( ...@@ -674,31 +707,27 @@ AutofillDownloadManager::GetRequestURLAndMethodForApi(
// ID of the resource to add to the API request URL. Nothing will be added if // ID of the resource to add to the API request URL. Nothing will be added if
// |resource_id| is empty. // |resource_id| is empty.
std::string resource_id; std::string resource_id;
std::string method = "POST";
// Get the resource id of corresponding webpage when doing a query request.
if (request_data.request_type == AutofillDownloadManager::REQUEST_QUERY) { if (request_data.request_type == AutofillDownloadManager::REQUEST_QUERY) {
if (request_data.payload.length() <= kMaxQueryGetSize) { if (request_data.payload.length() <= kMaxAPIQueryGetSize) {
base::Base64UrlEncode(request_data.payload, resource_id = request_data.payload;
base::Base64UrlEncodePolicy::INCLUDE_PADDING, method = "GET";
&resource_id); UMA_HISTOGRAM_BOOLEAN("Autofill.Query.ApiUrlIsTooLong", false);
} else {
UMA_HISTOGRAM_BOOLEAN("Autofill.Query.ApiUrlIsTooLong", true);
} }
// Query method is always GET (represented by 0) with API. UMA_HISTOGRAM_BOOLEAN("Autofill.Query.Method", (method == "GET") ? 0 : 1);
UMA_HISTOGRAM_BOOLEAN("Autofill.Query.Method", 0);
} }
// Make the canonical URL to query the API, e.g., // Make the canonical URL to query the API, e.g.,
// https://autofill.googleapis.com/v1/forms/1234?alt=proto. // https://autofill.googleapis.com/v1/forms/1234?alt=proto.
GURL url = autofill_server_url_.Resolve( GURL url = autofill_server_url_.Resolve(
GetAPIMethodUrl(request_data.request_type, resource_id)); GetAPIMethodUrl(request_data.request_type, resource_id, method));
// Add the query parameter to set the response format to a serialized proto. // Add the query parameter to set the response format to a serialized proto.
url = net::AppendQueryParameter(url, "alt", "proto"); url = net::AppendQueryParameter(url, "alt", "proto");
// Determine the HTTP method that should be used.
std::string method =
(request_data.request_type == AutofillDownloadManager::REQUEST_QUERY)
? "GET"
: "POST";
return std::make_tuple(std::move(url), std::move(method)); return std::make_tuple(std::move(url), std::move(method));
} }
...@@ -714,6 +743,14 @@ bool AutofillDownloadManager::StartRequest(FormRequestData request_data) { ...@@ -714,6 +743,14 @@ bool AutofillDownloadManager::StartRequest(FormRequestData request_data) {
UseApi() ? GetRequestURLAndMethodForApi(request_data) UseApi() ? GetRequestURLAndMethodForApi(request_data)
: GetRequestURLAndMethod(request_data); : GetRequestURLAndMethod(request_data);
// Track the URL length for GET queries because the URL length can be in the
// thousands when rich metadata is enabled.
if (request_data.request_type == AutofillDownloadManager::REQUEST_QUERY &&
method == "GET") {
UMA_HISTOGRAM_COUNTS_100000("Autofill.Query.GetUrlLength",
request_url.spec().length());
}
auto resource_request = std::make_unique<network::ResourceRequest>(); auto resource_request = std::make_unique<network::ResourceRequest>();
resource_request->url = request_url; resource_request->url = request_url;
resource_request->load_flags = resource_request->load_flags =
...@@ -750,10 +787,14 @@ bool AutofillDownloadManager::StartRequest(FormRequestData request_data) { ...@@ -750,10 +787,14 @@ bool AutofillDownloadManager::StartRequest(FormRequestData request_data) {
simple_loader->SetAllowHttpErrorResults(true); simple_loader->SetAllowHttpErrorResults(true);
if (method == "POST") { if (method == "POST") {
const std::string content_type = const std::string& content_type =
UseApi() ? "application/x-protobuf" : "text/proto"; UseApi() ? "application/x-protobuf" : "text/proto";
const std::string& payload =
UseApi()
? GetAPIBodyPayload(request_data.payload, request_data.request_type)
: request_data.payload;
// Attach payload data and add data format header. // Attach payload data and add data format header.
simple_loader->AttachStringForUpload(request_data.payload, content_type); simple_loader->AttachStringForUpload(payload, content_type);
} }
// Transfer ownership of the loader into url_loaders_. Temporarily hang // Transfer ownership of the loader into url_loaders_. Temporarily hang
......
...@@ -31,6 +31,8 @@ namespace autofill { ...@@ -31,6 +31,8 @@ namespace autofill {
class AutofillDriver; class AutofillDriver;
class FormStructure; class FormStructure;
const size_t kMaxAPIQueryGetSize = 10240; // 10 KiB
// A helper to make sure that tests which modify the set of active autofill // A helper to make sure that tests which modify the set of active autofill
// experiments do not interfere with one another. // experiments do not interfere with one another.
struct ScopedActiveAutofillExperiments { struct ScopedActiveAutofillExperiments {
......
...@@ -52,6 +52,7 @@ ...@@ -52,6 +52,7 @@
#include "services/network/test/test_utils.h" #include "services/network/test/test_utils.h"
#include "testing/gmock/include/gmock/gmock.h" #include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h" #include "testing/gtest/include/gtest/gtest.h"
#include "third_party/re2/src/re2/re2.h"
#include "url/third_party/mozilla/url_parse.h" #include "url/third_party/mozilla/url_parse.h"
using base::UTF8ToUTF16; using base::UTF8ToUTF16;
...@@ -117,6 +118,35 @@ bool GetUploadRequestProtoFromRequest( ...@@ -117,6 +118,35 @@ bool GetUploadRequestProtoFromRequest(
return true; return true;
} }
bool GetAutofillPageResourceQueryRequestFromRequest(
network::TestURLLoaderFactory::PendingRequest* loader_request,
AutofillPageResourceQueryRequest* query_request) {
if (loader_request->request.request_body == nullptr) {
return false;
}
std::string request_body_content = GetStringFromDataElements(
loader_request->request.request_body->elements());
if (!query_request->ParseFromString(request_body_content)) {
return false;
}
return true;
}
bool DeserializeAutofillPageQueryRequest(base::StringPiece serialized_content,
AutofillPageQueryRequest* request) {
std::string decoded_content;
if (!base::Base64UrlDecode(serialized_content,
base::Base64UrlDecodePolicy::REQUIRE_PADDING,
&decoded_content)) {
return false;
}
if (!request->ParseFromString(decoded_content)) {
return false;
}
return true;
}
} // namespace } // namespace
// This tests AutofillDownloadManager. AutofillDownloadManagerTest implements // This tests AutofillDownloadManager. AutofillDownloadManagerTest implements
...@@ -499,22 +529,150 @@ TEST_F(AutofillDownloadManagerTest, QueryAPITest) { ...@@ -499,22 +529,150 @@ TEST_F(AutofillDownloadManagerTest, QueryAPITest) {
// Inspect the request that the test URL loader sent. // Inspect the request that the test URL loader sent.
network::TestURLLoaderFactory::PendingRequest* request = network::TestURLLoaderFactory::PendingRequest* request =
test_url_loader_factory_.GetPendingRequest(0); test_url_loader_factory_.GetPendingRequest(0);
// This is the URL we expect to query the API. The sub-path right after
// "/page" corresponds to the serialized AutofillPageQueryRequest proto (that // Verify request URL and the data payload it carries.
// we filled forms in) encoded in base64. The Autofill {
// https://clients1.google.com/ domain URL corresponds to the default domain // This is the URL we expect to query the API. The sub-path right after
// used by the download manager. // "/page" corresponds to the serialized AutofillPageQueryRequest proto
// (that we filled forms in) encoded in base64. The Autofill
// https://clients1.google.com/ domain URL corresponds to the default domain
// used by the download manager, which is invalid, but good for testing.
const std::string expected_url =
R"(https://clients1.google.com/v1/pages/(.+)\?alt=proto)";
std::string encoded_request;
ASSERT_TRUE(re2::RE2::FullMatch(request->request.url.spec(), expected_url,
&encoded_request));
AutofillPageQueryRequest request_content;
ASSERT_TRUE(
DeserializeAutofillPageQueryRequest(encoded_request, &request_content));
// Verify form content.
ASSERT_EQ(request_content.forms().size(), 1);
EXPECT_EQ(request_content.forms(0).signature(),
form_structures[0]->form_signature());
// Verify field content.
ASSERT_EQ(request_content.forms(0).fields().size(), 2);
EXPECT_EQ(request_content.forms(0).fields(0).signature(),
form_structures[0]->field(0)->GetFieldSignature());
EXPECT_EQ(request_content.forms(0).fields(1).signature(),
form_structures[0]->field(1)->GetFieldSignature());
}
// Verify API key header.
{
std::string header_value;
EXPECT_TRUE(
request->request.headers.GetHeader("X-Goog-Api-Key", &header_value));
EXPECT_EQ(header_value, "dummykey");
}
// Verify binary response header.
{
std::string header_value;
ASSERT_TRUE(request->request.headers.GetHeader(
"X-Goog-Encode-Response-If-Executable", &header_value));
EXPECT_EQ(header_value, "base64");
}
// Verify response.
test_url_loader_factory_.SimulateResponseWithoutRemovingFromPendingList(
request, "dummy response");
// Upon reception of a suggestions query, we expect OnLoadedServerPredictions
// to be called back from the observer and some histograms be incremented.
EXPECT_EQ(1U, responses_.size());
EXPECT_EQ(responses_.front().type_of_response,
AutofillDownloadManagerTest::QUERY_SUCCESSFULL);
histogram.ExpectBucketCount("Autofill.Query.WasInCache", CACHE_MISS, 1);
histogram.ExpectBucketCount("Autofill.Query.HttpResponseOrErrorCode",
net::HTTP_OK, 1);
}
TEST_F(AutofillDownloadManagerTest, QueryAPITestWhenTooLongUrl) {
base::test::ScopedFeatureList feature_list;
feature_list.InitWithFeatures(
// Enabled
// We want to query the API rather than the legacy server.
{features::kAutofillUseApi},
// Disabled
{});
// Build the form structures that we want to query.
FormData form;
FormFieldData field;
// Fill a really long field that will bust the request URL size limit of 10
// KiB. This is not a lot of memory, hence this should not cause problems for
// machines running this test. This will force the fallback to POST.
field.name_attribute = base::string16(kMaxAPIQueryGetSize, 'a');
field.form_control_type = "text";
form.fields.push_back(field);
std::vector<std::unique_ptr<FormStructure>> form_structures;
{
auto form_structure = std::make_unique<FormStructure>(form);
form_structure->set_is_rich_query_enabled(true);
form_structures.push_back(std::move(form_structure));
}
AutofillDownloadManager download_manager(&driver_, this, "dummykey");
// Start the query request and look if it is successful. No response was
// received yet.
base::HistogramTester histogram;
EXPECT_TRUE(
download_manager.StartQueryRequest(ToRawPointerVector(form_structures)));
// Verify request.
// Verify if histograms are right.
histogram.ExpectUniqueSample("Autofill.ServerQueryResponse",
AutofillMetrics::QUERY_SENT, 1);
// Verify that the logged method is POST.
histogram.ExpectUniqueSample("Autofill.Query.Method", METHOD_POST, 1);
// Get the latest request that the test URL loader sent.
network::TestURLLoaderFactory::PendingRequest* request =
test_url_loader_factory_.GetPendingRequest(0);
// Verify that the POST URL is used when request data too large.
const std::string expected_url = { const std::string expected_url = {
"https://clients1.google.com/v1/pages/" "https://clients1.google.com/v1/pages:get?alt=proto"};
"Chc2LjEuMTcxNS4xNDQyL2VuIChHR0xMKRIlCU9O84MyjH9NEgsNeu" // Verify API key header.
"FP4BIAGgAiABILDZxOStASABoAIgAaAA==?"
"alt=proto"};
EXPECT_EQ(request->request.url, expected_url); EXPECT_EQ(request->request.url, expected_url);
std::string api_key_header_value; {
EXPECT_TRUE(request->request.headers.GetHeader("X-Goog-Api-Key", std::string header_value;
&api_key_header_value)); EXPECT_TRUE(
EXPECT_EQ(api_key_header_value, "dummykey"); request->request.headers.GetHeader("X-Goog-Api-Key", &header_value));
EXPECT_EQ(header_value, "dummykey");
}
// Verify Content-Type header.
{
std::string header_value;
ASSERT_TRUE(
request->request.headers.GetHeader("Content-Type", &header_value));
EXPECT_EQ(header_value, "application/x-protobuf");
}
// Verify binary response header.
{
std::string header_value;
ASSERT_TRUE(request->request.headers.GetHeader(
"X-Goog-Encode-Response-If-Executable", &header_value));
EXPECT_EQ(header_value, "base64");
}
// Verify content of the POST body data.
{
AutofillPageResourceQueryRequest query_request;
ASSERT_TRUE(GetAutofillPageResourceQueryRequestFromRequest(request,
&query_request));
AutofillPageQueryRequest request_content;
ASSERT_TRUE(DeserializeAutofillPageQueryRequest(
query_request.serialized_request(), &request_content));
// Verify form content.
ASSERT_EQ(request_content.forms().size(), 1);
EXPECT_EQ(request_content.forms(0).signature(),
form_structures[0]->form_signature());
// Verify field content.
ASSERT_EQ(request_content.forms(0).fields().size(), 1);
EXPECT_EQ(request_content.forms(0).fields(0).signature(),
form_structures[0]->field(0)->GetFieldSignature());
}
// Verify response.
test_url_loader_factory_.SimulateResponseWithoutRemovingFromPendingList( test_url_loader_factory_.SimulateResponseWithoutRemovingFromPendingList(
request, "dummy response"); request, "dummy response");
// Upon reception of a suggestions query, we expect OnLoadedServerPredictions // Upon reception of a suggestions query, we expect OnLoadedServerPredictions
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment