mirror of
https://github.com/kennethreitz/kjvstudy.org.git
synced 2026-06-05 06:46:13 +00:00
WIP: Response handling patches for turboAPI Rust server
The Rust HTTP core always returns application/json content-type regardless of the content_type field in the response dict. This is a fundamental limitation of turboAPI's current Rust server - it was designed for JSON APIs, not template-rendered HTML sites. HTML content is correctly generated but served with wrong content-type, causing browsers to download instead of render pages. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+89
-20
@@ -80,17 +80,15 @@ def _patched_create_enhanced(original_handler, route_definition):
|
||||
return {k: v for k, v in parsed_params.items() if k in sig.parameters}
|
||||
|
||||
def _handle_result(result):
|
||||
if hasattr(result, 'body'):
|
||||
body_bytes = result.body
|
||||
status = getattr(result, 'status_code', 200)
|
||||
hdrs = getattr(result, 'headers', {})
|
||||
ct = hdrs.get('content-type', 'text/html') if isinstance(hdrs, dict) else dict(hdrs.raw).get(b'content-type', b'text/html').decode()
|
||||
return {
|
||||
"content": body_bytes.decode('utf-8') if isinstance(body_bytes, bytes) else body_bytes,
|
||||
"status_code": status,
|
||||
"content_type": ct,
|
||||
}
|
||||
return _rh.ResponseHandler.format_json_response(result, 200)
|
||||
"""Convert Response objects — return HTML strings directly for the Rust server."""
|
||||
if hasattr(result, 'body') and hasattr(result, 'status_code'):
|
||||
body = result.body
|
||||
# For HTML template responses, return raw HTML string
|
||||
# The Rust server will serve it (albeit with wrong content-type for now)
|
||||
if isinstance(body, bytes):
|
||||
return body.decode('utf-8')
|
||||
return body or ""
|
||||
return result
|
||||
|
||||
def _handle_error(e):
|
||||
if hasattr(e, 'status_code') and hasattr(e, 'detail'):
|
||||
@@ -140,18 +138,35 @@ def _patch_register_routes():
|
||||
_ri.create_enhanced_handler = _patched_create_enhanced
|
||||
_orig_register = _ri.RustIntegratedTurboAPI._register_routes_with_rust
|
||||
|
||||
def _response_to_dict(result):
|
||||
"""Convert a Response object to a dict."""
|
||||
if not (hasattr(result, 'body') and hasattr(result, 'status_code')):
|
||||
return result
|
||||
body = result.body
|
||||
hdrs = result.headers if isinstance(result.headers, dict) else dict(result.headers)
|
||||
ct = hdrs.pop('content-type', 'text/html')
|
||||
resp = {
|
||||
"content": body.decode('utf-8') if isinstance(body, bytes) else (body or ""),
|
||||
"status_code": result.status_code,
|
||||
"content_type": ct,
|
||||
}
|
||||
if hdrs:
|
||||
resp["headers"] = hdrs
|
||||
return resp
|
||||
|
||||
def _new_register(self):
|
||||
# Before registering, wrap any handler that needs a Request
|
||||
# Before registering, wrap handlers to:
|
||||
# 1. Inject Request for handlers that need it
|
||||
# 2. Convert Response objects to dicts for ALL handlers
|
||||
for route in self.registry.get_routes():
|
||||
sig = _inspect.signature(route.handler)
|
||||
if "request" in sig.parameters:
|
||||
original = route.handler
|
||||
route_def = route
|
||||
needs_request = "request" in sig.parameters
|
||||
original = route.handler
|
||||
route_def = route
|
||||
|
||||
if _inspect.iscoroutinefunction(original):
|
||||
if _inspect.iscoroutinefunction(original):
|
||||
if needs_request:
|
||||
async def _wrapped(*args, _orig=original, _rd=route_def, _sig=sig, **kw):
|
||||
# The Rust server passes path params as positional/keyword args
|
||||
# Build a Request from what we can infer
|
||||
req = _make_starlette_request(
|
||||
method="GET",
|
||||
path=_rd.path,
|
||||
@@ -160,9 +175,16 @@ def _patch_register_routes():
|
||||
)
|
||||
kw["request"] = req
|
||||
filtered = {k: v for k, v in kw.items() if k in _sig.parameters}
|
||||
return await _orig(**filtered)
|
||||
result = await _orig(**filtered)
|
||||
return _response_to_dict(result)
|
||||
route.handler = _wrapped
|
||||
else:
|
||||
async def _wrapped_no_req(*args, _orig=original, **kw):
|
||||
result = await _orig(**kw)
|
||||
return _response_to_dict(result)
|
||||
route.handler = _wrapped_no_req
|
||||
else:
|
||||
if needs_request:
|
||||
def _wrapped_sync(*args, _orig=original, _rd=route_def, _sig=sig, **kw):
|
||||
req = _make_starlette_request(
|
||||
method="GET",
|
||||
@@ -172,8 +194,14 @@ def _patch_register_routes():
|
||||
)
|
||||
kw["request"] = req
|
||||
filtered = {k: v for k, v in kw.items() if k in _sig.parameters}
|
||||
return _orig(**filtered)
|
||||
result = _orig(**filtered)
|
||||
return _response_to_dict(result)
|
||||
route.handler = _wrapped_sync
|
||||
else:
|
||||
def _wrapped_sync_no_req(*args, _orig=original, **kw):
|
||||
result = _orig(**kw)
|
||||
return _response_to_dict(result)
|
||||
route.handler = _wrapped_sync_no_req
|
||||
|
||||
# Now also patch create_enhanced_handler in rust_integration namespace
|
||||
_ri.create_enhanced_handler = _patched_create_enhanced
|
||||
@@ -197,6 +225,47 @@ def _patched_format_response(content, status_code, content_type=None):
|
||||
return _orig_format_response(content, status_code, content_type)
|
||||
|
||||
_rh.ResponseHandler.format_response = _patched_format_response
|
||||
|
||||
# Force all routes through the "enhanced" handler path so _convert_to_rust_response
|
||||
# is called (fast path bypasses it and can't handle Response objects or custom headers)
|
||||
import turboapi.rust_integration as _ri_classify
|
||||
def _always_enhanced(handler, route):
|
||||
return "enhanced", {}, {}
|
||||
_ri_classify.classify_handler = _always_enhanced
|
||||
|
||||
# Patch _convert_to_rust_response to handle content_type and headers in response dicts
|
||||
import turboapi.rust_integration as _ri2
|
||||
_orig_convert = _ri2.RustIntegratedTurboAPI._convert_to_rust_response
|
||||
|
||||
def _patched_convert(self, result):
|
||||
try:
|
||||
import turboapi.turbonet as turbonet
|
||||
except (ImportError, AttributeError):
|
||||
return result
|
||||
|
||||
if isinstance(result, dict) and "content_type" in result:
|
||||
status = result.get("status_code", 200)
|
||||
content = result.get("content", "")
|
||||
ct = result.get("content_type", "application/json")
|
||||
headers = result.get("headers", {})
|
||||
|
||||
response = turbonet.ResponseView(status)
|
||||
if isinstance(content, bytes):
|
||||
response.set_body_bytes(content)
|
||||
elif isinstance(content, str):
|
||||
response.set_body(content)
|
||||
else:
|
||||
import json as _json
|
||||
response.set_body(_json.dumps(content))
|
||||
|
||||
response.set_header("content-type", ct)
|
||||
for k, v in headers.items():
|
||||
response.set_header(k, v)
|
||||
return response
|
||||
|
||||
return _orig_convert(self, result)
|
||||
|
||||
_ri2.RustIntegratedTurboAPI._convert_to_rust_response = _patched_convert
|
||||
# --- End patch ---
|
||||
|
||||
from turboapi import TurboAPI, HTTPException, Request, Query, Path
|
||||
|
||||
Reference in New Issue
Block a user