diff --git a/api/relay.ts b/api/relay.ts index 674c6c0..6058ccb 100644 --- a/api/relay.ts +++ b/api/relay.ts @@ -105,92 +105,6 @@ function handleHealth(): Response { ); } -function handleDocs(): Response { - const html = ` - - - - - Edge Proxy Relay — Docs - - - -
-

Edge Proxy Relay

-

Forward HTTP requests to any target server via the x-relay-target header.

- -

Endpoints

- -
-

GET /health

-

Health check. Returns 200 OK with server status, uptime, and version.

-
- -
-

GET /docs

-

This page.

-
- -
-

Any Path (Catch-all Relay)

-

Send a request with the x-relay-target header and this proxy forwards it.

-
- -

Usage — HTTP Relay

-
curl -s \\
-  -H "x-relay-target: https://httpbin.org" \\
-  -H "x-relay-path: /get" \\
-  "https://your-proxy.example/any/path"
- - - - -
HeaderRequiredDescription
x-relay-targetYesBase URL of the upstream (http:// or https://)
x-relay-pathNoPath to append (default: /)
- -

Status Codes

- - - - - - - - - -
CodeMeaning
204CORS preflight success (OPTIONS)
400Missing x-relay-target header
403Target blocked (SSRF protection / not allowed)
413Request body exceeds size limit
429Rate limit exceeded
502Upstream network / DNS error
504Upstream timeout
- -

Note: WebSocket relay is not available on this deployment.

-
- -`; - - return new Response(html, { - status: 200, - headers: { - "Content-Type": "text/html; charset=utf-8", - "Access-Control-Allow-Origin": "*", - }, - }); -} - function handleIndex(): Response { const html = ` @@ -212,7 +126,7 @@ function handleIndex(): Response {

Edge Proxy Relay

Server is running

-

/health · /docs

+

/health

`; @@ -333,7 +247,7 @@ async function handleRelay(req: Request): Promise { // ── Execute upstream fetch ────────────────────────────────────── let response: Response; try { - response = await fetch(targetUrlString, fetchOptions); + response = await fetch(targetUrlString, fetchOptions); } catch (err) { const classified = classifyFetchError(err); logRelayEvent({ @@ -423,11 +337,18 @@ export default { if (authErr) return authErr; try { const body = await req.json(); - return handleChatCompletion(body); - } catch { + return await handleChatCompletion(body); + } catch (err) { + const isJsonError = err instanceof Error && (err.name === "SyntaxError" || err.message.includes("JSON")); + if (isJsonError) { + return new Response( + JSON.stringify({ error: { message: "Invalid JSON body", type: "invalid_request_error" } }), + { status: 400, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" } }, + ); + } return new Response( - JSON.stringify({ error: { message: "Invalid JSON body", type: "invalid_request_error" } }), - { status: 400, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" } }, + JSON.stringify({ error: { message: err instanceof Error ? err.message : "Internal Server Error", type: "server_error" } }), + { status: 500, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" } }, ); } } @@ -440,11 +361,24 @@ export default { if (authErr) return authErr; try { const body = await req.json(); - return handleAnthropicMessages(body); - } catch { + return await handleAnthropicMessages(body); + } catch (err) { + const isJsonError = err instanceof Error && (err.name === "SyntaxError" || err.message.includes("JSON")); + if (isJsonError) { + return new Response( + JSON.stringify({ + type: "error", + error: { message: "Invalid JSON body", type: "invalid_request_error" }, + }), + { status: 400, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" } }, + ); + } return new Response( - JSON.stringify({ error: { message: "Invalid JSON body", type: "invalid_request_error" } }), - { status: 400, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" } }, + JSON.stringify({ + type: "error", + error: { message: err instanceof Error ? err.message : "Internal Server Error", type: "server_error" }, + }), + { status: 500, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" } }, ); } } diff --git a/src/lib/mimo-auth.ts b/src/lib/mimo-auth.ts index 69741fc..632ea02 100644 --- a/src/lib/mimo-auth.ts +++ b/src/lib/mimo-auth.ts @@ -34,11 +34,26 @@ const EXPIRY_BUFFER_MS = 300_000; // 5 minutes * Uses Web Crypto API (available in Bun, Workers, and Node.js 20+). */ async function generateDeviceFingerprint(): Promise { - const hostname = os.hostname(); - const platform = process.platform; - const arch = process.arch; - const cpus = os.cpus(); - const cpuModel = cpus.length > 0 ? (cpus[0]?.model ?? "unknown") : "unknown"; + let hostname = "unknown"; + try { + hostname = os.hostname() ?? "unknown"; + } catch { + // OS module hostname not available in this sandbox + } + + const platform = process.platform ?? "unknown"; + const arch = process.arch ?? "unknown"; + + let cpuModel = "unknown"; + try { + const cpus = os.cpus(); + if (cpus && cpus.length > 0) { + cpuModel = cpus[0]?.model ?? "unknown"; + } + } catch { + // OS module cpus not available in this sandbox + } + const username = process.env.USER ?? process.env.USERNAME ?? "unknown"; const raw = `${hostname}|${platform}|${arch}|${cpuModel}|${username}`; diff --git a/src/lib/router.ts b/src/lib/router.ts index 3204e1a..3a75a16 100644 --- a/src/lib/router.ts +++ b/src/lib/router.ts @@ -9,7 +9,6 @@ import { createErrorResponse, createCorsPreflightResponse, getCorsHeaders, - classifyFetchError, } from "./relay-utils"; import { checkBodySize } from "../middleware/body-limiter"; @@ -615,11 +614,18 @@ export async function handleRequest( try { const body = await req.json(); const sessionId = crypto.randomUUID(); - return handleChatCompletion(body, proxyPool!, sessionPool!, sessionId); - } catch { + return await handleChatCompletion(body, proxyPool!, sessionPool!, sessionId); + } catch (err) { + const isJsonError = err instanceof Error && (err.name === "SyntaxError" || err.message.includes("JSON")); + if (isJsonError) { + return new Response( + JSON.stringify({ error: { message: "Invalid JSON body", type: "invalid_request_error" } }), + { status: 400, headers: { "Content-Type": "application/json", ...getCorsHeaders() } }, + ); + } return new Response( - JSON.stringify({ error: { message: "Invalid JSON body", type: "invalid_request_error" } }), - { status: 400, headers: { "Content-Type": "application/json", ...getCorsHeaders() } }, + JSON.stringify({ error: { message: err instanceof Error ? err.message : "Internal Server Error", type: "server_error" } }), + { status: 500, headers: { "Content-Type": "application/json", ...getCorsHeaders() } }, ); } } @@ -634,14 +640,24 @@ export async function handleRequest( const body = await req.json(); const sessionId = crypto.randomUUID(); const anthropicVersion = req.headers.get("anthropic-version") ?? undefined; - return handleAnthropicMessages(body, proxyPool!, sessionPool!, sessionId, undefined, anthropicVersion); - } catch { + return await handleAnthropicMessages(body, proxyPool!, sessionPool!, sessionId, undefined, anthropicVersion); + } catch (err) { + const isJsonError = err instanceof Error && (err.name === "SyntaxError" || err.message.includes("JSON")); + if (isJsonError) { + return new Response( + JSON.stringify({ + type: "error", + error: { message: "Invalid JSON body", type: "invalid_request_error" }, + }), + { status: 400, headers: { "Content-Type": "application/json", ...getCorsHeaders() } }, + ); + } return new Response( JSON.stringify({ type: "error", - error: { message: "Invalid JSON body", type: "invalid_request_error" }, + error: { message: err instanceof Error ? err.message : "Internal Server Error", type: "server_error" }, }), - { status: 400, headers: { "Content-Type": "application/json", ...getCorsHeaders() } }, + { status: 500, headers: { "Content-Type": "application/json", ...getCorsHeaders() } }, ); } }