0

Add RetryOptions to blink and plumb with mojo

This CL adds the RetryOptions idl and FetchRetryOptions mojom, which
allows JS-configured fetch retry policies. This value will be used in
crrev.com/c/6471874 which implements the actual retry logic.

Explainer: https://github.com/explainers-by-googlers/fetch-retry

Bug: 417930271
Change-Id: I17b40c4c887e1d680527dc02fb583d5c264ced46
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6490773
Reviewed-by: Kouhei Ueno <kouhei@chromium.org>
Reviewed-by: Daniel Cheng <dcheng@chromium.org>
Reviewed-by: Takashi Toyoshima <toyoshim@chromium.org>
Commit-Queue: Rakina Zata Amni <rakina@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1460675}
This commit is contained in:
Rakina Zata Amni
2025-05-15 06:26:29 -07:00
committed by Chromium LUCI CQ
parent 19a26c304c
commit e8d806fd94
22 changed files with 268 additions and 4 deletions

@ -123,7 +123,8 @@ namespace {
DO_FIELD(socket_tag) __VA_ARGS__ \
DO_FIELD(keepalive_token) __VA_ARGS__ \
DO_FIELD(allows_device_bound_session_registration) __VA_ARGS__ \
DO_FIELD(permissions_policy)
DO_FIELD(permissions_policy) __VA_ARGS__ \
DO_FIELD(fetch_retry_options)
// clang-format on

@ -248,6 +248,18 @@ component("integrity_policy") {
defines = [ "IS_NETWORK_CPP_INTEGRITY_POLICY_IMPL" ]
}
component("fetch_retry_options") {
sources = [
"fetch_retry_options.cc",
"fetch_retry_options.h",
]
deps = [
"//net",
"//services/network/public/mojom:url_loader_base_shared",
]
defines = [ "IS_NETWORK_CPP_FETCH_RETRY_OPTIONS_IMPL" ]
}
component("cookies_mojom_support") {
sources = [
"cookie_manager_shared_mojom_traits.cc",
@ -597,6 +609,7 @@ component("cpp_base") {
":crash_keys",
":cross_origin_embedder_policy",
":document_isolation_policy",
":fetch_retry_options",
":integrity_policy",
":ip_address_mojom_support",
":network_param_mojom_support",

@ -0,0 +1,20 @@
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "services/network/public/cpp/fetch_retry_options.h"
namespace network {
FetchRetryOptions::FetchRetryOptions() = default;
FetchRetryOptions::~FetchRetryOptions() = default;
FetchRetryOptions::FetchRetryOptions(FetchRetryOptions&&) = default;
FetchRetryOptions& FetchRetryOptions::operator=(FetchRetryOptions&&) = default;
FetchRetryOptions::FetchRetryOptions(const FetchRetryOptions&) = default;
FetchRetryOptions& FetchRetryOptions::operator=(const FetchRetryOptions&) =
default;
bool FetchRetryOptions::operator==(const FetchRetryOptions&) const = default;
} // namespace network

@ -0,0 +1,43 @@
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef SERVICES_NETWORK_PUBLIC_CPP_FETCH_RETRY_OPTIONS_H_
#define SERVICES_NETWORK_PUBLIC_CPP_FETCH_RETRY_OPTIONS_H_
#include <cstdint>
#include <string>
#include <vector>
#include "base/component_export.h"
#include "services/network/public/mojom/fetch_retry_options.mojom-shared.h"
namespace network {
// This implements a data structure holding the policy configurations for fetch
// retry feature.
//
// This struct is needed so that we can pass the same object when plumbing.
struct COMPONENT_EXPORT(NETWORK_CPP_FETCH_RETRY_OPTIONS) FetchRetryOptions {
FetchRetryOptions();
~FetchRetryOptions();
FetchRetryOptions(FetchRetryOptions&&);
FetchRetryOptions& operator=(FetchRetryOptions&&);
FetchRetryOptions(const FetchRetryOptions&);
FetchRetryOptions& operator=(const FetchRetryOptions&);
bool operator==(const FetchRetryOptions&) const;
uint32_t max_attempts = 0;
std::optional<base::TimeDelta> initial_delay;
std::optional<double> backoff_factor;
std::optional<base::TimeDelta> max_age;
bool retry_after_unload = false;
bool retry_non_idempotent = false;
};
} // namespace network
#endif // SERVICES_NETWORK_PUBLIC_CPP_FETCH_RETRY_OPTIONS_H_

@ -349,7 +349,8 @@ bool ResourceRequest::EqualsForTesting(const ResourceRequest& request) const {
socket_tag == request.socket_tag &&
allows_device_bound_session_registration ==
request.allows_device_bound_session_registration &&
permissions_policy == request.permissions_policy;
permissions_policy == request.permissions_policy &&
fetch_retry_options == request.fetch_retry_options;
}
bool ResourceRequest::SendsCookies() const {

@ -24,6 +24,7 @@
#include "net/socket/socket_tag.h"
#include "net/storage_access_api/status.h"
#include "net/url_request/referrer_policy.h"
#include "services/network/public/cpp/fetch_retry_options.h"
#include "services/network/public/cpp/optional_trust_token_params.h"
#include "services/network/public/cpp/permissions_policy/permissions_policy.h"
#include "services/network/public/cpp/resource_request_body.h"
@ -252,6 +253,8 @@ struct COMPONENT_EXPORT(NETWORK_CPP_BASE) ResourceRequest {
bool allows_device_bound_session_registration = false;
std::optional<network::PermissionsPolicy> permissions_policy;
std::optional<network::FetchRetryOptions> fetch_retry_options;
};
// LINT.ThenChange(//services/network/prefetch_matches.cc)

@ -26,6 +26,7 @@
#include "services/network/public/mojom/data_pipe_getter.mojom.h"
#include "services/network/public/mojom/device_bound_sessions.mojom.h"
#include "services/network/public/mojom/devtools_observer.mojom.h"
#include "services/network/public/mojom/fetch_retry_options.mojom.h"
#include "services/network/public/mojom/ip_address_space.mojom.h"
#include "services/network/public/mojom/trust_token_access_observer.mojom.h"
#include "services/network/public/mojom/trust_tokens.mojom.h"
@ -139,7 +140,8 @@ bool StructTraits<
!data.ReadKeepaliveToken(&out->keepalive_token) ||
!data.ReadStorageAccessApiStatus(&out->storage_access_api_status) ||
!data.ReadSocketTag(&out->socket_tag) ||
!data.ReadPermissionsPolicy(&out->permissions_policy)) {
!data.ReadPermissionsPolicy(&out->permissions_policy) ||
!data.ReadFetchRetryOptions(&out->fetch_retry_options)) {
// Note that data.ReadTrustTokenParams is temporarily handled below.
return false;
}
@ -314,4 +316,19 @@ bool StructTraits<network::mojom::SocketTagDataView, net::SocketTag>::Read(
return true;
}
bool StructTraits<network::mojom::FetchRetryOptionsDataView,
network::FetchRetryOptions>::
Read(network::mojom::FetchRetryOptionsDataView data,
FetchRetryOptions* out) {
out->max_attempts = data.max_attempts();
if (!data.ReadInitialDelay(&out->initial_delay) ||
!data.ReadMaxAge(&out->max_age)) {
return false;
}
out->backoff_factor = data.backoff_factor();
out->retry_after_unload = data.retry_after_unload();
out->retry_non_idempotent = data.retry_non_idempotent();
return true;
}
} // namespace mojo

@ -36,6 +36,7 @@
#include "services/network/public/mojom/data_pipe_getter.mojom.h"
#include "services/network/public/mojom/device_bound_sessions.mojom-forward.h"
#include "services/network/public/mojom/devtools_observer.mojom-forward.h"
#include "services/network/public/mojom/fetch_retry_options.mojom.h"
#include "services/network/public/mojom/ip_address_space.mojom-forward.h"
#include "services/network/public/mojom/trust_token_access_observer.mojom-forward.h"
#include "services/network/public/mojom/trust_tokens.mojom-forward.h"
@ -424,6 +425,10 @@ struct COMPONENT_EXPORT(NETWORK_CPP_BASE)
const network::ResourceRequest& request) {
return request.permissions_policy;
}
static const std::optional<network::FetchRetryOptions>& fetch_retry_options(
const network::ResourceRequest& request) {
return request.fetch_retry_options;
}
static bool Read(network::mojom::URLRequestDataView data,
network::ResourceRequest* out);
@ -570,6 +575,38 @@ struct COMPONENT_EXPORT(NETWORK_CPP_BASE)
static bool Read(network::mojom::SocketTagDataView data, net::SocketTag* out);
};
template <>
struct COMPONENT_EXPORT(NETWORK_CPP_BASE)
StructTraits<network::mojom::FetchRetryOptionsDataView,
network::FetchRetryOptions> {
using FetchRetryOptions = network::FetchRetryOptions;
static uint32_t max_attempts(const FetchRetryOptions& options) {
return options.max_attempts;
}
static std::optional<base::TimeDelta> initial_delay(
const FetchRetryOptions& options) {
return options.initial_delay;
}
static std::optional<base::TimeDelta> max_age(
const FetchRetryOptions& options) {
return options.max_age;
}
static const std::optional<double>& backoff_factor(
const FetchRetryOptions& options) {
return options.backoff_factor;
}
static bool retry_after_unload(const FetchRetryOptions& options) {
return options.retry_after_unload;
}
static bool retry_non_idempotent(const FetchRetryOptions& options) {
return options.retry_non_idempotent;
}
static bool Read(network::mojom::FetchRetryOptionsDataView data,
network::FetchRetryOptions* out);
};
} // namespace mojo
#endif // SERVICES_NETWORK_PUBLIC_CPP_URL_REQUEST_MOJOM_TRAITS_H_

