earendil-works/pi · error

Proxy error: ${response.status} ${response.statusText}

Error message

Proxy error: ${response.status} ${response.statusText}

What it means

streamProxy POSTs the model/context/options to {proxyUrl}/api/stream with a Bearer authToken; any non-2xx response becomes 'Proxy error: <status> <statusText>', replaced by the server's JSON {error} field when the body parses. The throw happens inside the stream's async body, so per the StreamFn contract it reaches the caller as an error event / final AssistantMessage with stopReason 'error', not as an exception thrown from streamProxy() itself.

Source

Thrown at packages/agent/src/proxy.ts:178

				body: JSON.stringify({
					model,
					context,
					options: buildProxyRequestOptions(options),
				}),
				signal: options.signal,
			});

			if (!response.ok) {
				let errorMessage = `Proxy error: ${response.status} ${response.statusText}`;
				try {
					const errorData = (await response.json()) as { error?: string };
					if (errorData.error) {
						errorMessage = `Proxy error: ${errorData.error}`;
					}
				} catch {
					// Couldn't parse error response
				}
				throw new Error(errorMessage);
			}

			reader = response.body!.getReader();
			const decoder = new TextDecoder();
			let buffer = "";

			while (true) {
				const { done, value } = await reader.read();
				if (done) break;

				if (options.signal?.aborted) {
					throw new Error("Request aborted by user");
				}

				buffer += decoder.decode(value, { stream: true });
				const lines = buffer.split("\n");
				buffer = lines.pop() || "";

View on GitHub (pinned to 4af9d21d3b)

Solutions

  1. On 401/403, refresh authToken and retry the stream once
  2. Verify proxyUrl (scheme, host, /api/stream path) with a direct curl POST
  3. Check the proxy server's logs for the failing request to see the upstream cause
  4. If behind a gateway, confirm it forwards the Authorization header and returns JSON errors

Example fix

// before
const stream = streamProxy(model, context, { ...options, authToken, proxyUrl });

// after - consume the final message, refresh token once on 401
let token = await getToken();
for (let attempt = 0; attempt < 2; attempt++) {
  const stream = streamProxy(model, context, { ...options, authToken: token, proxyUrl });
  const msg = await stream.result;
  if (msg.stopReason !== "error" || !/^Proxy error: 40[13]/.test(msg.errorMessage ?? "")) break;
  token = await refreshToken();
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (!authToken || !proxyUrl) {
  throw new Error("streamProxy requires authToken and proxyUrl");
}
const url = new URL(proxyUrl); // throws early on malformed proxyUrl
await fetch(url.origin); // optional liveness probe before the real stream

Type guard

const isProxyError = (m: { stopReason?: string; errorMessage?: string }): boolean =>
  m.stopReason === "error" && (m.errorMessage ?? "").startsWith("Proxy error:");

Try / catch

// errors arrive on the event stream, not as exceptions
const stream = streamProxy(model, context, opts);
const msg = await stream.result;
if (isProxyError(msg)) {
  const status = /Proxy error: (\d{3})/.exec(msg.errorMessage ?? "")?.[1];
  if (status === "401" || status === "403") {
    authToken = await refreshToken();
    return streamProxy(model, context, { ...opts, authToken });
  }
  throw new Error(msg.errorMessage);
}

Prevention

When it happens

Trigger: Expired or invalid authToken producing 401/403; wrong or truncated proxyUrl hitting 404/502 from the wrong host; the proxy server failing to reach the upstream LLM provider and returning 500 with its own error message; a gateway in front returning an HTML error page so the JSON override never applies.

Common situations: Long-running apps whose token expires mid-session because refresh was never wired; proxyUrl pointing at a different deployment or missing the /api/stream route; nginx/Cloudflare stripping the Authorization header or returning non-JSON error bodies.

Understand the failure class

Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.

Related errors


AI-assisted analysis of earendil-works/pi@4af9d21d3b (2026-08-24). Data as JSON: /api/errors/4057dee0445e859e. Report an issue: GitHub.