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.
| Merge brokered secrets: a secret a job uses only to call an API can be tied to the hosts it is for, and the job gets a stand-in in its place, which g1t fills in on the way out to those hosts and nowhere else, so the value never enters the sandbox; and a request to an allowed host that fails upstream is retried when it can be, or answered with a 502 that says why, rather than reset. | 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 | ||
| 12 | import { normalizeHost } from "./egress.ts"; | |
| 13 | ||
| 14 | /** The waits before the second and third tries, in milliseconds. */ | |
| 15 | export const RETRY_WAITS_MS: readonly number[] = [250, 1000]; | |
| 16 | ||
| 17 | /** Whether a request can be sent again as it was: no body, nothing changed. */ | |
| 18 | export 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. */ | |
| 23 | export 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 | ||
| 31 | const 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 | */ | |
| 38 | export 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 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.