History

Tighten HTTP error schemas, keep rich causes internally (#137)

* 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).

Rhys Sullivan committed 7 months ago

Commit d2ce9d71 parent 0ccccd914 files · +93 −107Browse files

Showing the changes to packages/plugins/openapi/src (3 of 14 files). Show all

Unified · Split

3 files
  1. packages/plugins/openapi/src/sdk/errors.ts +8 −9
  2. packages/plugins/openapi/src/sdk/invoke.ts +1 −3
  3. packages/plugins/openapi/src/sdk/parse.ts +0 −1
packages/plugins/openapi/src/sdk/errors.ts+8−9
@@ -1,10 +1,10 @@
1import { Schema } from "effect";1import { Data, Schema } from "effect";
2import type { Option } from "effect";
2 3
3export class OpenApiParseError extends Schema.TaggedError<OpenApiParseError>()(4export class OpenApiParseError extends Schema.TaggedError<OpenApiParseError>()(
4 "OpenApiParseError",5 "OpenApiParseError",
5 {6 {
6 message: Schema.String,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
18export class OpenApiInvocationError extends Schema.TaggedError<OpenApiInvocationError>()(18export class OpenApiInvocationError extends Data.TaggedError(
19 "OpenApiInvocationError",19 "OpenApiInvocationError",
20 {20)<{
21 message: Schema.String,21 readonly message: string;
22 statusCode: Schema.optionalWith(Schema.Number, { as: "Option" }),22 readonly statusCode: Option.Option<number>;
23 error: Schema.Defect,23 readonly cause?: unknown;
24 },24}> {}
25) {}
packages/plugins/openapi/src/sdk/invoke.ts+1−3
@@ -76,7 +76,6 @@ const resolvePath = Effect.fn("OpenApi.resolvePath")(function* ( · 75 unmodified lines
76 return yield* new OpenApiInvocationError({76 return yield* new OpenApiInvocationError({
77 message: `Missing required path parameter: ${param.name}`,77 message: `Missing required path parameter: ${param.name}`,
78 statusCode: Option.none(),78 statusCode: Option.none(),
79 error: undefined,
80 });79 });
81 }80 }
82 continue;81 continue;
@@ -111,7 +110,6 @@ const resolvePath = Effect.fn("OpenApi.resolvePath")(function* ( · 28 unmodified lines
111 return yield* new OpenApiInvocationError({110 return yield* new OpenApiInvocationError({
112 message: `Unresolved path parameters: ${[...new Set(unresolved)].join(", ")}`,111 message: `Unresolved path parameters: ${[...new Set(unresolved)].join(", ")}`,
113 statusCode: Option.none(),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 new OpenApiInvocationError({243 new OpenApiInvocationError({
246 message: `HTTP request failed: ${err.message}`,244 message: `HTTP request failed: ${err.message}`,
247 statusCode: Option.none(),245 statusCode: Option.none(),
248 error: err,246 cause: err,
249 }),247 }),
250 ),248 ),
251 );249 );
packages/plugins/openapi/src/sdk/parse.ts+0−1
@@ -20,7 +20,6 @@ export const parse = Effect.fn("OpenApi.parse")(function* (input: string) { · 19 unmodified lines
20 catch: (error) =>20 catch: (error) =>
21 new OpenApiParseError({21 new OpenApiParseError({
22 message: `Failed to parse OpenAPI document: ${error instanceof Error ? error.message : String(error)}`,22 message: `Failed to parse OpenAPI document: ${error instanceof Error ? error.message : String(error)}`,
23 error,
24 }),23 }),
25 });24 });
26 25