@ -613,6 +613,7 @@ mojom("url_loader_base") {
"early_hints.mojom",
"encoded_body_length.mojom",
"fetch_api.mojom",
"fetch_retry_options.mojom",
"http_raw_headers.mojom",
"http_request_headers.mojom",
"ip_address_space.mojom",

@ -0,0 +1,18 @@
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
module network.mojom;
import "mojo/public/mojom/base/time.mojom";
// See third_party/blink/renderer/core/fetch/retry_options.idl for details of
// each member.
struct FetchRetryOptions {
uint32 max_attempts;
mojo_base.mojom.TimeDelta? initial_delay;
double? backoff_factor;
mojo_base.mojom.TimeDelta? max_age;
bool retry_after_unload = false;
bool retry_non_idempotent = false;
};

@ -19,6 +19,7 @@ import "services/network/public/mojom/data_pipe_getter.mojom";
import "services/network/public/mojom/device_bound_sessions.mojom";
import "services/network/public/mojom/devtools_observer.mojom";
import "services/network/public/mojom/fetch_api.mojom";
import "services/network/public/mojom/fetch_retry_options.mojom";
import "services/network/public/mojom/http_raw_headers.mojom";
import "services/network/public/mojom/http_request_headers.mojom";
import "services/network/public/mojom/ip_address_space.mojom";
@ -611,6 +612,10 @@ struct URLRequest {
// If it's nullopt then the request's initiator didn't set the permissions
// policy. Example: https://crrev.com/c/6295903
network.mojom.PermissionsPolicy? permissions_policy;
// The policies to be used when retrying a fetch. Will only be set on fetch
// requests who specified its RetryOptions.
network.mojom.FetchRetryOptions? fetch_retry_options;
};
// URLRequestBody represents body (i.e. upload data) of a HTTP request.

@ -396,6 +396,8 @@ generated_dictionary_sources_in_core = [
"$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_resize_observer_options.h",
"$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_response_init.cc",
"$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_response_init.h",
"$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_retry_options.cc",
"$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_retry_options.h",
"$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_sanitizer_attribute_namespace.cc",
"$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_sanitizer_attribute_namespace.h",
"$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_sanitizer_config.cc",

@ -277,6 +277,7 @@ static_idl_files_in_core = [
"//third_party/blink/renderer/core/fetch/request_init.idl",
"//third_party/blink/renderer/core/fetch/response.idl",
"//third_party/blink/renderer/core/fetch/response_init.idl",
"//third_party/blink/renderer/core/fetch/retry_options.idl",
"//third_party/blink/renderer/core/fetch/window_fetch.idl",
"//third_party/blink/renderer/core/fetch/worker_fetch.idl",
"//third_party/blink/renderer/core/fileapi/blob.idl",

@ -1159,6 +1159,10 @@ void FetchLoaderBase::PerformHTTPFetch(ExceptionState& exception_state) {
UseCounter::Count(execution_context_, mojom::WebFeature::kFetchKeepalive);
}
if (fetch_request_data_->HasRetryOptions()) {
request.SetFetchRetryOptions(fetch_request_data_->RetryOptions().value());
}
request.SetBrowsingTopics(fetch_request_data_->BrowsingTopics());
request.SetAdAuctionHeaders(fetch_request_data_->AdAuctionHeaders());
request.SetAttributionReportingEligibility(

@ -247,6 +247,7 @@ FetchRequestData* FetchRequestData::CloneExceptBody() {
request->attribution_reporting_support_ = attribution_reporting_support_;
request->service_worker_race_network_request_token_ =
service_worker_race_network_request_token_;
request->retry_options_ = retry_options_;
return request;
}

@ -10,6 +10,7 @@
#include "base/memory/scoped_refptr.h"
#include "base/unguessable_token.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "services/network/public/cpp/fetch_retry_options.h"
#include "services/network/public/mojom/attribution.mojom-blink.h"
#include "services/network/public/mojom/fetch_api.mojom-blink-forward.h"
#include "services/network/public/mojom/referrer_policy.mojom-blink-forward.h"
@ -205,6 +206,16 @@ class CORE_EXPORT FetchRequestData final
service_worker_race_network_request_token_ = token;
}
bool HasRetryOptions() const { return retry_options_.has_value(); }
const std::optional<network::FetchRetryOptions>& RetryOptions() const {
return retry_options_;
}
void SetRetryOptions(network::FetchRetryOptions retry_options) {
retry_options_ = retry_options;
}
void Trace(Visitor*) const;
private:
@ -260,6 +271,7 @@ class CORE_EXPORT FetchRequestData final
network::mojom::AttributionReportingEligibility::kUnset;
network::mojom::AttributionSupport attribution_reporting_support_ =
network::mojom::AttributionSupport::kUnset;
std::optional<network::FetchRetryOptions> retry_options_;
// A specific factory that should be used for this request instead of whatever
// the system would otherwise decide to use to load this request.
// Currently used for blob: URLs, to ensure they can still be loaded even if

@ -30,6 +30,7 @@
#include "third_party/blink/renderer/bindings/core/v8/v8_request_init.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_request_mode.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_request_redirect.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_retry_options.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_union_request_usvstring.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_url_search_params.h"
#include "third_party/blink/renderer/core/dom/abort_signal.h"
@ -182,6 +183,9 @@ FetchRequestData* CreateCopyOfFetchRequestDataForFetch(
request->SetAttributionReportingSupport(original->AttributionSupport());
request->SetServiceWorkerRaceNetworkRequestToken(
original->ServiceWorkerRaceNetworkRequestToken());
if (original->HasRetryOptions()) {
request->SetRetryOptions(original->RetryOptions().value());
}
// When a new request is created from another the destination is always reset
// to be `kEmpty`. In order to facilitate some later checks when a service
@ -205,7 +209,8 @@ static bool AreAnyMembersPresent(const RequestInit* init) {
init->hasKeepalive() || init->hasBrowsingTopics() ||
init->hasAdAuctionHeaders() || init->hasSharedStorageWritable() ||
init->hasPriority() || init->hasSignal() || init->hasDuplex() ||
init->hasPrivateToken() || init->hasAttributionReporting();
init->hasPrivateToken() || init->hasAttributionReporting() ||
init->hasRetryOptions();
}
static BodyStreamBuffer* ExtractBody(ScriptState* script_state,
@ -642,6 +647,25 @@ Request* Request::CreateRequestWithRequestOrString(
if (init->hasKeepalive())
request->SetKeepalive(init->keepalive());
if (init->hasRetryOptions()) {
network::FetchRetryOptions options;
RetryOptions* retry_options = init->retryOptions();
options.max_attempts = retry_options->maxAttempts();
if (retry_options->hasInitialDelay()) {
options.initial_delay =
base::Milliseconds(retry_options->initialDelay().value());
}
if (retry_options->hasBackoffFactor()) {
options.backoff_factor = retry_options->backoffFactor();
}
if (retry_options->hasMaxAge()) {
options.max_age = base::Milliseconds(retry_options->maxAge().value());
}
options.retry_after_unload = retry_options->retryAfterUnload();
options.retry_non_idempotent = retry_options->retryNonIdempotent();
request->SetRetryOptions(options);
}
if (init->hasBrowsingTopics()) {
if (!execution_context->IsSecureContext()) {
exception_state.ThrowTypeError(

@ -33,6 +33,7 @@ dictionary RequestInit {
// because the SecureContext IDL attribute doesn't affect dictionary members.
[RuntimeEnabled=PrivateStateTokens] PrivateToken privateToken;
[RuntimeEnabled=AttributionReporting] AttributionReportingRequestOptions attributionReporting;
[RuntimeEnabled=FetchRetry] RetryOptions retryOptions;
// TODO(domfarolino): add support for RequestInit window member.
//any window; // can only be set to null
};

@ -0,0 +1,39 @@
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Explainer: https://github.com/explainers-by-googlers/fetch-retry.
// Note: In the final form, we might remove some of these settings if e.g. we
// ended up not having use cases for them / browser-controlled policies are
// enough. We will re-evaluate them after origin trial.
dictionary RetryOptions {
// Required: Maximum number of retry attempts after the initial one fails.
// A value of 0 means no retries beyond the initial attempt.
required unsigned long maxAttempts;
// Optional: Delay before the first retry attempt in milliseconds.
// Defaults to browser-configured value if not specified.
unsigned long? initialDelay;
// Optional: Multiplier for increasing delay between retries (e.g., 2.0 for exponential backoff).
// A factor of 1.0 means fixed delay. Defaults to browser-configured value if not specified.
double? backoffFactor;
// Optional: Maximum total time allowed for all retry attempts in milliseconds,
// measured from when the first attempt fails. If this duration is exceeded,
// no further retries will be made, even if maxAttempts has not been reached.
// Defaults to browser-configured value if not specified.
unsigned long? maxAge;
// Optional: Controls whether the browser should continue attempting retries
// even after the originating document has been unloaded.
// This requires `keepalive: true` to be set on the Request.
// Defaults to false.
boolean retryAfterUnload = false;
// Optional: Specifies whether to retry when the HTTP request method is
// non-idempotent (e.g. POST, PUT, DELETE). If this is not set while the HTTP
// request method of the fetch is non-idempotent, no retry will be attempted.
// Defaults to false.
boolean retryNonIdempotent = false;
};

@ -38,6 +38,7 @@
#include "net/filter/source_stream_type.h"
#include "net/storage_access_api/status.h"
#include "services/metrics/public/cpp/ukm_source_id.h"
#include "services/network/public/cpp/fetch_retry_options.h"
#include "services/network/public/mojom/attribution.mojom-blink.h"
#include "services/network/public/mojom/chunked_data_pipe_getter.mojom-blink-forward.h"
#include "services/network/public/mojom/cors.mojom-blink-forward.h"
@ -292,6 +293,16 @@ class PLATFORM_EXPORT ResourceRequestHead {
: std::nullopt;
}
bool HasFetchRetryOptions() const { return fetch_retry_options_.has_value(); }
const std::optional<network::FetchRetryOptions>& FetchRetryOptions() const {
return fetch_retry_options_;
}
void SetFetchRetryOptions(
const network::FetchRetryOptions& fetch_retry_options) {
fetch_retry_options_ = fetch_retry_options;
}
// True if the request should be considered for computing and attaching the
// topics headers.
bool GetBrowsingTopics() const { return browsing_topics_; }
@ -834,6 +845,8 @@ class PLATFORM_EXPORT ResourceRequestHead {
// TODO(crbug.com/382527001): Consider merge this field with `keepalive_`.
std::optional<base::UnguessableToken> keepalive_token_;
std::optional<network::FetchRetryOptions> fetch_retry_options_;
#if DCHECK_IS_ON()
bool is_set_url_allowed_ = true;
#endif

@ -21,6 +21,7 @@
#include "services/network/public/mojom/chunked_data_pipe_getter.mojom-blink.h"
#include "services/network/public/mojom/data_pipe_getter.mojom-blink.h"
#include "services/network/public/mojom/data_pipe_getter.mojom.h"
#include "services/network/public/mojom/fetch_retry_options.mojom-shared.h"
#include "services/network/public/mojom/trust_tokens.mojom-blink.h"
#include "services/network/public/mojom/trust_tokens.mojom.h"
#include "third_party/blink/public/common/loader/network_utils.h"
@ -362,6 +363,9 @@ void PopulateResourceRequest(const ResourceRequestHead& src,
dest->throttling_profile_id = src.GetDevToolsToken();
dest->trust_token_params = ConvertTrustTokenParams(src.TrustTokenParams());
dest->required_ip_address_space = src.GetTargetAddressSpace();
if (src.HasFetchRetryOptions()) {
dest->fetch_retry_options = src.FetchRetryOptions();
}
if (base::UnguessableToken window_id = src.GetFetchWindowId())
dest->fetch_window_id = std::make_optional(window_id);

@ -2065,6 +2065,10 @@
name: "FetchLaterAPI",
status: "stable",
},
{
name: "FetchRetry",
status: "experimental",
},
{
name: "FetchUploadStreaming",
status: "stable",