This is an automated email from the ASF dual-hosted git repository. cmcfarlen pushed a commit to branch 10.2.x in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit 1b26c56fd3cfaf31327c88d16612365b182827f2 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). (cherry picked from commit 9f9752d47c5c7443cf84b8419002e0dbf4ba36ab) --- .../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 bf797f7a9c..86d3411512 100644 --- a/include/proxy/http/HttpConfig.h +++ b/include/proxy/http/HttpConfig.h @@ -248,6 +248,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 7a06f4c972..20f1fee0db 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 27c090d616..8137257f26 100644 --- a/src/proxy/http/HttpTransact.cc +++ b/src/proxy/http/HttpTransact.cc @@ -8651,6 +8651,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()
