feat: Enhance error handling for exit node issues and improve 502 error response with troubleshooting guide

This commit is contained in:
Abolfazl
2026-05-13 00:57:08 +03:30
parent 1f93575cf6
commit ff3195ee33
+389 -6
View File
@@ -22,6 +22,7 @@ classify_relay_error(raw) -> str
import base64
import gzip
import html
import json
import logging
import re
@@ -103,6 +104,17 @@ _ADMIN_PATTERNS = (
"administrator to enable",
)
# Exit node / network errors
_EXIT_NODE_PATTERNS = (
"dns",
"connection refused",
"connection reset",
"unable to connect",
"timeout",
"exit node",
"invalid url",
"url not valid",
)
# ── Error classifier ──────────────────────────────────────────────────────────
@@ -168,21 +180,335 @@ def classify_relay_error(raw: str) -> str:
f"(raw: {raw})"
)
if any(p in lower for p in _EXIT_NODE_PATTERNS):
# Exit node errors
if "dns" in lower:
return (
"DNS error in exit node. "
"The exit node URL in config.json might be misspelled or unreachable. "
"Check: (1) exit_node.url is spelled correctly, "
"(2) the domain can be resolved, "
"(3) your network can reach that exit node, "
"(4) try disabling the exit node temporarily: set exit_node.enabled to false"
)
else:
return (
f"Network error from exit node: {raw} "
"Check your exit_node configuration in config.json or try disabling it."
)
# Unknown — strip the leading 'Exception: ' / 'Error: ' prefix that
# Apps Script always prepends, so the message is shorter and cleaner.
cleaned = re.sub(r'^(Exception|Error):\s*', '', raw, flags=re.IGNORECASE).strip()
return f"Relay error from Apps Script: {cleaned or raw}"
return f"Relay error: {cleaned or raw}"
# ── Low-level HTTP helpers ────────────────────────────────────────────────────
def _build_502_html(message: str) -> str:
"""Build HTML page for 502 errors with troubleshooting guide."""
safe_message = html.escape(message, quote=True)
# JSON-encode the message so it can be safely embedded as a JS string literal
js_message = json.dumps(message)
return f'''<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>502 — Relay Error</title>
<style>
*, *::before, *::after {{ box-sizing: border-box; margin: 0; padding: 0; }}
body {{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0f0c29;
background: linear-gradient(160deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
color: #e2e8f0;
}}
.card {{
max-width: 560px;
width: 100%;
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 16px;
backdrop-filter: blur(12px);
overflow: hidden;
box-shadow: 0 25px 60px rgba(0,0,0,0.5);
}}
.resource-bar {{
padding: 14px 18px;
border-bottom: 1px solid rgba(255,255,255,0.08);
background: linear-gradient(120deg, rgba(56,189,248,0.12), rgba(99,102,241,0.12));
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}}
.resource-title {{
font-size: 0.72em;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #93c5fd;
margin-right: 6px;
font-weight: 700;
}}
.resource-link {{
text-decoration: none;
color: #dbeafe;
font-size: 0.8em;
background: rgba(15,23,42,0.42);
border: 1px solid rgba(147,197,253,0.35);
border-radius: 999px;
padding: 5px 10px;
transition: all 0.18s ease;
white-space: nowrap;
}}
.resource-link:hover {{
background: rgba(15,23,42,0.65);
border-color: rgba(147,197,253,0.7);
color: #eff6ff;
transform: translateY(-1px);
}}
.card-header {{
padding: 32px 32px 24px;
border-bottom: 1px solid rgba(255,255,255,0.07);
display: flex;
align-items: center;
gap: 16px;
}}
.badge {{
background: rgba(239,68,68,0.15);
border: 1px solid rgba(239,68,68,0.35);
border-radius: 10px;
padding: 10px 12px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}}
.badge svg {{ display: block; }}
.card-header h1 {{ font-size: 1.7em; font-weight: 700; color: #f1f5f9; }}
.card-header p {{ font-size: 0.85em; color: #94a3b8; margin-top: 3px; }}
.card-body {{ padding: 24px 32px; display: flex; flex-direction: column; gap: 18px; }}
.error-box {{
background: rgba(239,68,68,0.08);
border: 1px solid rgba(239,68,68,0.25);
border-radius: 10px;
padding: 16px 18px;
}}
.error-box .label {{
font-size: 0.72em;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #f87171;
margin-bottom: 8px;
}}
.error-box div#error-text,
.error-box p {{
font-size: 0.9em;
line-height: 1.65;
color: #fca5a5;
word-break: break-word;
}}
.error-box div#error-text p {{ margin-bottom: 6px; }}
.error-box div#error-text ol {{
padding-left: 18px;
display: flex;
flex-direction: column;
gap: 4px;
margin-top: 6px;
}}
.hints-box {{
background: rgba(59,130,246,0.08);
border: 1px solid rgba(59,130,246,0.22);
border-radius: 10px;
padding: 16px 18px;
}}
.hints-box .label {{
font-size: 0.72em;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #60a5fa;
margin-bottom: 12px;
}}
.hints-box ol {{
padding-left: 18px;
display: flex;
flex-direction: column;
gap: 8px;
}}
.hints-box li {{ font-size: 0.875em; line-height: 1.55; color: #bfdbfe; }}
.hints-box li strong {{ color: #93c5fd; font-weight: 600; }}
.tips-box {{
background: rgba(245,158,11,0.07);
border: 1px solid rgba(245,158,11,0.22);
border-radius: 10px;
padding: 16px 18px;
}}
.tips-box .label {{
font-size: 0.72em;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #fbbf24;
margin-bottom: 12px;
}}
.tips-box ul {{
padding-left: 18px;
display: flex;
flex-direction: column;
gap: 7px;
}}
.tips-box li {{ font-size: 0.875em; line-height: 1.55; color: #fde68a; }}
.card-footer {{
padding: 16px 32px;
border-top: 1px solid rgba(255,255,255,0.07);
font-size: 0.78em;
color: #64748b;
text-align: center;
}}
.card-footer a {{
color: #93c5fd;
text-decoration: none;
font-weight: 600;
}}
.card-footer a:hover {{ color: #bfdbfe; }}
code {{
background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.1);
padding: 1px 6px;
border-radius: 4px;
font-family: 'SFMono-Regular', 'Consolas', 'Monaco', monospace;
font-size: 0.9em;
color: #a5b4fc;
}}
</style>
</head>
<body>
<div class="card">
<div class="resource-bar">
<span class="resource-title">MasterHttpRelayVPN</span>
<a class="resource-link" href="https://github.com/masterking32/MasterHttpRelayVPN" target="_blank" rel="noopener noreferrer">GitHub Repo</a>
<a class="resource-link" href="https://t.me/MasterDnsVPN" target="_blank" rel="noopener noreferrer">Telegram Channel</a>
<a class="resource-link" href="https://t.me/MasterDnsVPNGroup" target="_blank" rel="noopener noreferrer">Telegram Group</a>
</div>
<div class="card-header">
<div class="badge"><svg width="28" height="28" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 2L2 20h20L12 2z" stroke="#f87171" stroke-width="2" stroke-linejoin="round"/><line x1="12" y1="10" x2="12" y2="14" stroke="#f87171" stroke-width="2" stroke-linecap="round"/><circle cx="12" cy="17" r="1" fill="#f87171"/></svg></div>
<div>
<h1>502 Bad Gateway</h1>
<p>The relay could not complete your request</p>
</div>
</div>
<div class="card-body">
<div class="error-box">
<div class="label">Error Details</div>
<div id="error-text">{safe_message}</div>
</div>
<div class="hints-box">
<div class="label">How to Fix</div>
<div id="hints-content">
<ol>
<li><strong>Check Deployment ID</strong> in config.json matches Google Apps Script</li>
<li><strong>Verify AUTH_KEY</strong> matches in Code.gs and config.json</li>
<li><strong>Create NEW deployment</strong> if Code.gs was edited</li>
<li><strong>Confirm permissions:</strong> Execute as Me, Anyone can access</li>
<li><strong>Check quota</strong> — 20,000 requests/day limit</li>
</ol>
</div>
</div>
<div class="tips-box">
<div class="label">General Tips</div>
<ul>
<li>Check the proxy terminal for detailed logs</li>
<li>Test internet connectivity: open <a href="https://eth0.me" target="_blank" rel="noopener noreferrer"><code>eth0.me</code></a> in your browser</li>
<li>Scan for a working Google IP: <code>python main.py --scan</code></li>
<li>Deploy multiple Apps Script projects as backup</li>
</ul>
</div>
</div>
<div class="card-footer">
Read <a href="https://github.com/masterking32/MasterHttpRelayVPN/blob/python_testing/docs/TROUBLESHOOTING.md" target="_blank" rel="noopener noreferrer">Troubleshooting Guide</a> or run <code>python setup.py</code> for full setup help
</div>
</div>
<script>
(function () {{
const rawMsg = {js_message};
const lower = rawMsg.toLowerCase();
const container = document.getElementById('hints-content');
if (!container) return;
let hints = '';
if (lower.includes('dns')) {{
hints = `<ol>
<li><strong>Check the exit node URL</strong> spelling in config.json</li>
<li><strong>Verify the domain exists</strong> and is publicly reachable</li>
<li><strong>Disable exit node temporarily:</strong> set <code>exit_node.enabled</code> to false</li>
<li><strong>Common typo:</strong> "workefrs""workers"</li>
<li><strong>Test in browser:</strong> open the exit node URL directly</li>
</ol>`;
}} else if (lower.includes('auth') || lower.includes('unauthorized')) {{
hints = `<ol>
<li><strong>Copy AUTH_KEY</strong> exactly from Code.gs</li>
<li><strong>Paste into config.json</strong> — must match character-for-character</li>
<li><strong>Create a NEW deployment</strong> after any change</li>
<li><strong>Permissions:</strong> Execute as Me, access Anyone</li>
<li><strong>Authorize the script</strong> by running it manually once</li>
</ol>`;
}} else if (lower.includes('quota') || lower.includes('too many') || lower.includes('service invoked')) {{
hints = `<ol>
<li><strong>Daily limit hit:</strong> 20,000 URL-fetch calls/day</li>
<li><strong>Wait for reset</strong> — quotas reset at midnight UTC</li>
<li><strong>Add more scripts:</strong> deploy 23 separate projects</li>
<li><strong>Use script_ids array</strong> in config.json for load balancing</li>
<li><strong>Restart proxy</strong> after updating config</li>
</ol>`;
}} else if (lower.includes('html') || lower.includes('deployment') || lower.includes('not_found')) {{
hints = `<ol>
<li><strong>Use the Deployment ID</strong>, not the Script ID</li>
<li><strong>Check the deployment is active</strong> (not archived)</li>
<li><strong>Create a NEW deployment</strong> after editing Code.gs</li>
<li><strong>Permissions:</strong> Execute as Me, access Anyone</li>
<li><strong>Verify script_id</strong> in config.json is correct</li>
</ol>`;
}} else {{
hints = `<ol>
<li><strong>Verify Deployment ID</strong> matches Google Apps Script</li>
<li><strong>Check AUTH_KEY</strong> matches in both places</li>
<li><strong>Create NEW deployment</strong> if Code.gs was edited</li>
<li><strong>Confirm permissions:</strong> Me + Anyone access</li>
<li><strong>Check proxy terminal</strong> for more details</li>
</ol>`;
}}
container.innerHTML = hints;
// Keep error details text-only to avoid any HTML/script injection path.
}})();
</script>
</body>
</html>'''
def error_response(status: int, message: str) -> bytes:
"""Build a minimal HTML error response."""
body = f"<html><body><h1>{status}</h1><p>{message}</p></body></html>"
"""Build an HTML error response. For 502 errors, includes troubleshooting guide."""
if status == 502:
body = _build_502_html(message)
else:
body = f"<html><body><h1>{status}</h1><p>{message}</p></body></html>"
return (
f"HTTP/1.1 {status} Error\r\n"
f"Content-Type: text/html\r\n"
f"Content-Length: {len(body)}\r\n"
f"Content-Type: text/html; charset=utf-8\r\n"
f"Content-Length: {len(body.encode())}\r\n"
f"\r\n"
f"{body}"
).encode()
@@ -433,6 +759,63 @@ def parse_relay_response(body: bytes, max_body_bytes: int) -> bytes:
data = load_relay_json(text)
if data is None:
return error_response(502, f"No JSON: {text[:200]}")
# Provide a better error message based on what the response looks like
preview = text[:1000] # Larger preview to catch errors in HTML
preview_lower = preview.lower()
# Check for specific error patterns FIRST, regardless of format
if "dns" in preview_lower:
# Extract just the DNS error portion from the text
dns_match = re.search(r'(dns[^<\n]{0,200})', preview, re.IGNORECASE)
raw_hint = dns_match.group(1).strip() if dns_match else ""
error_msg = (
"DNS error in exit node. "
"The exit node URL in config.json might be misspelled or unreachable. "
"Check: (1) exit_node_url is correctly set, "
"(2) the domain can be resolved, "
"(3) your network can reach that exit node."
+ (f" [{raw_hint}]" if raw_hint else "")
)
elif "cloudflare" in preview_lower or "cf_challenge" in preview_lower:
error_msg = (
"Cloudflare error from exit node. "
"The exit node URL (Cloudflare Worker) returned a Cloudflare error page. "
"Verify: (1) exit_node.url is correctly set in config.json, "
"(2) the Cloudflare Worker deployment is working, "
"(3) try disabling exit node: set exit_node.enabled to false"
)
elif preview_lower.startswith("<"):
# HTML response from script.google.com usually indicates that
# Deployment ID is wrong/archived or the deployment was not updated.
# This signature commonly appears as a generic Google Docs wrapper.
if any(sig in preview_lower for sig in (
"web word processing, presentations and spreadsheets",
"docs.google.com",
"google docs",
)):
error_msg = (
"Wrong Apps Script deployment (script_id). "
"Google returned a generic HTML page instead of relay JSON. "
"Fix: (1) use Web App Deployment ID (not Script ID), "
"(2) confirm deployment is active/not archived, "
"(3) redeploy after editing Code.gs, "
"(4) update script_id in config.json."
)
else:
error_msg = (
"Apps Script returned HTML instead of JSON. "
"This usually means: (1) wrong Deployment ID, "
"(2) the deployment doesn't exist or is archived, "
"(3) Code.gs wasn't deployed or has syntax errors. "
"Try: Create a NEW deployment and update script_id in config.json"
)
elif "exception" in preview_lower or "error" in preview_lower:
# Looks like an error message - use it directly
error_msg = f"Relay returned an error: {preview[:200]}"
else:
error_msg = f"Invalid JSON response from relay: {preview[:200]}"
log.warning("JSON parse failed. Response: %s", preview[:200])
return error_response(502, error_msg)
return parse_relay_json(data, max_body_bytes)