|
11 | 11 | // --------------------------------------------------------------------------- |
12 | 12 |
|
13 | 13 | import { describe, expect, it } from "@effect/vitest"; |
14 | | -import { Effect, Exit, Schema } from "effect"; |
15 | | -import { FetchHttpClient, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"; |
| 14 | +import { Cause, Data, Effect, Exit, Layer, Schema } from "effect"; |
| 15 | +import { |
| 16 | + FetchHttpClient, |
| 17 | + HttpClient, |
| 18 | + HttpClientError, |
| 19 | + HttpServerRequest, |
| 20 | + HttpServerResponse, |
| 21 | +} from "effect/unstable/http"; |
16 | 22 | import { |
17 | 23 | HttpApi, |
18 | 24 | HttpApiBuilder, |
@@ -43,8 +49,10 @@ import { |
43 | 49 |
|
44 | 50 | import { openApiPlugin } from "./plugin"; |
45 | 51 |
|
46 | | -const testPlugins = () => |
47 | | - [openApiPlugin({ httpClientLayer: FetchHttpClient.layer }), memoryCredentialsPlugin()] as const; |
| 52 | +class AdapterDefect extends Data.TaggedError("AdapterDefect") {} |
| 53 | + |
| 54 | +const testPlugins = (httpClientLayer = FetchHttpClient.layer) => |
| 55 | + [openApiPlugin({ httpClientLayer }), memoryCredentialsPlugin()] as const; |
48 | 56 |
|
49 | 57 | // `/things` GET op `listThings` under group "things" → tool path |
50 | 58 | // `things.listThings`, used verbatim (dots and all) as the address tool segment. |
@@ -114,9 +122,11 @@ const FailureApi = HttpApi.make("failuresTest") |
114 | 122 |
|
115 | 123 | // Build an executor + connection from the FailureApi HttpApi against an |
116 | 124 | // arbitrary baseUrl (used for the Node-transport socket-drop / slow cases). |
117 | | -const buildExecutor = (baseUrl: string) => |
| 125 | +const buildExecutor = (baseUrl: string, httpClientLayer = FetchHttpClient.layer) => |
118 | 126 | Effect.gen(function* () { |
119 | | - const executor = yield* createExecutor(makeTestConfig({ plugins: testPlugins() })); |
| 127 | + const executor = yield* createExecutor( |
| 128 | + makeTestConfig({ plugins: testPlugins(httpClientLayer) }), |
| 129 | + ); |
120 | 130 | yield* executor.openapi.addSpec( |
121 | 131 | makeOpenApiHttpApiTestIntegrationConfig(FailureApi, { slug: "f", baseUrl }), |
122 | 132 | ); |
@@ -450,6 +460,62 @@ describe("OpenAPI upstream failure modes", () => { |
450 | 460 | }), |
451 | 461 | ); |
452 | 462 |
|
| 463 | + it.effect("request encoding failures remain invocation failures", () => |
| 464 | + Effect.gen(function* () { |
| 465 | + const httpClientLayer = Layer.succeed( |
| 466 | + HttpClient.HttpClient, |
| 467 | + HttpClient.make((request) => |
| 468 | + Effect.fail( |
| 469 | + new HttpClientError.HttpClientError({ |
| 470 | + reason: new HttpClientError.EncodeError({ request, cause: new AdapterDefect() }), |
| 471 | + }), |
| 472 | + ), |
| 473 | + ), |
| 474 | + ); |
| 475 | + const { executor, address } = yield* buildExecutor( |
| 476 | + "https://upstream.example", |
| 477 | + httpClientLayer, |
| 478 | + ); |
| 479 | + const exit = yield* executor.execute(address, {}).pipe(Effect.exit); |
| 480 | + expect(Exit.isFailure(exit)).toBe(true); |
| 481 | + }), |
| 482 | + ); |
| 483 | + |
| 484 | + it.effect("transport defects remain defects", () => |
| 485 | + Effect.gen(function* () { |
| 486 | + const defect = new AdapterDefect(); |
| 487 | + const httpClientLayer = Layer.succeed( |
| 488 | + HttpClient.HttpClient, |
| 489 | + HttpClient.make(() => Effect.die(defect)), |
| 490 | + ); |
| 491 | + const { executor, address } = yield* buildExecutor( |
| 492 | + "https://upstream.example", |
| 493 | + httpClientLayer, |
| 494 | + ); |
| 495 | + const exit = yield* executor.execute(address, {}).pipe(Effect.exit); |
| 496 | + expect(Exit.isFailure(exit)).toBe(true); |
| 497 | + expect(Exit.match(exit, { onFailure: Cause.hasDies, onSuccess: () => false })).toBe(true); |
| 498 | + }), |
| 499 | + ); |
| 500 | + |
| 501 | + it.effect("interrupted transport remains interrupted", () => |
| 502 | + Effect.gen(function* () { |
| 503 | + const httpClientLayer = Layer.succeed( |
| 504 | + HttpClient.HttpClient, |
| 505 | + HttpClient.make(() => Effect.interrupt), |
| 506 | + ); |
| 507 | + const { executor, address } = yield* buildExecutor( |
| 508 | + "https://upstream.example", |
| 509 | + httpClientLayer, |
| 510 | + ); |
| 511 | + const exit = yield* executor.execute(address, {}).pipe(Effect.exit); |
| 512 | + expect(Exit.isFailure(exit)).toBe(true); |
| 513 | + expect(Exit.match(exit, { onFailure: Cause.hasInterrupts, onSuccess: () => false })).toBe( |
| 514 | + true, |
| 515 | + ); |
| 516 | + }), |
| 517 | + ); |
| 518 | + |
453 | 519 | // Port 1 refuses immediately. The same path used to throw `Internal tool |
454 | 520 | // error [hex]` because the raw HttpClientError carries the request URL. |
455 | 521 | it.effect("connection refused returns upstream_unreachable without leaking the path", () => |
|
0 commit comments