* Tighten HTTP error schemas, keep rich causes internally Establish a clear rule: Schema.TaggedError is HTTP wire-facing — every field must be a safe public field. Data.TaggedError is internal-only; it can carry rich cause: unknown for logging/debugging and can't be accidentally wired to an HttpApi endpoint because it isn't a Schema. - OpenApiParseError (HTTP-exposed): drop error: Schema.Defect field - GraphqlIntrospectionError (HTTP-exposed): drop error: Schema.Defect field; log underlying causes via Effect.logError at construction sites in introspect.ts - OpenApiInvocationError, GraphqlInvocationError, GoogleDiscoveryParseError, GoogleDiscoveryInvocationError, KeychainError, ToolInvocationError: convert Schema.TaggedError -> Data.TaggedError, rename error -> cause - Cloud auth (UserStoreError, WorkOSError): keep empty schema + 500 annotation, log full cause via Effect.tapErrorCause at service wrappers * Extract withServiceLogging helper + errors.test.ts contract Collapse the repetitive tapErrorCause + mapError + withSpan ceremony at each service wrapper into one helper in auth/errors.ts. Add a real HttpApi integration test that pins down the contract: - Wire response contains only declared tagged-error fields - Server-side logs capture the full Cause chain (drizzle query, params, nested pg Error.cause) The test uses a real HttpApi + HttpApiBuilder.toWebHandler + fetch round-trip with a capturing Logger to verify both sides without mocking. Validates Option C from the earlier discussion: service-level logging is the only pattern that preserves the full cause because mapError discards it before any edge middleware could see it. * Remove errors.test.ts * Simplify graphql introspection error handling Drop ad-hoc Effect.logError sprinkles on branch-level failures (bad status, returned errors, missing __schema) — the tagged error's message already carries the useful info and there's no Cause chain to tap. Keep tapErrorCause + mapError inline for the two cases where an upstream error exists (HTTP request failure, JSON parse failure).
Showing the changes to packages/plugins/openapi/src (3 of 14 files). Show all
Unified · Split
| @@ -1,10 +1,10 @@ | ||
| 1 | −import { Schema } from "effect"; | |
| 1 | +import { Data, Schema } from "effect"; | |
| 2 | +import type { Option } from "effect"; | |
| 2 | 3 | |
| 3 | 4 | export class OpenApiParseError extends Schema.TaggedError<OpenApiParseError>()( |
| 4 | 5 | "OpenApiParseError", |
| 5 | 6 | { |
| 6 | 7 | message: Schema.String, |
| 7 | − error: Schema.Defect, | |
| 8 | 8 | }, |
| 9 | 9 | ) {} |
| 10 | 10 | |
| @@ -15,11 +15,10 @@ export class OpenApiExtractionError extends Schema.TaggedError<OpenApiExtraction · 4 unmodified lines | ||
| 15 | 15 | }, |
| 16 | 16 | ) {} |
| 17 | 17 | |
| 18 | −export class OpenApiInvocationError extends Schema.TaggedError<OpenApiInvocationError>()( | |
| 18 | +export class OpenApiInvocationError extends Data.TaggedError( | |
| 19 | 19 | "OpenApiInvocationError", |
| 20 | − { | |
| 21 | − message: Schema.String, | |
| 22 | − statusCode: Schema.optionalWith(Schema.Number, { as: "Option" }), | |
| 23 | − error: Schema.Defect, | |
| 24 | − }, | |
| 25 | −) {} | |
| 20 | +)<{ | |
| 21 | + readonly message: string; | |
| 22 | + readonly statusCode: Option.Option<number>; | |
| 23 | + readonly cause?: unknown; | |
| 24 | +}> {} | |
| @@ -76,7 +76,6 @@ const resolvePath = Effect.fn("OpenApi.resolvePath")(function* ( · 75 unmodified lines | ||
| 76 | 76 | return yield* new OpenApiInvocationError({ |
| 77 | 77 | message: `Missing required path parameter: ${param.name}`, |
| 78 | 78 | statusCode: Option.none(), |
| 79 | − error: undefined, | |
| 80 | 79 | }); |
| 81 | 80 | } |
| 82 | 81 | continue; |
| @@ -111,7 +110,6 @@ const resolvePath = Effect.fn("OpenApi.resolvePath")(function* ( · 28 unmodified lines | ||
| 111 | 110 | return yield* new OpenApiInvocationError({ |
| 112 | 111 | message: `Unresolved path parameters: ${[...new Set(unresolved)].join(", ")}`, |
| 113 | 112 | statusCode: Option.none(), |
| 114 | − error: undefined, | |
| 115 | 113 | }); |
| 116 | 114 | } |
| 117 | 115 | |
| @@ -245,7 +243,7 @@ export const invoke = Effect.fn("OpenApi.invoke")(function* ( · 127 unmodified lines | ||
| 245 | 243 | new OpenApiInvocationError({ |
| 246 | 244 | message: `HTTP request failed: ${err.message}`, |
| 247 | 245 | statusCode: Option.none(), |
| 248 | − error: err, | |
| 246 | + cause: err, | |
| 249 | 247 | }), |
| 250 | 248 | ), |
| 251 | 249 | ); |
| @@ -20,7 +20,6 @@ export const parse = Effect.fn("OpenApi.parse")(function* (input: string) { · 19 unmodified lines | ||
| 20 | 20 | catch: (error) => |
| 21 | 21 | new OpenApiParseError({ |
| 22 | 22 | message: `Failed to parse OpenAPI document: ${error instanceof Error ? error.message : String(error)}`, |
| 23 | − error, | |
| 24 | 23 | }), |
| 25 | 24 | }); |
| 26 | 25 | |