Skip to content

Commit 9de29ad

Browse files
fix: misc httpx instrumentation fixes (#22)
1 parent 35d04cc commit 9de29ad

4 files changed

Lines changed: 724 additions & 472 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,3 +222,6 @@ __marimo__/
222222

223223
# macOS
224224
.DS_Store
225+
226+
# Bug tracking
227+
**/BUG_TRACKING.md

drift/instrumentation/httpx/e2e-tests/src/app.py

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,265 @@ async def fetch():
361361
return jsonify({"error": str(e)}), 500
362362

363363

364+
@app.route("/test/streaming", methods=["GET"])
365+
def test_streaming():
366+
"""Test 5: Streaming response using client.stream() context manager."""
367+
try:
368+
with httpx.Client() as client:
369+
with client.stream("GET", "https://jsonplaceholder.typicode.com/posts/6") as response:
370+
# Read the streaming response
371+
content = response.read()
372+
data = response.json()
373+
return jsonify(data)
374+
except Exception as e:
375+
return jsonify({"error": str(e)}), 500
376+
377+
378+
@app.route("/test/toplevel-stream", methods=["GET"])
379+
def test_toplevel_stream():
380+
"""Test 6: Top-level httpx.stream() context manager."""
381+
try:
382+
with httpx.stream("GET", "https://jsonplaceholder.typicode.com/posts/7") as response:
383+
content = response.read()
384+
data = response.json()
385+
return jsonify(data)
386+
except Exception as e:
387+
return jsonify({"error": str(e)}), 500
388+
389+
390+
@app.route("/test/multipart-files", methods=["POST"])
391+
def test_multipart_files():
392+
"""Test 7: Multipart file upload using files= parameter."""
393+
try:
394+
# Create in-memory file-like content
395+
files = {"file": ("test.txt", b"Hello, World!", "text/plain")}
396+
with httpx.Client() as client:
397+
# Use httpbin.org which echoes back file uploads
398+
response = client.post(
399+
"https://httpbin.org/post",
400+
files=files,
401+
)
402+
result = response.json()
403+
return jsonify({"uploaded": True, "files": result.get("files", {})})
404+
except Exception as e:
405+
return jsonify({"error": str(e)}), 500
406+
407+
408+
@app.route("/test/follow-redirects", methods=["GET"])
409+
def test_follow_redirects():
410+
"""Test 12: Following HTTP redirects."""
411+
try:
412+
with httpx.Client(follow_redirects=True) as client:
413+
# httpbin.org/redirect/2 will redirect twice before returning
414+
response = client.get("https://httpbin.org/redirect/2")
415+
return jsonify(
416+
{
417+
"final_url": str(response.url),
418+
"status_code": response.status_code,
419+
"redirect_count": len(response.history),
420+
}
421+
)
422+
except Exception as e:
423+
return jsonify({"error": str(e)}), 500
424+
425+
426+
@app.route("/test/async-send", methods=["GET"])
427+
def test_async_send():
428+
"""Test 14: AsyncClient.send() method - bypasses AsyncClient.request()."""
429+
430+
async def fetch():
431+
async with httpx.AsyncClient() as client:
432+
req = client.build_request("GET", "https://jsonplaceholder.typicode.com/posts/10")
433+
response = await client.send(req)
434+
return response.json()
435+
436+
try:
437+
result = asyncio.run(fetch())
438+
return jsonify(result)
439+
except Exception as e:
440+
return jsonify({"error": str(e)}), 500
441+
442+
443+
@app.route("/test/async-stream", methods=["GET"])
444+
def test_async_stream():
445+
"""Test 15: Async streaming response using AsyncClient.stream()."""
446+
447+
async def fetch():
448+
async with httpx.AsyncClient() as client:
449+
async with client.stream("GET", "https://jsonplaceholder.typicode.com/posts/11") as response:
450+
await response.aread()
451+
return response.json()
452+
453+
try:
454+
result = asyncio.run(fetch())
455+
return jsonify(result)
456+
except Exception as e:
457+
return jsonify({"error": str(e)}), 500
458+
459+
460+
@app.route("/test/basic-auth", methods=["GET"])
461+
def test_basic_auth():
462+
try:
463+
with httpx.Client() as client:
464+
# httpbin.org/basic-auth/{user}/{passwd} returns 200 if auth succeeds
465+
response = client.get(
466+
"https://httpbin.org/basic-auth/testuser/testpass",
467+
auth=("testuser", "testpass"),
468+
)
469+
return jsonify(response.json())
470+
except Exception as e:
471+
return jsonify({"error": str(e)}), 500
472+
473+
474+
@app.route("/test/event-hooks", methods=["GET"])
475+
def test_event_hooks():
476+
try:
477+
request_headers_captured = []
478+
response_headers_captured = []
479+
480+
def log_request(request):
481+
request_headers_captured.append(dict(request.headers))
482+
request.headers["X-Hook-Added"] = "true"
483+
484+
def log_response(response):
485+
response_headers_captured.append(dict(response.headers))
486+
487+
with httpx.Client(event_hooks={"request": [log_request], "response": [log_response]}) as client:
488+
response = client.get("https://httpbin.org/headers")
489+
result = response.json()
490+
return jsonify(
491+
{
492+
"hook_header_present": "X-Hook-Added" in result.get("headers", {}),
493+
"request_captured": len(request_headers_captured) > 0,
494+
"response_captured": len(response_headers_captured) > 0,
495+
}
496+
)
497+
except Exception as e:
498+
return jsonify({"error": str(e)}), 500
499+
500+
501+
@app.route("/test/response-hook-only", methods=["GET"])
502+
def test_response_hook_only():
503+
try:
504+
response_data_captured = []
505+
506+
def capture_response(response):
507+
response_data_captured.append(
508+
{
509+
"status": response.status_code,
510+
"url": str(response.url),
511+
}
512+
)
513+
514+
with httpx.Client(event_hooks={"response": [capture_response]}) as client:
515+
response = client.get("https://httpbin.org/get")
516+
return jsonify(
517+
{
518+
"captured": len(response_data_captured) > 0,
519+
"response_status": response.status_code,
520+
}
521+
)
522+
except Exception as e:
523+
return jsonify({"error": str(e)}), 500
524+
525+
526+
@app.route("/test/request-hook-modify-url", methods=["GET"])
527+
def test_request_hook_modify_url():
528+
try:
529+
530+
def add_query_param(request):
531+
request.headers["X-Hook-Tried-Url-Modify"] = "true"
532+
return request
533+
534+
with httpx.Client(event_hooks={"request": [add_query_param]}) as client:
535+
response = client.get("https://httpbin.org/get?original=param")
536+
result = response.json()
537+
return jsonify(
538+
{
539+
"headers": result.get("headers", {}),
540+
"args": result.get("args", {}),
541+
}
542+
)
543+
except Exception as e:
544+
return jsonify({"error": str(e)}), 500
545+
546+
547+
@app.route("/test/digest-auth", methods=["GET"])
548+
def test_digest_auth():
549+
try:
550+
with httpx.Client() as client:
551+
auth = httpx.DigestAuth("digestuser", "digestpass")
552+
response = client.get(
553+
"https://httpbin.org/digest-auth/auth/digestuser/digestpass",
554+
auth=auth,
555+
)
556+
return jsonify(response.json())
557+
except Exception as e:
558+
return jsonify({"error": str(e)}), 500
559+
560+
561+
@app.route("/test/async-hooks", methods=["GET"])
562+
def test_async_hooks():
563+
"""Test: AsyncClient with async event hooks."""
564+
565+
async def fetch():
566+
request_count = [0]
567+
response_count = [0]
568+
569+
async def async_request_hook(request):
570+
request_count[0] += 1
571+
request.headers["X-Async-Hook"] = "true"
572+
573+
async def async_response_hook(response):
574+
response_count[0] += 1
575+
576+
async with httpx.AsyncClient(
577+
event_hooks={
578+
"request": [async_request_hook],
579+
"response": [async_response_hook],
580+
}
581+
) as client:
582+
response = await client.get("https://httpbin.org/headers")
583+
result = response.json()
584+
return {
585+
"request_hook_called": request_count[0] > 0,
586+
"response_hook_called": response_count[0] > 0,
587+
"async_hook_header_present": "X-Async-Hook" in result.get("headers", {}),
588+
}
589+
590+
try:
591+
result = asyncio.run(fetch())
592+
return jsonify(result)
593+
except Exception as e:
594+
return jsonify({"error": str(e)}), 500
595+
596+
597+
@app.route("/test/file-like-body", methods=["POST"])
598+
def test_file_like_body():
599+
"""Test: Request body from file-like object (BytesIO)."""
600+
try:
601+
import io
602+
603+
file_content = b'{"title": "File Body", "body": "From BytesIO", "userId": 1}'
604+
file_obj = io.BytesIO(file_content)
605+
606+
with httpx.Client() as client:
607+
response = client.post(
608+
"https://httpbin.org/post",
609+
content=file_obj,
610+
headers={"Content-Type": "application/json"},
611+
)
612+
result = response.json()
613+
return jsonify(
614+
{
615+
"posted_data": result.get("data", ""),
616+
"content_type": result.get("headers", {}).get("Content-Type", ""),
617+
}
618+
)
619+
except Exception as e:
620+
return jsonify({"error": str(e)}), 500
621+
622+
364623
if __name__ == "__main__":
365624
sdk.mark_app_as_ready()
366625
app.run(host="0.0.0.0", port=8000, debug=False)

drift/instrumentation/httpx/e2e-tests/src/test_requests.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,28 @@ def make_request(method, endpoint, **kwargs):
100100
# Async sequential chained requests
101101
make_request("GET", "/api/async/chain")
102102

103+
make_request("GET", "/test/streaming")
104+
105+
make_request("GET", "/test/toplevel-stream")
106+
107+
make_request("POST", "/test/multipart-files")
108+
109+
make_request("GET", "/test/async-send")
110+
111+
make_request("GET", "/test/async-stream")
112+
113+
make_request("GET", "/test/follow-redirects")
114+
115+
make_request("GET", "/test/basic-auth")
116+
117+
make_request("GET", "/test/event-hooks")
118+
119+
make_request("GET", "/test/request-hook-modify-url")
120+
121+
make_request("GET", "/test/digest-auth")
122+
123+
make_request("GET", "/test/async-hooks")
124+
125+
make_request("POST", "/test/file-like-body")
126+
103127
print("\nAll requests completed successfully")

0 commit comments

Comments
 (0)