import { expect, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import { HttpClientRequest, HttpClientResponse } from "effect/unstable/http"; import * as HttpClientError from "effect/unstable/http/HttpClientError"; import { filterRelayResponse, relayRequestError, shouldRetryCloudLink } from "./relayResponse.ts"; const response = ( status: number, body: string | Record, headers?: Record, ) => HttpClientResponse.fromWeb( HttpClientRequest.post("https://relay.example.test/v1/client/environment-links"), typeof body === "string" ? new Response(body, { status, ...(headers ? { headers } : {}) }) : Response.json(body, { status, ...(headers ? { headers } : {}) }), ); it("reports a transport failure category without exposing request or cause details", () => { const error = relayRequestError( new HttpClientError.HttpClientError({ reason: new HttpClientError.TransportError({ request: HttpClientRequest.post("https://relay.example.test/link?token=private-token"), description: "private transport details", cause: new Error("private cause details"), }), }), ); expect(error._tag).toBe("EnvironmentHttpInternalServerError"); expect(error.message).toContain("TransportError"); expect(error.message).toContain("network connection"); expect(error.message).not.toContain("relay.example.test"); expect(error.message).not.toContain("private"); expect(shouldRetryCloudLink(error)).toBe(true); }); it.effect("reports the tunnel limit and relay trace instead of a generic 403", () => Effect.gen(function* () { const error = yield* filterRelayResponse( response(403, { _tag: "RelayEnvironmentLinkLimitExceededError", code: "environment_link_limit_exceeded", maxTunnels: 3, traceId: "trace-limit", }), ).pipe(Effect.mapError(relayRequestError), Effect.flip); expect(error._tag).toBe("EnvironmentHttpForbiddenError"); expect(error.message).toContain("at most 3 tunnels"); expect(error.message).toContain("Unlink an unused environment"); expect(error.message).toContain("Trace ID: trace-limit"); }), ); it.effect("makes revoked authorization actionable and non-retryable", () => Effect.gen(function* () { const error = yield* filterRelayResponse( response(401, { _tag: "RelayAuthInvalidError", code: "auth_invalid", reason: "invalid_bearer", traceId: "trace-auth", }), ).pipe(Effect.mapError(relayRequestError), Effect.flip); expect(error._tag).toBe("EnvironmentHttpUnauthorizedError"); expect(error.message).toContain("invalid_bearer"); expect(error.message).toContain("t3 connect login"); expect(error.message).toContain("Trace ID: trace-auth"); }), ); it.effect("reports an unrecognized access denial without printing its response body", () => Effect.gen(function* () { const error = yield* filterRelayResponse( response(403, "private upstream details", { "content-type": "text/html", "cf-ray": "abcdef1234-IAD", }), ).pipe(Effect.flip); expect(error._tag).toBe("EnvironmentHttpForbiddenError"); expect(error.message).toContain("HTTP 403"); expect(error.message).toContain("proxy or firewall"); expect(error.message).toContain("Cloudflare Ray ID: abcdef1234-IAD"); expect(error.message).not.toContain("private upstream details"); }), ); it.effect.each([408, 429, 500, 502, 503, 504])( "keeps transient HTTP %s failures retryable", (status) => Effect.gen(function* () { const error = yield* filterRelayResponse(response(status, "unavailable")).pipe(Effect.flip); expect(error._tag).toBe("EnvironmentHttpInternalServerError"); expect(error.message).toContain(`HTTP ${status}`); }), ); it.effect.each([ { status: 401, attempts: 1 }, { status: 403, attempts: 1 }, { status: 429, attempts: 2 }, { status: 503, attempts: 2 }, ])("stops rejected startup links but retries temporary failures: $status", ({ status, attempts }) => Effect.gen(function* () { let requests = 0; const result = yield* Effect.suspend(() => { requests++; return filterRelayResponse(response(requests === 1 ? status : 200, "{}")); }).pipe( Effect.mapError(relayRequestError), Effect.retry({ while: shouldRetryCloudLink, times: 1 }), Effect.result, ); expect(requests).toBe(attempts); expect(result._tag).toBe(attempts === 1 ? "Failure" : "Success"); }), ); it.effect("keeps the relay failure reason and trace when tunnel cleanup fails", () => Effect.gen(function* () { const error = yield* filterRelayResponse( response(500, { _tag: "RelayInternalError", code: "internal_error", reason: "upstream_unavailable", traceId: "trace-cleanup", }), ).pipe(Effect.flip); expect(error._tag).toBe("EnvironmentHttpInternalServerError"); expect(error.message).toContain("upstream_unavailable"); expect(error.message).toContain("Trace ID: trace-cleanup"); }), ); it.effect("leaves successful response bodies available to their decoder", () => Effect.gen(function* () { const result = yield* filterRelayResponse(response(200, '{"ok":true}')); expect(yield* result.json).toEqual({ ok: true }); }), );