| 1 | import { RelayProtectedError } from "@t3tools/contracts/relay"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Option from "effect/Option"; |
| 4 | import * as Schema from "effect/Schema"; |
| 5 | import * as HttpClientResponse from "effect/http/HttpClientResponse"; |
| 6 | import { isHttpClientError } from "effect/http/HttpClientError"; |
| 7 | |
| 8 | /** |
| 9 | * A relay request that did not succeed. `unavailable` means it may succeed on |
| 10 | * retry; the relay refused the other kinds. The description is safe to show: |
| 11 | * it carries the relay's own explanation and trace ID, never request details. |
| 12 | */ |
| 13 | export class RelayRequestError extends Schema.TaggedError<RelayRequestError>()( |
| 14 | "RelayRequestError", |
| 15 | { |
| 16 | rejection: Schema.Literals(["unauthorized", "forbidden", "rejected", "unavailable"]), |
| 17 | description: Schema.String, |
| 18 | }, |
| 19 | ) { |
| 20 | override get message(): string { |
| 21 | return this.description; |
| 22 | } |
| 23 | } |
| 24 | |
| 25 | const isRelayRequestError = Schema.is(RelayRequestError); |
| 26 | |
| 27 | export function relayRequestError(cause: unknown): RelayRequestError { |
| 28 | return isRelayRequestError(cause) |
| 29 | ? cause |
| 30 | : new RelayRequestError({ |
| 31 | rejection: "unavailable", |
| 32 | description: `Could not complete the T3 Connect relay request. ${isHttpClientError(cause) ? `The relay request failed (${cause.reason._tag}).` : "The relay returned an unexpected response."} Check this machine's network connection and relay availability, then retry.`, |
| 33 | }); |
| 34 | } |
| 35 | |
| 36 | /** Whether a failure may succeed on retry: anything but a relay refusal. */ |
| 37 | export const shouldRetryRelayRequest = (error: unknown): boolean => |
| 38 | !isRelayRequestError(error) || error.rejection === "unavailable"; |
| 39 | |
| 40 | function recoveryHint(error: RelayProtectedError): string { |
| 41 | switch (error._tag) { |
| 42 | case "RelayEnvironmentLinkLimitExceededError": |
| 43 | return "Unlink an unused environment in T3 Connect, then restart T3 Code on this machine."; |
| 44 | case "RelayAuthInvalidError": |
| 45 | return "Run `t3 connect login` to check this machine's authorization. If the stored credential was revoked, sign out with `t3 connect logout`, then run `t3 connect` again. Restart T3 Code after signing in."; |
| 46 | case "RelayEnvironmentLinkProofExpiredError": |
| 47 | case "RelayEnvironmentLinkProofInvalidError": |
| 48 | return "Check this machine's date and time, update T3 Code, then restart it."; |
| 49 | default: |
| 50 | return "Retry when the relay is available. If this continues, include the trace ID when reporting it."; |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | /** Preserve relay diagnostics before converting permanent rejections into non-retryable errors. */ |
| 55 | export const filterRelayResponse = Effect.fn("cloud.filter_relay_response")(function* ( |
| 56 | response: HttpClientResponse.HttpClientResponse, |
| 57 | ) { |
| 58 | if (response.status >= 200 && response.status < 300) return response; |
| 59 | const decoded = yield* HttpClientResponse.schemaBodyJson(RelayProtectedError)(response).pipe( |
| 60 | Effect.option, |
| 61 | ); |
| 62 | const ray = response.headers["cf-ray"]; |
| 63 | const requestId = ray && /^[a-zA-Z0-9-]{1,128}$/.test(ray) ? ` Cloudflare Ray ID: ${ray}.` : ""; |
| 64 | const description = Option.isSome(decoded) |
| 65 | ? `T3 Connect: ${decoded.value.message}. ${recoveryHint(decoded.value)} Trace ID: ${decoded.value.traceId}.` |
| 66 | : `T3 Connect relay returned HTTP ${response.status} without a recognized error response. Check relay access and any proxy or firewall restrictions, then restart T3 Code.${requestId}`; |
| 67 | |
| 68 | if (response.status === 401) { |
| 69 | return yield* new RelayRequestError({ rejection: "unauthorized", description }); |
| 70 | } |
| 71 | if (response.status === 403) { |
| 72 | return yield* new RelayRequestError({ rejection: "forbidden", description }); |
| 73 | } |
| 74 | if ( |
| 75 | response.status >= 400 && |
| 76 | response.status < 500 && |
| 77 | response.status !== 408 && |
| 78 | response.status !== 429 |
| 79 | ) { |
| 80 | return yield* new RelayRequestError({ rejection: "rejected", description }); |
| 81 | } |
| 82 | return yield* new RelayRequestError({ rejection: "unavailable", description }); |
| 83 | }); |