huggingface/open-r1 · error · PistonError
Empty response. status={status}
Error message
Empty response. status={status} What it means
PistonClient.send_execute raises PistonError('Empty response. status=...') when the HTTP status is 200 but the JSON body parses to None. This indicates a proxy or worker that returned an empty 200 body instead of a valid Piston execution result. It is treated as a retriable failure.
Source
Thrown at src/open_r1/utils/competitive_programming/piston_client.py:162
status = None
endpoint = None
for attempt in range(max_retries + 1):
try:
endpoint = await self._wait_for_endpoint()
if attempt > 0:
await asyncio.sleep(1)
async with self.session.post(
f"{endpoint.rstrip('/')}/execute", json=data, headers={"Content-Type": "application/json"}
) as response:
status = response.status
res_json = await response.json(content_type=None)
if status != 200:
raise PistonError(f"Server error. status={status}. {res_json}")
if res_json is None:
raise PistonError(f"Empty response. status={status}")
# piston overloaded
if "run" in res_json and "Resource temporarily unavailable" in res_json["run"].get("stderr", ""):
raise PistonError(f"Piston overloaded: {res_json['run']['stderr']}")
return res_json
except (PistonError, asyncio.TimeoutError, aiohttp.ClientConnectionError, RuntimeError) as e:
# Only retry if we haven't reached max retries yet
if attempt < max_retries:
# Calculate backoff with jitter
delay = min(base_delay * (2**attempt), 10) # Exponential backoff, capped at 10 seconds
jitter = delay * 0.2 * (2 * asyncio.get_event_loop().time() % 1 - 0.5) # Add ±10% jitter
retry_delay = delay + jitter
print(f"Retrying in {retry_delay:.2f} seconds [{self.endpoint_ids[endpoint]}] {endpoint} - {e}")
# special case: worker died
if isinstance(e, aiohttp.ClientConnectionError) and "Connect call failed" in str(e):
await self._check_failed_endpoint(endpoint)
else:View on GitHub (pinned to 1416fa0cf2)
Solutions
- Retry the request; the client already retries with backoff, so a persistent message means the endpoint is broken
- curl the /execute endpoint and inspect the raw response body
- Remove or fix the misbehaving proxy in front of Piston
- Point base_endpoints at the Piston worker directly to isolate the proxy
- Drop the unhealthy endpoint from the pool via the client's health checking
Example fix
// before curl -s -X POST http://proxy.example.com/api/v2/execute -d '...' # empty body // after curl -s -X POST http://piston-worker:2000/api/v2/execute -d '...' # returns JSON result
Defensive patterns
Strategy: try-catch
Validate before calling
import aiohttp
async def returns_body(url, payload):
async with aiohttp.ClientSession() as s:
async with s.post(f"{url.rstrip('/')}/execute", json=payload) as r:
body = await r.json(content_type=None)
return r.status == 200 and body is not None Type guard
def is_valid_piston_result(res_json):
return isinstance(res_json, dict) and "run" in res_json Try / catch
try:
res = await client.send_execute(data)
except PistonError as e:
if str(e).startswith("Empty response"):
res = await client.send_execute(data) # retry, possibly on a different endpoint
else:
raise Prevention
- Test each endpoint with a smoke execute request during setup
- Avoid proxies that buffer/strip response bodies in front of Piston
- Verify Content-Type handling; rely on the client's content_type=None parsing
- Rotate out endpoints that repeatedly return empty 200s
When it happens
Trigger: response.json(content_type=None) returns None despite status==200, typically when the endpoint returns an empty body, a 204-like response, or a proxy stripping the body while keeping 200.
Common situations: Misconfigured reverse proxy / ingress that swallows response bodies, a health-check page or wrong service listening on the port, flaky network equipment truncating responses.
Related errors
- Server error. status={status}. {res_json}
- All endpoints are unhealthy. Please check your Piston worker
AI-assisted analysis of huggingface/open-r1@1416fa0cf2 (2026-08-30).
Data as JSON: /api/errors/d0073ce8557d58e7.
Report an issue: GitHub.