Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
56 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

A sandbox's request to an allowed host that fails upstream no longer resets the sandbox's connection. The runner Worker sent each one on with a bare fetch, so a reset or a failure upstream (as under npm ci's many tarball fetches) was thrown out of the outbound handler, and npm saw ECONNRESET and gave up. Forwarding is now one function (forward.ts) that egress() and the brokered-secret helper both use: a GET or HEAD with no body is tried twice more, after 250 ms and then a second, and what still fails comes back as a 502 whose plain-text body names the host and the error, with x-g1t-egress: upstream-failed, which npm and most tools retry. A request with a body is never sent twice. Each failure is one line of the Worker's log, with the host, the method and the error.1/**
2 * How the outbound handler sends a sandbox's request on to an allowed
3 * host. An upstream that resets the connection or fails outright would
4 * otherwise throw out of the handler, and the sandbox would see its own
5 * connection reset, which tools such as npm count as a failure rather
6 * than something to retry. So a request that can be sent again (a GET or
7 * HEAD with no body) is tried twice more after a short wait, and what
8 * still fails comes back as a `502` saying which host and why, which
9 * those tools do retry. It never throws.
10 */
11
12import { normalizeHost } from "./egress.ts";
13
14/** The waits before the second and third tries, in milliseconds. */
15export const RETRY_WAITS_MS: readonly number[] = [250, 1000];
16
17/** Whether a request can be sent again as it was: no body, nothing changed. */
18export function replayable(request: Request): boolean {
19 return (request.method === "GET" || request.method === "HEAD") && request.body === null;
20}
21
22/** What a request that could not be sent gets back. */
23export function upstreamFailed(host: string, error: unknown): Response {
24 const why = error instanceof Error ? error.message : String(error);
25 return new Response(`g1t egress: ${normalizeHost(host)} could not be reached: ${why}\n`, {
26 status: 502,
27 headers: { "content-type": "text/plain; charset=utf-8", "x-g1t-egress": "upstream-failed" },
28 });
29}
30
31const wait = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
32
33/**
34 * Sends `request` with `send` (`fetch`), trying a replayable one again on
35 * a thrown error, and answering `502` when it cannot. Each failure is one
36 * line of the Worker's log.
37 */
38export async function forward(
39 request: Request,
40 send: (request: Request) => Promise<Response> = fetch,
41 sleep: (ms: number) => Promise<void> = wait,
42): Promise<Response> {
43 const host = new URL(request.url).host;
44 const tries = replayable(request) ? RETRY_WAITS_MS.length + 1 : 1;
45 let failure: unknown = null;
46 for (let attempt = 1; attempt <= tries; attempt++) {
47 if (attempt > 1) await sleep(RETRY_WAITS_MS[attempt - 2]);
48 try {
49 return await send(request);
50 } catch (error) {
51 failure = error;
52 console.log("egress upstream failed", normalizeHost(host), request.method, `try ${attempt} of ${tries}`, String(error));
53 }
54 }
55 return upstreamFailed(host, failure);
56}