mirror of
https://github.com/masterking32/MasterHttpRelayVPN.git
synced 2026-05-19 08:04:38 +03:00
feat: Enhance error handling for exit node issues and improve 502 error response with troubleshooting guide
This commit is contained in:
+389
-6
@@ -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 2–3 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)
|
||||
|
||||
Reference in New Issue
Block a user