Handle stringified Gradio JSON bodies

#39
Files changed (1) hide show
  1. app.py +67 -1
app.py CHANGED
@@ -2,6 +2,7 @@
2
  import logging
3
  import sys
4
  import os
 
5
 
6
  from constants import FONT_FAMILY_SHORT
7
 
@@ -404,6 +405,71 @@ class RootRedirectMiddleware(BaseHTTPMiddleware):
404
  return await call_next(request)
405
 
406
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
  # Create a parent FastAPI app with redirect_slashes=False to prevent
408
  # automatic trailing slash redirects that cause issues with Gradio
409
  root_app = FastAPI(redirect_slashes=False)
@@ -415,6 +481,7 @@ root_app.mount("/api", api_app)
415
 
416
  # Mount Gradio app at root path
417
  app = gr.mount_gradio_app(root_app, demo, path="/")
 
418
  logger.info("REST API mounted at /api, Gradio app mounted at /")
419
 
420
 
@@ -427,4 +494,3 @@ if __name__ == "__main__":
427
  logger.info(f"Launching app on {host}:{port}")
428
  uvicorn.run(app, host=host, port=port)
429
  logger.info("App launched successfully")
430
-
 
2
  import logging
3
  import sys
4
  import os
5
+ import json
6
 
7
  from constants import FONT_FAMILY_SHORT
8
 
 
405
  return await call_next(request)
406
 
407
 
408
+ class StringifiedGradioJSONMiddleware:
409
+ """Normalize JSON bodies double-encoded by the custom-domain proxy.
410
+
411
+ Requests sent through index.openhands.dev can arrive at Gradio as a JSON
412
+ string containing the real request object, which makes FastAPI validation
413
+ reject interactive callbacks with 422. Direct HF Space traffic already sends
414
+ proper JSON objects, so this only rewrites bodies that decode to strings.
415
+ """
416
+
417
+ def __init__(self, app):
418
+ self.app = app
419
+
420
+ async def __call__(self, scope, receive, send):
421
+ if (
422
+ scope["type"] == "http"
423
+ and scope.get("method") == "POST"
424
+ and scope.get("path", "").startswith("/gradio_api/")
425
+ ):
426
+ headers = {
427
+ key.decode("latin-1").lower(): value.decode("latin-1")
428
+ for key, value in scope.get("headers", [])
429
+ }
430
+ content_type = headers.get("content-type", "")
431
+ if "application/json" not in content_type:
432
+ return await self.app(scope, receive, send)
433
+
434
+ body_parts = []
435
+ while True:
436
+ message = await receive()
437
+ if message["type"] != "http.request":
438
+ break
439
+ body_parts.append(message.get("body", b""))
440
+ if not message.get("more_body", False):
441
+ break
442
+
443
+ body = b"".join(body_parts)
444
+ replacement_body = body
445
+ try:
446
+ decoded = json.loads(body)
447
+ except json.JSONDecodeError:
448
+ decoded = None
449
+
450
+ if isinstance(decoded, str):
451
+ stripped = decoded.strip()
452
+ if stripped.startswith(("{", "[")):
453
+ replacement_body = stripped.encode("utf-8")
454
+
455
+ sent = False
456
+
457
+ async def replay_receive():
458
+ nonlocal sent
459
+ if sent:
460
+ return {"type": "http.request", "body": b"", "more_body": False}
461
+ sent = True
462
+ return {
463
+ "type": "http.request",
464
+ "body": replacement_body,
465
+ "more_body": False,
466
+ }
467
+
468
+ return await self.app(scope, replay_receive, send)
469
+
470
+ return await self.app(scope, receive, send)
471
+
472
+
473
  # Create a parent FastAPI app with redirect_slashes=False to prevent
474
  # automatic trailing slash redirects that cause issues with Gradio
475
  root_app = FastAPI(redirect_slashes=False)
 
481
 
482
  # Mount Gradio app at root path
483
  app = gr.mount_gradio_app(root_app, demo, path="/")
484
+ app = StringifiedGradioJSONMiddleware(app)
485
  logger.info("REST API mounted at /api, Gradio app mounted at /")
486
 
487
 
 
494
  logger.info(f"Launching app on {host}:{port}")
495
  uvicorn.run(app, host=host, port=port)
496
  logger.info("App launched successfully")