commit 9f0b1c96f7918d1ead5cb1089ebfaf9bb6f63234
parent 3b6c1fee6bfd6b8732b99143a716c415405b1ce9
Author: Valentin Gosu <valentin.gosu@gmail.com>
Date: Fri, 12 Dec 2025 14:06:16 +0000
Bug 1999247 - Make NEL implementation use ReportDeliver::Fetch to enqueue report r=necko-reviewers,jesup
Differential Revision: https://phabricator.services.mozilla.com/D271979
Diffstat:
7 files changed, 114 insertions(+), 59 deletions(-)
diff --git a/netwerk/protocol/http/NetworkErrorLogging.sys.mjs b/netwerk/protocol/http/NetworkErrorLogging.sys.mjs
@@ -270,7 +270,7 @@ export class NetworkErrorLogging {
!Services.scriptSecurityManager.getChannelResultPrincipal(aChannel)
.isOriginPotentiallyTrustworthy
) {
- return;
+ return null;
}
// 2. Let origin be request's origin.
let origin =
@@ -283,7 +283,7 @@ export class NetworkErrorLogging {
origin: policyOrigin,
} = this.choosePolicyForRequest(aChannel);
if (!policy) {
- return;
+ return null;
}
// 4. Determine the active sampling rate for this request:
@@ -302,7 +302,7 @@ export class NetworkErrorLogging {
// 5. Decide whether or not to report on this request. Let roll be a random number between 0.0 and 1.0, inclusive. If roll ≥ sampling rate, return null.
if (Math.random() >= samplingRate) {
- return;
+ return null;
}
// 6. Let report body be a new ECMAScript object with the following properties:
@@ -342,7 +342,7 @@ export class NetworkErrorLogging {
policy.subdomains &&
report_body.phase != "dns"
) {
- return;
+ return null;
}
// 10. If report body's phase property is not dns, and report body's server_ip property is non-empty and not equal to policy's received IP address:
@@ -378,35 +378,25 @@ export class NetworkErrorLogging {
if (report_body.phase == "dns" || report_body.phase == "connection") {
uriMutator.setPathQueryRef("");
}
+ if (aChannel.URI.hasUserPass) {
+ uriMutator.setUserPass("");
+ }
+ let url = uriMutator.finalize().spec;
// 4. Generate a network report given these parameters:
- let report = {
- type: "network-error",
- url: uriMutator.finalize().spec,
- user_agent: Cc["@mozilla.org/network/protocol;1?name=http"].getService(
- Ci.nsIHttpProtocolHandler
- ).userAgent,
- body: report_body,
+ // nsINetworkErrorReport
+ let retObj = {
+ body: JSON.stringify(report_body),
+ group: policy.reportTo.group,
+ url,
};
- // XXX: this would benefit from using the actual reporting API,
- // but it's not clear how easy it is to:
- // - use it in the parent process
- // - have it use the Report-To header
- // https://w3c.github.io/reporting/#queue-report
+
if (policy && policy.reportTo.group === policy.nel.report_to) {
- // TODO: defer to later.
- fetch(policy.reportTo.endpoints[0].url, {
- method: "POST",
- mode: "cors",
- credentials: "omit",
- headers: {
- "Content-Type": "application/reports+json",
- },
- body: JSON.stringify([report]),
- triggeringPrincipal:
- Services.scriptSecurityManager.getChannelResultPrincipal(aChannel),
- });
+ // nsHttpChannel will call ReportDeliver::Fetch
+ return retObj;
}
+
+ return null;
}
QueryInterface = ChromeUtils.generateQI(["nsINetworkErrorLogging"]);
diff --git a/netwerk/protocol/http/nsHttpChannel.cpp b/netwerk/protocol/http/nsHttpChannel.cpp
@@ -155,6 +155,9 @@
# include "mozilla/StaticPrefs_fuzzing.h"
#endif
+#include "mozilla/dom/ReportDeliver.h"
+#include "mozilla/dom/ReportingHeader.h"
+
namespace mozilla {
using namespace dom;
@@ -4546,6 +4549,76 @@ bool nsHttpChannel::ShouldBypassProcessNotModified() {
return false;
}
+void nsHttpChannel::MaybeGenerateNELReport() {
+ if (!StaticPrefs::network_http_network_error_logging_enabled()) {
+ return;
+ }
+
+ nsCOMPtr<nsINetworkErrorReport> report;
+ if (nsCOMPtr<nsINetworkErrorLogging> nel =
+ components::NetworkErrorLogging::Service()) {
+ nel->GenerateNELReport(this, getter_AddRefs(report));
+ }
+
+ mReportedNEL = true;
+
+ // https://www.w3.org/TR/2023/WD-network-error-logging-20231005/#deliver-a-network-report
+ // 4. Generate a network report given these parameters:
+ // type: network-error
+ // url
+ // user_agent
+ // body
+
+ if (!report) {
+ return;
+ }
+
+ nsCOMPtr<nsIScriptSecurityManager> ssm = nsContentUtils::GetSecurityManager();
+ if (!ssm) {
+ return;
+ }
+
+ nsCOMPtr<nsIPrincipal> channelPrincipal;
+ ssm->GetChannelResultPrincipal(this, getter_AddRefs(channelPrincipal));
+ if (!channelPrincipal) {
+ return;
+ }
+
+ nsAutoCString body;
+ nsAutoString group;
+ nsAutoString url;
+
+ report->GetBody(body);
+ report->GetGroup(group);
+ report->GetUrl(url);
+
+ nsAutoCString endpointURL;
+ ReportingHeader::GetEndpointForReportIncludeSubdomains(
+ group, channelPrincipal, /* includeSubdomains */ true, endpointURL);
+ if (endpointURL.IsEmpty()) {
+ return;
+ }
+
+ ReportDeliver::ReportData data;
+ data.mType = u"network-error"_ns;
+ data.mGroupName = group;
+ data.mURL = url;
+ data.mFailures = 0;
+ data.mCreationTime = TimeStamp::Now();
+
+ data.mPrincipal = channelPrincipal;
+ data.mEndpointURL = endpointURL;
+ data.mReportBodyJSON = body;
+ nsAutoCString userAgent;
+ // XXX(valentin): Should this be the potentially user set value of the header
+ // or the current value of user_agent from http handler?
+ (void)mRequestHead.GetHeader(nsHttp::User_Agent, userAgent);
+ data.mUserAgent = NS_ConvertUTF8toUTF16(userAgent);
+
+ // Enqueue the report to be delivered by the reporting API
+ ReportDeliver::Fetch(data);
+}
+
nsresult nsHttpChannel::ProcessNotModified(
const std::function<nsresult(nsHttpChannel*, nsresult)>&
aContinueProcessResponseFunc) {
@@ -4598,13 +4671,8 @@ nsresult nsHttpChannel::ProcessNotModified(
rv = mCacheEntry->SetMetaDataElement("response-head", head.get());
if (NS_FAILED(rv)) return rv;
- if (StaticPrefs::network_http_network_error_logging_enabled() &&
- LoadUsedNetwork() && !mReportedNEL) {
- if (nsCOMPtr<nsINetworkErrorLogging> nel =
- components::NetworkErrorLogging::Service()) {
- nel->GenerateNELReport(this);
- }
- mReportedNEL = true;
+ if (LoadUsedNetwork() && !mReportedNEL) {
+ MaybeGenerateNELReport();
}
// make the cached response be the current response
@@ -7113,13 +7181,8 @@ nsresult nsHttpChannel::CancelInternal(nsresult status) {
return NS_OK;
}
- if (StaticPrefs::network_http_network_error_logging_enabled() &&
- LoadUsedNetwork() && !mReportedNEL) {
- if (nsCOMPtr<nsINetworkErrorLogging> nel =
- components::NetworkErrorLogging::Service()) {
- nel->GenerateNELReport(this);
- }
- mReportedNEL = true;
+ if (LoadUsedNetwork() && !mReportedNEL) {
+ MaybeGenerateNELReport();
}
// We don't want the content process to see any header values
@@ -10310,13 +10373,8 @@ nsresult nsHttpChannel::ContinueOnStopRequest(nsresult aStatus, bool aIsFromNet,
mAuthRetryPending = false;
}
- if (StaticPrefs::network_http_network_error_logging_enabled() &&
- LoadUsedNetwork() && !mReportedNEL) {
- if (nsCOMPtr<nsINetworkErrorLogging> nel =
- components::NetworkErrorLogging::Service()) {
- nel->GenerateNELReport(this);
- }
- mReportedNEL = true;
+ if (LoadUsedNetwork() && !mReportedNEL) {
+ MaybeGenerateNELReport();
}
// notify "http-on-before-stop-request" observers
diff --git a/netwerk/protocol/http/nsHttpChannel.h b/netwerk/protocol/http/nsHttpChannel.h
@@ -845,6 +845,8 @@ class nsHttpChannel final : public HttpBaseChannel,
void RecordOnStartTelemetry(nsresult aStatus, bool aIsNavigation);
+ void MaybeGenerateNELReport();
+
// Timer used to delay the network request, or to trigger the network
// request if retrieving the cache entry takes too long.
nsCOMPtr<nsITimer> mNetworkTriggerTimer;
diff --git a/netwerk/protocol/http/nsINetworkErrorLogging.idl b/netwerk/protocol/http/nsINetworkErrorLogging.idl
@@ -7,6 +7,14 @@
#include "nsISupports.idl"
interface nsIHttpChannel;
+[scriptable, uuid(53b81d9c-3b62-4ecb-b8d2-5f5a17914c9e)]
+interface nsINetworkErrorReport : nsISupports
+{
+ readonly attribute AUTF8String body;
+ readonly attribute AString group;
+ readonly attribute AString url;
+};
+
[scriptable, uuid(391ba410-0a68-42f7-b3e4-3ec26db645c0)]
interface nsINetworkErrorLogging : nsISupports
{
@@ -18,5 +26,5 @@ interface nsINetworkErrorLogging : nsISupports
/**
* Maybe send error log payload if theres a policy.
*/
- void generateNELReport(in nsIHttpChannel aChannel);
+ nsINetworkErrorReport generateNELReport(in nsIHttpChannel aChannel);
};
diff --git a/testing/web-platform/meta/network-error-logging/__dir__.ini b/testing/web-platform/meta/network-error-logging/__dir__.ini
@@ -1,2 +1,6 @@
implementation-status: backlog
-prefs: [dom.reporting.enabled:true, dom.reporting.featurePolicy.enabled:true, network.http.network_error_logging.enabled: true]
+prefs: [dom.reporting.enabled:true, dom.reporting.featurePolicy.enabled:true,
+dom.reporting.header.enabled:true,
+dom.reporting.delivering.timeout:1,
+network.http.rcwn.enabled:false,
+network.http.network_error_logging.enabled: true]
diff --git a/testing/web-platform/meta/network-error-logging/no-report-on-failed-cors-preflight.https.html.ini b/testing/web-platform/meta/network-error-logging/no-report-on-failed-cors-preflight.https.html.ini
@@ -1,7 +0,0 @@
-[no-report-on-failed-cors-preflight.https.html]
- expected:
- if os == "win": [OK, TIMEOUT]
- [\n Test that NEL reports are not sent if the CORS preflight fails\n ]
- expected:
- if os == "win": [FAIL, PASS, TIMEOUT]
- FAIL
diff --git a/testing/web-platform/meta/network-error-logging/no-report-on-unexpired-cached-response.https.html.ini b/testing/web-platform/meta/network-error-logging/no-report-on-unexpired-cached-response.https.html.ini
@@ -1,3 +1,3 @@
[no-report-on-unexpired-cached-response.https.html]
[\n Test that NEL reports are not sent for cached responses that don't hit the\n network\n ]
- expected: FAIL
+ expected: [PASS, FAIL]