This is an automated email from the ASF dual-hosted git repository.
bneradt pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
The following commit(s) were added to refs/heads/master by this push:
new 9f9752d47c proxy.process.http.000_responses metric (#12861)
9f9752d47c is described below
commit 9f9752d47c5c7443cf84b8419002e0dbf4ba36ab
Author: Brian Neradt <[email protected]>
AuthorDate: Thu Feb 19 12:31:26 2026 -0600
proxy.process.http.000_responses metric (#12861)
Add a new HTTP stat to track responses where no valid HTTP status code
was sent to the client. This typically occurs when the client aborts
the connection before a response is sent (ERR_CLIENT_ABORT).
---
.../statistics/core/http-response-code.en.rst | 7 ++
include/proxy/http/HttpConfig.h | 1 +
src/proxy/http/HttpConfig.cc | 1 +
src/proxy/http/HttpTransact.cc | 4 +
tests/gold_tests/statistics/abort_client.py | 52 ++++++++++
.../statistics/metric_response_000.test.py | 107 +++++++++++++++++++++
6 files changed, 172 insertions(+)
diff --git
a/doc/admin-guide/monitoring/statistics/core/http-response-code.en.rst
b/doc/admin-guide/monitoring/statistics/core/http-response-code.en.rst
index 56904f284c..f5425c1ee1 100644
--- a/doc/admin-guide/monitoring/statistics/core/http-response-code.en.rst
+++ b/doc/admin-guide/monitoring/statistics/core/http-response-code.en.rst
@@ -28,6 +28,13 @@ HTTP status code of the response. Please refer to the
:ref:`appendix-http-status-codes` appendix for more details on what each status
code means.
+.. ts:stat:: global proxy.process.http.000_responses integer
+ :type: counter
+
+ The number of HTTP transactions where no valid HTTP response status code was
+ sent to the client. This typically occurs when the client aborts the
+ connection before a response is sent (ERR_CLIENT_ABORT).
+
.. ts:stat:: global proxy.process.http.100_responses integer
:type: counter
diff --git a/include/proxy/http/HttpConfig.h b/include/proxy/http/HttpConfig.h
index 4ac5e07183..4df4958b8a 100644
--- a/include/proxy/http/HttpConfig.h
+++ b/include/proxy/http/HttpConfig.h
@@ -208,6 +208,7 @@ struct HttpStatsBlock {
Metrics::Counter::AtomicType *pushed_document_total_size;
Metrics::Counter::AtomicType *pushed_response_header_total_size;
Metrics::Counter::AtomicType *put_requests;
+ Metrics::Counter::AtomicType *response_status_000_count;
Metrics::Counter::AtomicType *response_status_100_count;
Metrics::Counter::AtomicType *response_status_101_count;
Metrics::Counter::AtomicType *response_status_1xx_count;
diff --git a/src/proxy/http/HttpConfig.cc b/src/proxy/http/HttpConfig.cc
index a34f77d21e..957cb0da7f 100644
--- a/src/proxy/http/HttpConfig.cc
+++ b/src/proxy/http/HttpConfig.cc
@@ -399,6 +399,7 @@ register_stat_callbacks()
http_rsb.pushed_document_total_size =
Metrics::Counter::createPtr("proxy.process.http.pushed_document_total_size");
http_rsb.pushed_response_header_total_size =
Metrics::Counter::createPtr("proxy.process.http.pushed_response_header_total_size");
http_rsb.put_requests =
Metrics::Counter::createPtr("proxy.process.http.put_requests");
+ http_rsb.response_status_000_count =
Metrics::Counter::createPtr("proxy.process.http.000_responses");
http_rsb.response_status_100_count =
Metrics::Counter::createPtr("proxy.process.http.100_responses");
http_rsb.response_status_101_count =
Metrics::Counter::createPtr("proxy.process.http.101_responses");
http_rsb.response_status_1xx_count =
Metrics::Counter::createPtr("proxy.process.http.1xx_responses");
diff --git a/src/proxy/http/HttpTransact.cc b/src/proxy/http/HttpTransact.cc
index 08cccee0ef..5296532f27 100644
--- a/src/proxy/http/HttpTransact.cc
+++ b/src/proxy/http/HttpTransact.cc
@@ -8485,6 +8485,10 @@ HttpTransact::client_result_stat(State *s, ink_hrtime
total_time, ink_hrtime req
if (s->client_info.abort == ABORTED) {
client_transaction_result = ClientTransactionResult_t::ERROR_ABORT;
}
+ // Count 000 responses separately since they include aborts (the main source
of 000).
+ if (static_cast<int>(client_response_status) == 0) {
+ Metrics::Counter::increment(http_rsb.response_status_000_count);
+ }
// Count the status codes, assuming the client didn't abort (i.e. there is
an m_http)
if ((s->source != Source_t::NONE) && (s->client_info.abort == DIDNOT_ABORT))
{
switch (static_cast<int>(client_response_status)) {
diff --git a/tests/gold_tests/statistics/abort_client.py
b/tests/gold_tests/statistics/abort_client.py
new file mode 100644
index 0000000000..8fb322e659
--- /dev/null
+++ b/tests/gold_tests/statistics/abort_client.py
@@ -0,0 +1,52 @@
+#!/usr/bin/env python3
+"""A client that sends an HTTP request and immediately aborts."""
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import socket
+import sys
+
+
+def main() -> int:
+ """Connect, send a partial request, and abort."""
+ parser = argparse.ArgumentParser(description='Send a partial request and
abort.')
+ parser.add_argument('host', help='The host to connect to.')
+ parser.add_argument('port', type=int, help='The port to connect to.')
+ args = parser.parse_args()
+
+ # Connect to the server.
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.connect((args.host, args.port))
+ print(f'Connected to {args.host}:{args.port}')
+
+ # Send ONLY partial request headers (no terminating \r\n\r\n).
+ # This means ATS will wait for more data and never construct a response.
+ partial_request = b"GET / HTTP/1.1\r\nHost: www.example.com\r\n"
+ sock.sendall(partial_request)
+ print('Sent partial request (missing final CRLF), aborting...')
+
+ # Immediately close the socket.
+ # This triggers an ERR_CLIENT_ABORT before any response is constructed.
+ sock.close()
+ print('Connection closed.')
+
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/tests/gold_tests/statistics/metric_response_000.test.py
b/tests/gold_tests/statistics/metric_response_000.test.py
new file mode 100644
index 0000000000..30af20a185
--- /dev/null
+++ b/tests/gold_tests/statistics/metric_response_000.test.py
@@ -0,0 +1,107 @@
+"""Verify the proxy.process.http.000_responses stat is incremented for client
aborts."""
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import sys
+
+Test.Summary = __doc__
+
+
+class MetricResponse000Test:
+ """Verify that the 000_responses stat is incremented when a client
aborts."""
+
+ _abort_client = 'abort_client.py'
+ _server_counter = 0
+ _ts_counter = 0
+
+ def __init__(self):
+ """Configure and run the test."""
+ self._configure_server()
+ self._configure_traffic_server()
+ self._configure_abort_client()
+ self._configure_successful_request()
+ self._verify_000_metric()
+
+ def _configure_server(self) -> None:
+ """Configure the origin server."""
+ self._server =
Test.MakeOriginServer(f'server-{MetricResponse000Test._server_counter}')
+ MetricResponse000Test._server_counter += 1
+
+ request_header = {"headers": "GET / HTTP/1.1\r\nHost:
www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+ response_header = {
+ "headers": "HTTP/1.1 200 OK\r\nConnection:
close\r\nContent-Length: 0\r\n\r\n",
+ "timestamp": "1469733493.993",
+ "body": ""
+ }
+ self._server.addResponse("sessionlog.json", request_header,
response_header)
+
+ def _configure_traffic_server(self) -> None:
+ """Configure ATS."""
+ self._ts =
Test.MakeATSProcess(f'ts-{MetricResponse000Test._ts_counter}',
enable_cache=False)
+ MetricResponse000Test._ts_counter += 1
+
+ self._ts.Disk.remap_config.AddLine(f'map /
http://127.0.0.1:{self._server.Variables.Port}/')
+ self._ts.Disk.records_config.update({
+ 'proxy.config.diags.debug.enabled': 0,
+ 'proxy.config.diags.debug.tags': 'http',
+ })
+
+ def _configure_abort_client(self) -> None:
+ """Configure a client to send a partial request and abort."""
+ tr = Test.AddTestRun('Trigger a client abort with partial request')
+
+ tr.Setup.CopyAs(os.path.join(Test.TestDirectory, self._abort_client),
Test.RunDirectory)
+
+ p = tr.Processes.Default
+ p.Command = f'{sys.executable} {self._abort_client} 127.0.0.1
{self._ts.Variables.port}'
+ p.ReturnCode = 0
+
+ self._ts.StartBefore(self._server)
+ p.StartBefore(self._ts)
+
+ tr.StillRunningAfter = self._ts
+ tr.StillRunningAfter = self._server
+
+ def _configure_successful_request(self) -> None:
+ """Send a successful request to verify it doesn't increment 000
stat."""
+ tr = Test.AddTestRun('Send a successful request')
+ tr.Processes.Default.Command = f'curl -s -o /dev/null -w
"%{{http_code}}" http://127.0.0.1:{self._ts.Variables.port}/'
+ tr.Processes.Default.ReturnCode = 0
+ tr.Processes.Default.Streams.All = Testers.ContainsExpression('200',
'Expected 200 response')
+ tr.StillRunningAfter = self._ts
+ tr.StillRunningAfter = self._server
+
+ def _verify_000_metric(self) -> None:
+ """Verify the 000_responses stat is incremented."""
+ # Wait for stats to propagate.
+ tr = Test.AddTestRun('Wait for stats')
+ tr.Processes.Default.Command = 'sleep 2'
+ tr.Processes.Default.ReturnCode = 0
+ tr.StillRunningAfter = self._ts
+
+ # Verify the 000_responses stat is non-zero.
+ tr = Test.AddTestRun('Check 000_responses stat')
+ tr.Processes.Default.Command = 'traffic_ctl metric get
proxy.process.http.000_responses'
+ tr.Processes.Default.Env = self._ts.Env
+ tr.Processes.Default.ReturnCode = 0
+ tr.Processes.Default.Streams.All = Testers.ContainsExpression(
+ 'proxy.process.http.000_responses 1', 'The 000_responses stat
should be 1')
+ tr.StillRunningAfter = self._ts
+
+
+MetricResponse000Test()