Skip to content

Commit 0e0ddb8

Browse files
Hypercorn HTTP/2 Testing (#1248)
* Move testing cert to testing_support directory * Add dependencies for hypercorn http2/3 testing * Add HTTP/2 tests for Hypercorn * Add generic HTTP/2 and 3 request function based on niquests * Fix missing fixtures in port fixture * Clean up testing file * Linting * Change HTTP/3 requests to use preset quic_cache --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent 14a219f commit 0e0ddb8

File tree

6 files changed

+154
-27
lines changed

6 files changed

+154
-27
lines changed

tests/adapter_hypercorn/test_hypercorn.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,16 @@
1515
import asyncio
1616
import threading
1717
import time
18-
from urllib.request import HTTPError, urlopen
1918

19+
import niquests
2020
import pytest
21+
from testing_support.certs import CERT_PATH
2122
from testing_support.fixtures import (
2223
override_application_settings,
2324
raise_background_exceptions,
2425
wait_for_background_threads,
2526
)
27+
from testing_support.http_23_testing import make_request
2628
from testing_support.sample_asgi_applications import (
2729
AppWithCall,
2830
AppWithCallRaw,
@@ -97,7 +99,9 @@ async def shutdown_trigger():
9799

98100
config = hypercorn.config.Config.from_mapping(
99101
{
100-
"bind": [f"127.0.0.1:{port}"],
102+
"bind": [f"localhost:{port}"],
103+
"certfile": CERT_PATH,
104+
"keyfile": CERT_PATH,
101105
}
102106
)
103107

@@ -123,7 +127,7 @@ def wait_for_port(port, retries=10):
123127
status = None
124128
for _ in range(retries):
125129
try:
126-
status = urlopen(f"http://localhost:{port}/ignored", timeout=1).status # nosec
130+
status = make_request(host="localhost", port=port, path="/ignored", timeout=1).status_code
127131
assert status == 200
128132
return
129133
except Exception as e:
@@ -134,8 +138,9 @@ def wait_for_port(port, retries=10):
134138
raise RuntimeError(f"Failed to wait for port {port}. Got status {status}")
135139

136140

141+
@pytest.mark.parametrize("http_version", [1, 2], ids=["HTTP/1", "HTTP/2"])
137142
@override_application_settings({"transaction_name.naming_scheme": "framework"})
138-
def test_hypercorn_200(port, app):
143+
def test_hypercorn_200(port, app, http_version):
139144
hypercorn_version = get_package_version("hypercorn")
140145

141146
@validate_transaction_metrics(
@@ -147,19 +152,20 @@ def test_hypercorn_200(port, app):
147152
@raise_background_exceptions()
148153
@wait_for_background_threads()
149154
def response():
150-
return urlopen(f"http://localhost:{port}", timeout=10) # nosec
155+
return make_request(host="localhost", port=port, path="/", http_version=http_version, timeout=10)
151156

152-
assert response().status == 200
157+
response().raise_for_status()
153158

154159

160+
@pytest.mark.parametrize("http_version", [1, 2], ids=["HTTP/1", "HTTP/2"])
155161
@override_application_settings({"transaction_name.naming_scheme": "framework"})
156-
def test_hypercorn_500(port, app):
162+
def test_hypercorn_500(port, app, http_version):
157163
@validate_transaction_errors(["builtins:ValueError"])
158164
@validate_transaction_metrics(callable_name(app))
159165
@raise_background_exceptions()
160166
@wait_for_background_threads()
161167
def _test():
162-
with pytest.raises(HTTPError):
163-
urlopen(f"http://localhost:{port}/exc") # nosec
168+
with pytest.raises(niquests.exceptions.HTTPError):
169+
make_request(host="localhost", port=port, path="/exc", http_version=http_version, timeout=10)
164170

165171
_test()

tests/agent_unittests/test_http_client.py

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,11 @@
1717
import os.path
1818
import ssl
1919
import zlib
20-
20+
from http.server import BaseHTTPRequestHandler, HTTPServer
2121
from io import StringIO
2222

2323
import pytest
24-
25-
from http.server import BaseHTTPRequestHandler, HTTPServer
24+
from testing_support.certs import CERT_PATH
2625
from testing_support.mock_external_http_server import MockExternalHTTPServer
2726

2827
from newrelic.common import certs
@@ -41,9 +40,6 @@
4140
from newrelic.packages.urllib3.util import Url
4241

4342

44-
SERVER_CERT = os.path.join(os.path.dirname(__file__), "cert.pem")
45-
46-
4743
def echo_full_request(self):
4844
self.server.connections.append(self.connection)
4945
request_line = str(self.requestline).encode("utf-8")
@@ -100,7 +96,7 @@ def __init__(self, *args, **kwargs):
10096
super(SecureServer, self).__init__(*args, **kwargs)
10197
try:
10298
self.context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
103-
self.context.load_cert_chain(certfile=SERVER_CERT, keyfile=SERVER_CERT)
99+
self.context.load_cert_chain(certfile=CERT_PATH, keyfile=CERT_PATH)
104100
self.httpd.socket = self.context.wrap_socket(
105101
sock=self.httpd.socket,
106102
server_side=True,
@@ -110,8 +106,8 @@ def __init__(self, *args, **kwargs):
110106
self.httpd.socket = ssl.wrap_socket(
111107
self.httpd.socket,
112108
server_side=True,
113-
keyfile=SERVER_CERT,
114-
certfile=SERVER_CERT,
109+
keyfile=CERT_PATH,
110+
certfile=CERT_PATH,
115111
do_handshake_on_connect=False,
116112
)
117113

@@ -310,7 +306,7 @@ def test_http_payload_compression(server, client_cls, method, threshold):
310306
]
311307
assert internal_metrics["Supportability/Python/Collector/Output/Bytes"][:2] == [
312308
2,
313-
len(payload)*2,
309+
len(payload) * 2,
314310
]
315311

316312
if threshold < 20:
@@ -320,7 +316,7 @@ def test_http_payload_compression(server, client_cls, method, threshold):
320316

321317
# Verify the compressed payload length is recorded
322318
assert internal_metrics["Supportability/Python/Collector/method1/ZLIB/Bytes"][:2] == [1, payload_byte_len]
323-
assert internal_metrics["Supportability/Python/Collector/ZLIB/Bytes"][:2] == [2, payload_byte_len*2]
319+
assert internal_metrics["Supportability/Python/Collector/ZLIB/Bytes"][:2] == [2, payload_byte_len * 2]
324320

325321
assert len(internal_metrics) == 8
326322
else:
@@ -354,7 +350,7 @@ def test_http_payload_compression(server, client_cls, method, threshold):
354350

355351

356352
def test_cert_path(server):
357-
with HttpClient("localhost", server.port, ca_bundle_path=SERVER_CERT) as client:
353+
with HttpClient("localhost", server.port, ca_bundle_path=CERT_PATH) as client:
358354
status, data = client.send_request()
359355

360356

@@ -367,7 +363,7 @@ def test_default_cert_path(monkeypatch, system_certs_available):
367363
cert_file = None
368364
ca_path = None
369365

370-
class DefaultVerifyPaths():
366+
class DefaultVerifyPaths:
371367
cafile = cert_file
372368
capath = ca_path
373369

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Copyright 2010 New Relic, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import os
16+
17+
CERT_PATH = os.path.join(os.path.dirname(__file__), "cert.pem")
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
This is not a secret key!
2+
This private key is used only for testing and is not functionally used in the agent.
3+
To generate a new key and certificate, use the following.
4+
openssl req -nodes -newkey rsa:2048 -x509 -keyout key.pem -out cert.pem -subj '/CN=localhost' -days 3650
5+
-----BEGIN PRIVATE KEY-----
6+
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDYF0U/0zXikonW
7+
Ez532avkDL1QbQ8Yz5ULMwZz8j+cLEhdw/4pQJ7Dox6KEZbsan1nZZqpcZWT0d39
8+
aY/AQ9udwZT+G4biRCQQHd3IaYdveDuq/MQXEiDmcs1CpCwyWjmfRnjCm2/c8wdW
9+
JCBaRq1hG/hsZ5UKhJxIA9BUl3qPizr7qU/VwNK8+8QQ28EsnUrkLDPL2x1fkIzy
10+
XR89d/NO97a02YV7WwEf8GH9gB0KLhIZzDDh/BM3olHc1BRlaqkATcuxPbWqKH53
11+
HqL5Wi9O8Pxe9OSBXbAOSlBhmRRUVFx4siRNV+Bkv3VOBCUtk2/5SQwJdxfkICbP
12+
0YlrBPqFAgMBAAECggEBAKzbiJiy0xMIl9w4jqr+4+LMUhBo/T+iph5MVeggK8Q5
13+
JDZllwXW3GmxLbfStEEwOlqgy2SqKLYTlpmlfMmXPrHmbdILoQ2U5qhBy+0Khb2k
14+
l06DXjT6Wnkd8pZRj81DoX6IuAcsogJEImVFBuBQU1cwMbw969p7FC0DZ/6TIgZ6
15+
KHvm5Z43uy/wcbHFa2PoMaVvyutKun1po3NG90FlVMJmQYiph2V4/kxcZo8wWXU2
16+
hE8cbL2g1pv60ZF3wZTWWhnSZTRB2uesW0opNmpwcqqQjQOczJ91y8B4BIjy1QTD
17+
ICxUO9pEtOexNi3/JnzreByHxQt5g7wINbYxFk8MR40CgYEA751xj/B6jut3NMmr
18+
bTLngjE1IAjv/8xEXKhfDAp6fgyU+ATbD8ysIllkch/k2Q0boiIB+XoQ/+KqB/pC
19+
iGw6cGtY3ZF75u/NokHMhQVtHIu3CDbNYSCCtLekG3osXfZaaD4QJVHSrgBYkmez
20+
Ty7my+Sub+uqizz4fK2DUmBpDyMCgYEA5t4HJRgCRQzz340ou+H63QoN5P1x5FlP
21+
M8QpklclpU+dOJsbvmcHzbZJgvuZb6Vt18LuUYLWGeVRrpvUVCapJecklbnNF3wL
22+
YIehBDIiJd2Qq8rbg+yKjNTElbaDWJP+RqPX8IGfvGr23Lsw34vDj/d7N9Ch6xm5
23+
XChbVCi6/jcCgYB0RhhnWrB+TfDIotwW307MNIitBOlBXaQGuoV02FjcdcqMF/8d
24+
SZp2CJ7fam6ojN3N7Wa74uoA4cLUoDJM9QfeqZiz2/cd91v30qomGp357iphSAad
25+
jSMgAsUVuFFzPypbz1ISagQr/2r7kGrIj9/bLRsgoGFfs7R4+9Hv1WzltQKBgEof
26+
BKo7IBdtRisC1g4kSneHD9jyKgvHRK95DmPGiPafLfoLiofB6nZ4TPe5sZRvx2lb
27+
U0pmODkOMABgVXZDB1F8+Xj8s0UT9U8jnGWNdvszPIx7T6j2W7FFamwqsdbRhPTH
28+
C8BSzacfrGxHyTQsWjgxm6Ta3fFuS92zs0a84PRXAoGANEm47fczWIdWR0SmuEoL
29+
gIGLBi8b2nKZWIeATNwTyWWvD/jEJJXIdjXYxYMK3iG0CCXIColvfoqzEKfNCkz4
30+
p5wl5yH6EOI+QVISNq7ovrtgLXpUUPPXA/FjYk74e5ITAd5Ute3nB32bX0jPQCGN
31+
cXIVO2dC+mN517lRVQyF/GQ=
32+
-----END PRIVATE KEY-----
33+
-----BEGIN CERTIFICATE-----
34+
MIIDCTCCAfGgAwIBAgIUbytSUISOZeaoqVCzd/zLDhQgZ1owDQYJKoZIhvcNAQEL
35+
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIxMDMwMzAxMzY1MloXDTMxMDMw
36+
MTAxMzY1MlowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
37+
AAOCAQ8AMIIBCgKCAQEA2BdFP9M14pKJ1hM+d9mr5Ay9UG0PGM+VCzMGc/I/nCxI
38+
XcP+KUCew6MeihGW7Gp9Z2WaqXGVk9Hd/WmPwEPbncGU/huG4kQkEB3dyGmHb3g7
39+
qvzEFxIg5nLNQqQsMlo5n0Z4wptv3PMHViQgWkatYRv4bGeVCoScSAPQVJd6j4s6
40+
+6lP1cDSvPvEENvBLJ1K5Cwzy9sdX5CM8l0fPXfzTve2tNmFe1sBH/Bh/YAdCi4S
41+
Gcww4fwTN6JR3NQUZWqpAE3LsT21qih+dx6i+VovTvD8XvTkgV2wDkpQYZkUVFRc
42+
eLIkTVfgZL91TgQlLZNv+UkMCXcX5CAmz9GJawT6hQIDAQABo1MwUTAdBgNVHQ4E
43+
FgQUiC/0q2fQCAYC01Opw5iDBfhLNPQwHwYDVR0jBBgwFoAUiC/0q2fQCAYC01Op
44+
w5iDBfhLNPQwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAaxSo
45+
gJh6X/ywmT3BXcS15MlAXufMm3uybVbMZjPszZ+vIPF65oelAbSnw/+JHp77fF7F
46+
Erv19MGY8IlMEeUf9agXRF6JNVJD7N3i3zE/2GXoer9UOHQqz5/WWs4F17FAmZW8
47+
YkzMA70GVa20RedIMreEUxxIyN2eUL8xLfs3E9DEYovOldKfC0Ie1BHFMBhp1tja
48+
6Ag91xyPqP9Pw9ofgS0DoYq6m2ltDNXLoWep1yi1OTwiTvI+GD6JJhmWbCjK0ofA
49+
IkJENYq5tKA6yvQ2Roi9o6oixDJP/SGQtUKPGGRoFcN9gqn+IVC2XmvxHzTOxUWr
50+
/FMyhRqe1k81s3T2hg==
51+
-----END CERTIFICATE-----
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Copyright 2010 New Relic, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import niquests
16+
17+
VALID_HTTP_VERSIONS = frozenset((None, 1, 2, 3))
18+
INVALID_HTTP_INPUT_WARNING = "Invalid HTTP version. Expected an integer (1, 2, or 3) or None for no specific version."
19+
INVALID_HTTP_VERSION_USED_WARNING = "Incorrect HTTP version used: {}"
20+
21+
def make_request(host, port, path="", method="GET", body=None, http_version=None, timeout=10):
22+
assert http_version in VALID_HTTP_VERSIONS, INVALID_HTTP_INPUT_WARNING
23+
24+
# Disable other HTTP connection types
25+
session_kwargs = {}
26+
if http_version == 1:
27+
session_kwargs["disable_http2"] = True
28+
session_kwargs["disable_http3"] = True
29+
elif http_version == 2:
30+
session_kwargs["disable_http1"] = True
31+
session_kwargs["disable_http3"] = True
32+
elif http_version == 3:
33+
# HTTP/1.1 must remain enabled to allow the session to open
34+
session_kwargs["disable_http2"] = True
35+
36+
# Create session
37+
with niquests.Session(**session_kwargs) as session:
38+
session.verify = False # Disable SSL verification
39+
if http_version == 3:
40+
# Preset quic cache to enable HTTP/3 connections
41+
session.quic_cache_layer[(host, port)] = ("", port)
42+
43+
# Send Request
44+
response = session.request(method.upper(), f"https://{host}:{port}{path}", data=body, timeout=timeout)
45+
response.ok # Ensure response is completed
46+
response.raise_for_status() # Check response status code
47+
48+
# Check HTTP version used was correct
49+
if http_version == 1:
50+
assert response.http_version in {10, 11}, INVALID_HTTP_VERSION_USED_WARNING.format(response.http_version)
51+
elif http_version == 2:
52+
assert response.http_version == 20, INVALID_HTTP_VERSION_USED_WARNING.format(response.http_version)
53+
elif http_version == 3:
54+
assert response.http_version == 30, INVALID_HTTP_VERSION_USED_WARNING.format(response.http_version)
55+
56+
return response

tox.ini

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -206,11 +206,12 @@ deps =
206206
adapter_gunicorn-aiohttp03-py312: aiohttp==3.9.0rc0
207207
adapter_gunicorn-gunicorn19: gunicorn<20
208208
adapter_gunicorn-gunicornlatest: gunicorn
209-
adapter_hypercorn-hypercornlatest: hypercorn
210-
adapter_hypercorn-hypercorn0013: hypercorn<0.14
211-
adapter_hypercorn-hypercorn0012: hypercorn<0.13
212-
adapter_hypercorn-hypercorn0011: hypercorn<0.12
213-
adapter_hypercorn-hypercorn0010: hypercorn<0.11
209+
adapter_hypercorn-hypercornlatest: hypercorn[h3]
210+
adapter_hypercorn-hypercorn0013: hypercorn[h3]<0.14
211+
adapter_hypercorn-hypercorn0012: hypercorn[h3]<0.13
212+
adapter_hypercorn-hypercorn0011: hypercorn[h3]<0.12
213+
adapter_hypercorn-hypercorn0010: hypercorn[h3]<0.11
214+
adapter_hypercorn: niquests
214215
adapter_uvicorn-uvicorn014: uvicorn<0.15
215216
adapter_uvicorn-uvicornlatest: uvicorn
216217
adapter_uvicorn: typing-extensions

0 commit comments

Comments
 (0)