0

[FLEDGE] BidderWorklet reuse CL 3: Rework BidderWorklet load errors.

This CL makes BidderWorklet load errors behave like seller worklet load
errors: On error, pending callbacks are not invoked. Instead, the Mojo
pipe is reset with an error message.

Bug: 1276639
Change-Id: I746f2a3718a4f5087e76a3b6292dd78bc2744104
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3365661
Reviewed-by: Maks Orlovich <morlovich@chromium.org>
Reviewed-by: Daniel Cheng <dcheng@chromium.org>
Commit-Queue: Matt Menke <mmenke@chromium.org>
Cr-Commit-Position: refs/heads/main@{#957372}
This commit is contained in:
Matt Menke
2022-01-11 01:47:57 +00:00
committed by Chromium LUCI CQ
parent e8528d1222
commit 5272f09fe3
8 changed files with 276 additions and 242 deletions

@ -303,7 +303,7 @@ void AuctionRunner::OnBidderWorkletProcessReceived(BidState* bid_state) {
bid_state->state = BidState::State::kGeneratingBid;
LoadBidderWorklet(*bid_state,
/*disconnect_handler=*/base::BindOnce(
&AuctionRunner::OnGenerateBidCrashed,
&AuctionRunner::OnBidderWorkletDisconnected,
base::Unretained(this), bid_state));
const blink::InterestGroup& interest_group = bid_state->bidder.interest_group;
bid_state->bidder_worklet->GenerateBid(
@ -318,11 +318,20 @@ void AuctionRunner::OnBidderWorkletProcessReceived(BidState* bid_state) {
base::Unretained(this), bid_state));
}
void AuctionRunner::OnGenerateBidCrashed(BidState* state) {
void AuctionRunner::OnBidderWorkletDisconnected(
BidState* state,
uint32_t custom_reason,
const std::string& description) {
std::vector<std::string> errors;
if (description.empty()) {
errors.emplace_back(
base::StrCat({state->bidder.interest_group.bidding_url->spec(),
" crashed while trying to run generateBid()."}));
} else {
errors.push_back(description);
}
OnGenerateBidComplete(state, auction_worklet::mojom::BidderWorkletBidPtr(),
std::vector<std::string>{base::StrCat(
{state->bidder.interest_group.bidding_url->spec(),
" crashed while trying to run generateBid()."})});
errors);
}
void AuctionRunner::OnGenerateBidComplete(
@ -563,12 +572,9 @@ void AuctionRunner::ReportBidWin(
// Load the script for the top-scoring bidder worklet again, and invoke its
// ReportWin() method.
LoadBidderWorklet(
*top_bidder_, /*disconnect_handler=*/base::BindOnce(
&AuctionRunner::FailAuction, base::Unretained(this),
AuctionResult::kWinningBidderWorkletCrashed,
base::StrCat({top_bidder_->bidder.interest_group.bidding_url->spec(),
" crashed while trying to run reportWin()."})));
LoadBidderWorklet(*top_bidder_, /*disconnect_handler=*/base::BindOnce(
&AuctionRunner::WinningBidderWorkletDisconnected,
base::Unretained(this)));
top_bidder_->bidder_worklet->ReportWin(
top_bidder_->bidder.interest_group.name,
auction_config_->shareable_auction_ad_config->auction_signals,
@ -593,6 +599,24 @@ void AuctionRunner::OnReportBidWinComplete(
ReportSuccess();
}
void AuctionRunner::WinningBidderWorkletDisconnected(
uint32_t custom_reason,
const std::string& description) {
std::vector<std::string> errors;
if (description.empty()) {
// Empty `description` means the process crashed.
FailAuction(
AuctionResult::kWinningBidderWorkletCrashed,
{base::StrCat({top_bidder_->bidder.interest_group.bidding_url->spec(),
" crashed while trying to run reportWin()."})});
} else {
// Non-empty `description` means there was a different error (e.g., script
// failed to load). This currently does not fail the auction.
errors_.push_back(description);
ReportSuccess();
}
}
void AuctionRunner::ReportSuccess() {
DCHECK(callback_);
DCHECK(top_bidder_->bid_result);
@ -661,8 +685,9 @@ void AuctionRunner::RecordResult(AuctionResult result) const {
}
}
void AuctionRunner::LoadBidderWorklet(BidState& bid_state,
base::OnceClosure disconnect_handler) {
void AuctionRunner::LoadBidderWorklet(
BidState& bid_state,
mojo::ConnectionErrorWithReasonCallback disconnect_handler) {
const blink::InterestGroup& interest_group = bid_state.bidder.interest_group;
mojo::PendingRemote<network::mojom::URLLoaderFactory> url_loader_factory;
@ -691,7 +716,7 @@ void AuctionRunner::LoadBidderWorklet(BidState& bid_state,
interest_group.bidding_wasm_helper_url,
interest_group.trusted_bidding_signals_url,
browser_signals_->top_frame_origin);
bid_state.bidder_worklet.set_disconnect_handler(
bid_state.bidder_worklet.set_disconnect_with_reason_handler(
std::move(disconnect_handler));
}

@ -21,6 +21,7 @@
#include "content/services/auction_worklet/public/mojom/auction_worklet_service.mojom.h"
#include "content/services/auction_worklet/public/mojom/bidder_worklet.mojom.h"
#include "content/services/auction_worklet/public/mojom/seller_worklet.mojom.h"
#include "mojo/public/cpp/bindings/connection_error_callback.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "services/network/public/mojom/client_security_state.mojom-forward.h"
#include "services/network/public/mojom/url_loader_factory.mojom-forward.h"
@ -266,7 +267,9 @@ class CONTENT_EXPORT AuctionRunner {
// bid.
void OnBidderWorkletProcessReceived(BidState* bid_state);
void OnGenerateBidCrashed(BidState* state);
void OnBidderWorkletDisconnected(BidState* state,
uint32_t custom_reason,
const std::string& description);
void OnGenerateBidComplete(BidState* state,
auction_worklet::mojom::BidderWorkletBidPtr bid,
const std::vector<std::string>& errors);
@ -305,6 +308,11 @@ class CONTENT_EXPORT AuctionRunner {
void OnReportBidWinComplete(const absl::optional<GURL>& bidder_report_url,
const std::vector<std::string>& error_msgs);
// Called when the BidderWorklet that won an auction is disconnected during
// the ReportWin() call.
void WinningBidderWorkletDisconnected(uint32_t custom_reason,
const std::string& description);
// Completes the auction, invoking `callback_` and preventing any future
// calls into `this` by closing mojo pipes and disposing of weak pointers. The
// owner must be able to safely delete `this` when the callback is invoked.
@ -319,8 +327,9 @@ class CONTENT_EXPORT AuctionRunner {
// Loads a bidder worklet for `bid_state`, setting the provided disconnect
// handler.
void LoadBidderWorklet(BidState& bid_state,
base::OnceClosure disconnect_handler);
void LoadBidderWorklet(
BidState& bid_state,
mojo::ConnectionErrorWithReasonCallback disconnect_handler);
const raw_ptr<Delegate> delegate_;
const raw_ptr<InterestGroupManager> interest_group_manager_;

@ -35,12 +35,18 @@ void AuctionWorkletServiceImpl::LoadBidderWorklet(
const absl::optional<GURL>& wasm_helper_url,
const absl::optional<GURL>& trusted_bidding_signals_url,
const url::Origin& top_window_origin) {
bidder_worklets_.Add(
std::make_unique<BidderWorklet>(
auction_v8_helper_, pause_for_debugger_on_start,
std::move(pending_url_loader_factory), script_source_url,
wasm_helper_url, trusted_bidding_signals_url, top_window_origin),
std::move(bidder_worklet_receiver));
auto bidder_worklet = std::make_unique<BidderWorklet>(
auction_v8_helper_, pause_for_debugger_on_start,
std::move(pending_url_loader_factory), script_source_url, wasm_helper_url,
trusted_bidding_signals_url, top_window_origin);
auto* bidder_worklet_ptr = bidder_worklet.get();
mojo::ReceiverId receiver_id = bidder_worklets_.Add(
std::move(bidder_worklet), std::move(bidder_worklet_receiver));
bidder_worklet_ptr->set_close_pipe_callback(
base::BindOnce(&AuctionWorkletServiceImpl::DisconnectBidderWorklet,
base::Unretained(this), receiver_id));
}
void AuctionWorkletServiceImpl::LoadSellerWorklet(
@ -61,4 +67,11 @@ void AuctionWorkletServiceImpl::LoadSellerWorklet(
std::move(seller_worklet_receiver));
}
void AuctionWorkletServiceImpl::DisconnectBidderWorklet(
mojo::ReceiverId receiver_id,
const std::string& reason) {
bidder_worklets_.RemoveWithReason(receiver_id, /*custom_reason_code=*/0,
reason);
}
} // namespace auction_worklet

@ -62,6 +62,9 @@ class AuctionWorkletServiceImpl : public mojom::AuctionWorkletService {
LoadSellerWorkletCallback load_seller_worklet_callback) override;
private:
void DisconnectBidderWorklet(mojo::ReceiverId receiver_id,
const std::string& reason);
mojo::Receiver<mojom::AuctionWorkletService> receiver_;
scoped_refptr<AuctionV8Helper> auction_v8_helper_;

@ -179,13 +179,6 @@ BidderWorklet::BidderWorklet(
BidderWorklet::~BidderWorklet() {
DCHECK_CALLED_ON_VALID_SEQUENCE(user_sequence_checker_);
// Invoke any pending callbacks, since destroying uninvoked Mojo callbacks can
// sometimes cause issues if the pipe they're over is still open. This is not
// strictly necessary here, since the BidderWorklet's received pipe is
// destroyed before the BidderWorklet itself is, but makes the class safer
// against refactors of the Mojo API.
FailAllPendingTasks();
debug_id_->AbortDebuggerPauses();
}
@ -215,15 +208,6 @@ void BidderWorklet::GenerateBid(
generate_bid_task->auction_start_time = auction_start_time;
generate_bid_task->callback = std::move(generate_bid_callback);
// If worklet script failed to load, or WASM was requested and failed, fail
// and exit early.
if (CodeLoadState() == LoadState::kFailure) {
DeliverBidCallbackOnUserThread(generate_bid_task,
mojom::BidderWorkletBidPtr(),
/*error_msgs=*/std::vector<std::string>());
return;
}
const auto& trusted_bidding_signals_keys =
generate_bid_task->bidder_worklet_non_shared_params
->trusted_bidding_signals_keys;
@ -267,18 +251,9 @@ void BidderWorklet::ReportWin(
report_win_task->browser_signal_seller_origin = browser_signal_seller_origin;
report_win_task->callback = std::move(report_win_callback);
// If worklet script and, if requested, WASM, aren't loaded, can't run script
// immediately.
LoadState code_load_state = CodeLoadState();
if (code_load_state != LoadState::kSuccess) {
// If required files failed to load, fail and exit early.
if (code_load_state == LoadState::kFailure) {
DeliverReportWinOnUserThread(report_win_task,
/*report_url=*/absl::optional<GURL>(),
/*errors=*/std::vector<std::string>());
}
// If not yet ready, need to wait for load to complete.
if (!IsCodeReady())
return;
}
RunReportWin(report_win_task);
}
@ -724,9 +699,6 @@ void BidderWorklet::Start() {
debug_id_,
base::BindOnce(&BidderWorklet::OnWasmDownloaded,
base::Unretained(this)));
} else {
// WASM helper is optional if not specified.
worklet_wasm_load_state_ = LoadState::kSuccess;
}
}
@ -736,16 +708,18 @@ void BidderWorklet::OnScriptDownloaded(WorkletLoader::Result worklet_script,
worklet_loader_.reset();
// Fail all pending tasks if the script failed to load.
// On failure, close pipe and delete `this`, as it can't do anything without a
// loaded script.
if (!worklet_script.success()) {
worklet_js_load_state_ = LoadState::kFailure;
if (error_msg.has_value())
load_code_error_msgs_.push_back(std::move(error_msg.value()));
FailAllPendingTasks();
std::move(close_pipe_callback_)
.Run(error_msg ? error_msg.value() : std::string());
// `this` should be deleted at this point.
return;
}
worklet_js_load_state_ = LoadState::kSuccess;
if (error_msg.has_value())
load_code_error_msgs_.push_back(std::move(error_msg.value()));
v8_runner_->PostTask(FROM_HERE,
base::BindOnce(&BidderWorklet::V8State::SetWorkletScript,
base::Unretained(v8_state_.get()),
@ -760,16 +734,19 @@ void BidderWorklet::OnWasmDownloaded(WorkletWasmLoader::Result wasm_helper,
wasm_loader_.reset();
// If the WASM helper is actually requested, its failure fails the bid.
// If the WASM helper is actually requested, delete `this` and inform the
// browser process of the failure. ReportWin() calls would theoretically still
// be allowed, but that adds a lot more complexity around BidderWorklet reuse.
if (!wasm_helper.success()) {
worklet_wasm_load_state_ = LoadState::kFailure;
if (error_msg.has_value())
load_code_error_msgs_.push_back(std::move(error_msg.value()));
FailAllPendingTasks();
std::move(close_pipe_callback_)
.Run(error_msg ? error_msg.value() : std::string());
// `this` should be deleted at this point.
return;
}
worklet_wasm_load_state_ = LoadState::kSuccess;
if (error_msg.has_value())
load_code_error_msgs_.push_back(std::move(error_msg.value()));
v8_runner_->PostTask(FROM_HERE,
base::BindOnce(&BidderWorklet::V8State::SetWasmHelper,
base::Unretained(v8_state_.get()),
@ -811,12 +788,8 @@ void BidderWorklet::OnTrustedBiddingSignalsDownloaded(
void BidderWorklet::GenerateBidIfReady(GenerateBidTaskList::iterator task) {
DCHECK_CALLED_ON_VALID_SEQUENCE(user_sequence_checker_);
// Script or WASM load failure should abort all tasks before getting here.
LoadState code_load_state = CodeLoadState();
DCHECK_NE(code_load_state, LoadState::kFailure);
if (task->trusted_bidding_signals || code_load_state != LoadState::kSuccess) {
if (task->trusted_bidding_signals || !IsCodeReady())
return;
}
// Other than the callback field, no fields of `task` are needed after this
// point, so can consume them instead of copying them.
@ -855,20 +828,6 @@ void BidderWorklet::RunReportWin(ReportWinTaskList::iterator task) {
weak_ptr_factory_.GetWeakPtr(), task)));
}
void BidderWorklet::FailAllPendingTasks() {
DCHECK_CALLED_ON_VALID_SEQUENCE(user_sequence_checker_);
while (!generate_bid_tasks_.empty()) {
DeliverBidCallbackOnUserThread(generate_bid_tasks_.begin(),
mojom::BidderWorkletBidPtr(),
/*error_msgs=*/std::vector<std::string>());
}
while (!report_win_tasks_.empty()) {
DeliverReportWinOnUserThread(report_win_tasks_.begin(),
/*report_url=*/absl::nullopt,
/*errors=*/std::vector<std::string>());
}
}
void BidderWorklet::DeliverBidCallbackOnUserThread(
GenerateBidTaskList::iterator task,
mojom::BidderWorkletBidPtr bid,
@ -896,19 +855,11 @@ void BidderWorklet::DeliverReportWinOnUserThread(
report_win_tasks_.erase(task);
}
BidderWorklet::LoadState BidderWorklet::CodeLoadState() const {
if (worklet_js_load_state_ == LoadState::kFailure ||
worklet_wasm_load_state_ == LoadState::kFailure) {
return LoadState::kFailure;
}
if (worklet_js_load_state_ == LoadState::kLoading ||
worklet_wasm_load_state_ == LoadState::kLoading) {
return LoadState::kLoading;
}
DCHECK_EQ(worklet_js_load_state_, LoadState::kSuccess);
DCHECK_EQ(worklet_wasm_load_state_, LoadState::kSuccess);
return LoadState::kSuccess;
bool BidderWorklet::IsCodeReady() const {
// If `paused_`, loading hasn't started yet. Otherwise, null loaders indicate
// the worklet script has loaded successfully, and there's no WASM helper, or
// it has also loaded successfully.
return !paused_ && !worklet_loader_ && !wasm_loader_;
}
} // namespace auction_worklet

@ -53,6 +53,11 @@ namespace auction_worklet {
// interest group in different auctions.
class BidderWorklet : public mojom::BidderWorklet {
public:
// Deletes the worklet immediately and resets the BidderWorklet's Mojo pipe
// with the provided description. See mojo::Receiver::ResetWithReason().
using ClosePipeCallback =
base::OnceCallback<void(const std::string& description)>;
// Starts loading the worklet script on construction, as well as the trusted
// bidding data, if necessary. Will then call the script's generateBid()
// function and invoke the callback with the results. Callback will always be
@ -72,6 +77,15 @@ class BidderWorklet : public mojom::BidderWorklet {
~BidderWorklet() override;
BidderWorklet& operator=(const BidderWorklet&) = delete;
// Sets the callback to be invoked on errors which require closing the pipe.
// Callback will also immediately delete `this`. Not an argument to
// constructor because the Mojo ReceiverId needs to be bound to the callback,
// but can only get that after creating the worklet. Must be called
// immediately after creating a BidderWorklet.
void set_close_pipe_callback(ClosePipeCallback close_pipe_callback) {
close_pipe_callback_ = std::move(close_pipe_callback);
}
int context_group_id_for_testing() const;
// mojom::BidderWorklet implementation:
@ -220,8 +234,6 @@ class BidderWorklet : public mojom::BidderWorklet {
SEQUENCE_CHECKER(v8_sequence_checker_);
};
enum class LoadState { kLoading, kSuccess, kFailure };
void ResumeIfPaused();
void Start();
@ -246,10 +258,6 @@ class BidderWorklet : public mojom::BidderWorklet {
void RunReportWin(ReportWinTaskList::iterator task);
// Fails all pending GenerateBid() and ReportWin() tasks, removing all tasks
// from both lists.
void FailAllPendingTasks();
// Invokes the `callback` of `task` with the provided values, and removes
// `task` from `generate_bid_tasks_`.
void DeliverBidCallbackOnUserThread(GenerateBidTaskList::iterator task,
@ -262,8 +270,9 @@ class BidderWorklet : public mojom::BidderWorklet {
absl::optional<GURL> report_url,
std::vector<std::string> errors);
// Combines `worklet_js_load_state_` and `worklet_wasm_load_state_`.
LoadState CodeLoadState() const;
// Returns true if unpaused and the script and WASM helper (if needed) have
// loaded.
bool IsCodeReady() const;
scoped_refptr<base::SequencedTaskRunner> v8_runner_;
@ -282,11 +291,9 @@ class BidderWorklet : public mojom::BidderWorklet {
// Top window origin for the auctions sharing this BidderWorklet.
const url::Origin top_window_origin_;
// These are deleted once each resource is loaded.
std::unique_ptr<WorkletLoader> worklet_loader_;
LoadState worklet_js_load_state_ = LoadState::kLoading;
std::unique_ptr<WorkletWasmLoader> wasm_loader_;
LoadState worklet_wasm_load_state_ = LoadState::kLoading;
// Lives on `v8_runner_`. Since it's deleted there via DeleteSoon, tasks can
// be safely posted from main thread to it with an Unretained pointer.
@ -298,6 +305,8 @@ class BidderWorklet : public mojom::BidderWorklet {
GenerateBidTaskList generate_bid_tasks_;
ReportWinTaskList report_win_tasks_;
ClosePipeCallback close_pipe_callback_;
// Errors that occurred while loading the code, if any.
std::vector<std::string> load_code_error_msgs_;

@ -276,12 +276,19 @@ class BidderWorkletTest : public testing::Test {
url.is_empty() ? interest_group_bidding_url_ : url,
interest_group_wasm_url_, interest_group_trusted_bidding_signals_url_,
top_window_origin_);
if (out_bidder_worklet_impl)
*out_bidder_worklet_impl = bidder_worklet_impl.get();
auto* bidder_worklet_ptr = bidder_worklet_impl.get();
mojo::Remote<mojom::BidderWorklet> bidder_worklet;
mojo::MakeSelfOwnedReceiver(std::move(bidder_worklet_impl),
bidder_worklet.BindNewPipeAndPassReceiver());
mojo::ReceiverId receiver_id =
bidder_worklets_.Add(std::move(bidder_worklet_impl),
bidder_worklet.BindNewPipeAndPassReceiver());
bidder_worklet_ptr->set_close_pipe_callback(
base::BindOnce(&BidderWorkletTest::ClosePipeCallback,
base::Unretained(this), receiver_id));
bidder_worklet.set_disconnect_with_reason_handler(base::BindRepeating(
&BidderWorkletTest::OnDisconnectWithReason, base::Unretained(this)));
if (out_bidder_worklet_impl)
*out_bidder_worklet_impl = bidder_worklet_ptr;
return bidder_worklet;
}
@ -294,6 +301,19 @@ class BidderWorkletTest : public testing::Test {
base::Unretained(this)));
}
// Calls GenerateBid(), expecting the callback never to be invoked.
void GenerateBidExpectingCallbackNotInvoked(
mojom::BidderWorklet* bidder_worklet) {
bidder_worklet->GenerateBid(
CreateBidderWorkletNonSharedParams(), auction_signals_,
per_buyer_signals_, browser_signal_seller_origin_,
CreateBiddingBrowserSignals(), auction_start_time_,
base::BindOnce([](mojom::BidderWorkletBidPtr bid,
const std::vector<std::string>& errors) {
ADD_FAILURE() << "Callback should not be invoked.";
}));
}
// Create a BidderWorklet and invokes GenerateBid(), waiting for the
// GenerateBid() callback to be invoked. Returns a null Remote on failure.
mojo::Remote<mojom::BidderWorklet> CreateWorkletAndGenerateBid() {
@ -318,7 +338,39 @@ class BidderWorkletTest : public testing::Test {
load_script_run_loop_->Quit();
}
// Waits for OnDisconnectWithReason() to be invoked, if it hasn't been
// already, and returns the error string it was invoked with.
std::string WaitForDisconnect() {
DCHECK(!disconnect_run_loop_);
if (!disconnect_reason_) {
disconnect_run_loop_ = std::make_unique<base::RunLoop>();
disconnect_run_loop_->Run();
disconnect_run_loop_.reset();
}
DCHECK(disconnect_reason_);
std::string disconnect_reason = std::move(disconnect_reason_).value();
disconnect_reason_.reset();
return disconnect_reason;
}
protected:
void ClosePipeCallback(mojo::ReceiverId receiver_id,
const std::string& description) {
bidder_worklets_.RemoveWithReason(receiver_id, /*custom_reason_code=*/0,
description);
}
void OnDisconnectWithReason(uint32_t custom_reason,
const std::string& description) {
DCHECK(!disconnect_reason_);
disconnect_reason_ = description;
if (disconnect_run_loop_)
disconnect_run_loop_->Quit();
}
std::vector<mojo::StructPtr<mojom::PreviousWin>> CloneWinList(
const std::vector<mojo::StructPtr<mojom::PreviousWin>>& prev_win_list) {
std::vector<mojo::StructPtr<mojom::PreviousWin>> out;
@ -369,6 +421,15 @@ class BidderWorkletTest : public testing::Test {
network::TestURLLoaderFactory url_loader_factory_;
scoped_refptr<AuctionV8Helper> v8_helper_;
// Reuseable run loop for disconnection errors.
std::unique_ptr<base::RunLoop> disconnect_run_loop_;
absl::optional<std::string> disconnect_reason_;
// Owns all created BidderWorklets - having a ReceiverSet allows them to have
// a ClosePipeCallback which behaves just like the one in
// AuctionWorkletServiceImpl, to better match production behavior.
mojo::UniqueReceiverSet<mojom::BidderWorklet> bidder_worklets_;
};
// Test the case the BidderWorklet pipe is closed before invoking the
@ -378,31 +439,34 @@ class BidderWorkletTest : public testing::Test {
// invoking it.
TEST_F(BidderWorkletTest, PipeClosed) {
auto bidder_worklet = CreateWorklet();
GenerateBid(bidder_worklet.get());
GenerateBidExpectingCallbackNotInvoked(bidder_worklet.get());
bidder_worklet.reset();
EXPECT_FALSE(bidder_worklets_.empty());
// This should not result in a Mojo crash.
task_environment_.RunUntilIdle();
EXPECT_TRUE(bidder_worklets_.empty());
}
TEST_F(BidderWorkletTest, NetworkError) {
url_loader_factory_.AddResponse(interest_group_bidding_url_.spec(),
CreateBasicGenerateBidScript(),
net::HTTP_NOT_FOUND);
RunGenerateBidExpectingResult(
mojom::BidderWorkletBidPtr() /* expected_bid */,
{"Failed to load https://url.test/ HTTP status = 404 Not Found."});
auto bidder_worklet = CreateWorklet();
GenerateBidExpectingCallbackNotInvoked(bidder_worklet.get());
EXPECT_EQ("Failed to load https://url.test/ HTTP status = 404 Not Found.",
WaitForDisconnect());
}
TEST_F(BidderWorkletTest, CompileError) {
AddJavascriptResponse(&url_loader_factory_, interest_group_bidding_url_,
"Invalid Javascript");
EXPECT_FALSE(CreateWorkletAndGenerateBid());
auto bidder_worklet = CreateWorklet();
GenerateBidExpectingCallbackNotInvoked(bidder_worklet.get());
EXPECT_FALSE(bid());
ASSERT_EQ(1u, bid_errors().size());
EXPECT_THAT(bid_errors()[0], StartsWith("https://url.test/:1 "));
EXPECT_THAT(bid_errors()[0], HasSubstr("SyntaxError"));
std::string error = WaitForDisconnect();
EXPECT_THAT(error, StartsWith("https://url.test/:1 "));
EXPECT_THAT(error, HasSubstr("SyntaxError"));
}
// Test parsing of return values.
@ -927,58 +991,21 @@ TEST_F(BidderWorkletTest, GenerateBidParallel) {
}
// Test multiple GenerateBid calls on a single worklet, in parallel, in the case
// the script fails to load. Do this twice, once before the worklet has
// encountered a network error, and once after, to make sure both cases work.
TEST_F(BidderWorkletTest, GenerateBidNetworkErrorParallel) {
// the script fails to load.
TEST_F(BidderWorkletTest, GenerateBidParallelLoadFails) {
auto bidder_worklet = CreateWorklet();
// For the first loop iteration, call GenerateBid repeatedly and only then
// provide the network error. For the second loop iteration, reuse the bidder
// worklet from the first iteration, so the Javascript is loaded from the
// start.
for (bool generate_bid_invoked_before_network_error : {false, true}) {
SCOPED_TRACE(generate_bid_invoked_before_network_error);
base::RunLoop run_loop;
const size_t kNumGenerateBidCalls = 10;
size_t num_generate_bid_calls = 0;
for (size_t i = 0; i < kNumGenerateBidCalls; ++i) {
bidder_worklet->GenerateBid(
CreateBidderWorkletNonSharedParams(), auction_signals_,
per_buyer_signals_, browser_signal_seller_origin_,
CreateBiddingBrowserSignals(), auction_start_time_,
base::BindLambdaForTesting(
[&run_loop, &num_generate_bid_calls](
mojom::BidderWorkletBidPtr bid,
const std::vector<std::string>& errors) {
EXPECT_FALSE(bid);
EXPECT_EQ(errors, std::vector<std::string>{
"Failed to load https://url.test/ HTTP "
"status = 404 Not Found."});
++num_generate_bid_calls;
if (num_generate_bid_calls == kNumGenerateBidCalls)
run_loop.Quit();
}));
}
// If this is the first loop iteration, wait for all the Mojo calls to
// settle, and then provide the network error.
if (generate_bid_invoked_before_network_error == false) {
// Since the script hasn't loaded yet, or failed to load, no bids should
// be generated.
task_environment_.RunUntilIdle();
EXPECT_FALSE(run_loop.AnyQuitCalled());
EXPECT_EQ(0u, num_generate_bid_calls);
// Script fails to load.
url_loader_factory_.AddResponse(interest_group_bidding_url_.spec(),
CreateBasicGenerateBidScript(),
net::HTTP_NOT_FOUND);
}
run_loop.Run();
EXPECT_EQ(kNumGenerateBidCalls, num_generate_bid_calls);
for (size_t i = 0; i < 10; ++i) {
GenerateBidExpectingCallbackNotInvoked(bidder_worklet.get());
}
// Script fails to load.
url_loader_factory_.AddResponse(interest_group_bidding_url_.spec(),
CreateBasicGenerateBidScript(),
net::HTTP_NOT_FOUND);
EXPECT_EQ("Failed to load https://url.test/ HTTP status = 404 Not Found.",
WaitForDisconnect());
}
// Test multiple GenerateBid calls on a single worklet, in parallel, in the case
@ -1463,11 +1490,16 @@ TEST_F(BidderWorkletTest, GenerateBidWasm404) {
/*charset=*/absl::nullopt, "Error 404", kAllowFledgeHeader,
net::HTTP_NOT_FOUND);
RunGenerateBidWithJavascriptExpectingResult(
CreateBasicGenerateBidScript(),
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
{"Failed to load https://foo.test/helper.wasm "
"HTTP status = 404 Not Found."});
// The Javascript request receives valid JS.
AddJavascriptResponse(&url_loader_factory_, interest_group_bidding_url_,
CreateBasicGenerateBidScript());
auto bidder_worklet = CreateWorklet();
GenerateBidExpectingCallbackNotInvoked(bidder_worklet.get());
EXPECT_EQ(
"Failed to load https://foo.test/helper.wasm "
"HTTP status = 404 Not Found.",
WaitForDisconnect());
}
TEST_F(BidderWorkletTest, GenerateBidWasmFailure) {
@ -1477,12 +1509,17 @@ TEST_F(BidderWorkletTest, GenerateBidWasmFailure) {
kWasmMimeType,
/*charset=*/absl::nullopt, CreateBasicGenerateBidScript());
RunGenerateBidWithJavascriptExpectingResult(
CreateBasicGenerateBidScript(),
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
{"https://foo.test/helper.wasm Uncaught CompileError: "
"WasmModuleObject::Compile(): expected magic word 00 61 73 6d, found "
"0a 20 20 20 @+0."});
// The Javascript request receives valid JS.
AddJavascriptResponse(&url_loader_factory_, interest_group_bidding_url_,
CreateBasicGenerateBidScript());
auto bidder_worklet = CreateWorklet();
GenerateBidExpectingCallbackNotInvoked(bidder_worklet.get());
EXPECT_EQ(
"https://foo.test/helper.wasm Uncaught CompileError: "
"WasmModuleObject::Compile(): expected magic word 00 61 73 6d, found "
"0a 20 20 20 @+0.",
WaitForDisconnect());
}
TEST_F(BidderWorkletTest, GenerateBidWasm) {
@ -1561,8 +1598,14 @@ TEST_F(BidderWorkletTest, WasmOrdering) {
url_loader_factory_.ClearResponses();
mojo::Remote<mojom::BidderWorklet> bidder_worklet = CreateWorklet();
load_script_run_loop_ = std::make_unique<base::RunLoop>();
GenerateBid(bidder_worklet.get());
if (test.expect_success) {
// On success, callback should be invoked.
load_script_run_loop_ = std::make_unique<base::RunLoop>();
GenerateBid(bidder_worklet.get());
} else {
// On error, the pipe is closed without invoking the callback.
GenerateBidExpectingCallbackNotInvoked(bidder_worklet.get());
}
for (Event ev : test.events) {
switch (ev) {
@ -1591,9 +1634,16 @@ TEST_F(BidderWorkletTest, WasmOrdering) {
task_environment_.RunUntilIdle();
}
load_script_run_loop_->Run();
load_script_run_loop_.reset();
EXPECT_EQ(bid_.is_null(), !test.expect_success);
if (test.expect_success) {
// On success, the callback is invoked.
load_script_run_loop_->Run();
load_script_run_loop_.reset();
EXPECT_TRUE(bid_);
} else {
// On failure, the pipe is closed with a non-empty error message, without
// invoking the callback.
EXPECT_FALSE(WaitForDisconnect().empty());
}
}
}
@ -1872,54 +1922,28 @@ TEST_F(BidderWorkletTest, ReportWinParallel) {
}
// Test multiple ReportWin calls on a single worklet, in parallel, in the case
// the worklet script fails to load. Do this twice, once before the worklet
// script has received an error, and once after, to make sure both cases work.
TEST_F(BidderWorkletTest, ReportWinNetworkErrorParallel) {
// the worklet script fails to load.
TEST_F(BidderWorkletTest, ReportWinParallelLoadFails) {
auto bidder_worklet = CreateWorklet();
// For the first loop iteration, call ReportWin repeatedly before providing
// the network error script, then provide the network error. For the second
// loop iteration, reuse the bidder worklet from the first iteration, so the
// network error is present from the start.
for (bool report_win_invoked_before_worklet_script_loaded : {false, true}) {
SCOPED_TRACE(report_win_invoked_before_worklet_script_loaded);
base::RunLoop run_loop;
const size_t kNumReportWinCalls = 10;
size_t num_report_win_calls = 0;
for (size_t i = 0; i < kNumReportWinCalls; ++i) {
bidder_worklet->ReportWin(
interest_group_name_,
/*auction_signals_json=*/base::NumberToString(i), per_buyer_signals_,
seller_signals_, browser_signal_render_url_, browser_signal_bid_,
browser_signal_seller_origin_,
base::BindLambdaForTesting(
[&run_loop, &num_report_win_calls](
const absl::optional<GURL>& report_url,
const std::vector<std::string>& errors) {
EXPECT_FALSE(report_url);
EXPECT_EQ(errors, std::vector<std::string>{
"Failed to load https://url.test/ "
"HTTP status = 404 Not Found."});
++num_report_win_calls;
if (num_report_win_calls == kNumReportWinCalls)
run_loop.Quit();
}));
}
// If this is the first loop iteration, wait for all the Mojo calls to
// settle, and then provide the Javascript response body.
if (report_win_invoked_before_worklet_script_loaded == false) {
task_environment_.RunUntilIdle();
EXPECT_FALSE(run_loop.AnyQuitCalled());
url_loader_factory_.AddResponse(interest_group_bidding_url_.spec(),
CreateBasicGenerateBidScript(),
net::HTTP_NOT_FOUND);
}
run_loop.Run();
EXPECT_EQ(kNumReportWinCalls, num_report_win_calls);
for (size_t i = 0; i < 10; ++i) {
bidder_worklet->ReportWin(
interest_group_name_,
/*auction_signals_json=*/base::NumberToString(i), per_buyer_signals_,
seller_signals_, browser_signal_render_url_, browser_signal_bid_,
browser_signal_seller_origin_,
base::BindOnce([](const absl::optional<GURL>& report_url,
const std::vector<std::string>& errors) {
ADD_FAILURE() << "Callback should not be invoked.";
}));
}
url_loader_factory_.AddResponse(interest_group_bidding_url_.spec(),
CreateBasicGenerateBidScript(),
net::HTTP_NOT_FOUND);
EXPECT_EQ("Failed to load https://url.test/ HTTP status = 404 Not Found.",
WaitForDisconnect());
}
// Make sure Date() is not available when running reportWin().
@ -2272,7 +2296,7 @@ TEST_F(BidderWorkletTest, ParseErrorV8Debug) {
auto worklet =
CreateWorklet(interest_group_bidding_url_,
/*pause_for_debugger_on_start=*/true, &worklet_impl);
GenerateBid(worklet.get());
GenerateBidExpectingCallbackNotInvoked(worklet.get());
int id = worklet_impl->context_group_id_for_testing();
TestChannel* channel = inspector_support.ConnectDebuggerSession(id);
@ -2283,14 +2307,13 @@ TEST_F(BidderWorkletTest, ParseErrorV8Debug) {
R"({"id":2,"method":"Debugger.enable","params":{}})");
// Unpause execution.
load_script_run_loop_ = std::make_unique<base::RunLoop>();
channel->RunCommandAndWaitForResult(
3, "Runtime.runIfWaitingForDebugger",
R"({"id":3,"method":"Runtime.runIfWaitingForDebugger","params":{}})");
load_script_run_loop_->Run();
load_script_run_loop_.reset();
EXPECT_FALSE(bid_);
// Worklet should disconnect with an error message.
EXPECT_FALSE(WaitForDisconnect().empty());
// Should have gotten a parse error notification.
TestChannel::Event parse_error =
channel->WaitForMethodNotification("Debugger.scriptFailedToParse");

@ -32,11 +32,12 @@ interface AuctionWorkletService {
// the DevTools hooks restrict sharing to auctions within a single
// RenderFrame.
//
// There is no return value - any error loading the worklet script may only be
// learned from the BidderWorklet API itself, when trying to run a script.
// This reduces roundtrips, and allows fetching per-auction bidding signals
// URL in parallel with fetching the bidding worklet script. It also makes the
// API more robust, not having any invocation ordering requirements.
// All methods may be invoked immediately upon creation. If there's a pending
// load of the script, callbacks will be delayed until it completes.
//
// On load error, the worklet will close its pipe with a reason string. The
// reason string, and worklet errors messages more generally, are considered
// privileged and should not be passed to renderer processes.
//
// Arguments:
// `bidder_worklet` The pipe to communicate with the BidderWorklet. Closing