Skip to content

Tighten HTTP error schemas, keep rich causes internally - #137

Merged
RhysSullivan merged 4 commits into
mainfrom
04-09-errors_sweep_strip_internal_details_from_http_errors
Apr 9, 2026
Merged

Tighten HTTP error schemas, keep rich causes internally#137
RhysSullivan merged 4 commits into
mainfrom
04-09-errors_sweep_strip_internal_details_from_http_errors

Conversation

@RhysSullivan

Copy link
Copy Markdown
Collaborator

Summary

Establish a clear rule for tagged errors in the codebase:

  • Schema.TaggedError = HTTP wire-facing. Every field is a safe public field. No cause, no Schema.Unknown, no Schema.Defect.
  • Data.TaggedError = internal-only. Can carry rich cause: unknown for logging/debugging. Cannot be accidentally wired to an HttpApi endpoint because it isn't a Schema — the compiler rejects it.

Actual leaks fixed (HTTP-exposed errors with unsafe fields)

  • OpenApiParseError — wired to POST /openapi/preview and POST /openapi/specs. Dropped error: Schema.Defect.
  • GraphqlIntrospectionError — wired to POST /graphql/sources. Dropped error: Schema.Defect. Underlying causes are now logged via Effect.logError at each construction site in introspect.ts, so debugging info stays in server logs.

Latent footguns closed (internal errors that had unsafe schema fields)

Converted Schema.TaggedErrorData.TaggedError and renamed errorcause:

  • ToolInvocationError (packages/core/sdk)
  • OpenApiInvocationError
  • GraphqlInvocationError
  • GoogleDiscoveryParseError
  • GoogleDiscoveryInvocationError
  • KeychainError

These errors still flow through Effect error channels exactly as before — .catchTag, yield* new …, etc. all work identically. The only difference is they're no longer Schemas, so HttpApiEndpoint.addError(…) won't compile with them. If one ever needs to be exposed over HTTP in the future, whoever adds it is forced to think about the wire format at that moment.

Cloud auth (unchanged, already clean)

UserStoreError / WorkOSError keep their empty schemas + status: 500 annotation. The Effect.tapErrorCause + Effect.logError pattern in the service wrappers (context.ts, workos.ts) ensures the full drizzle/workos cause chain lands in server logs while the HTTP response body stays {"_tag":"UserStoreError"}.

Test plan

  • bun run typecheck — all touched packages clean; remaining workspace errors are pre-existing on main (the ExecutionResult/PausedExecution mismatch in packages/core/api/src/handlers/executions.ts plus a couple unused-import warnings)
  • bun run test — 89 tests across 13 files, all passing:
    • @executor/sdk 23/23
    • @executor/plugin-openapi 27/27
    • @executor/plugin-graphql 9/9
    • @executor/plugin-google-discovery 5/5
    • @executor/plugin-mcp 18/18
    • @executor/plugin-keychain 3/3
    • @executor/cloud 4/4

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
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Apr 9, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
executor-marketing 07efb47 Commit Preview URL Apr 09 2026, 09:52 PM

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.
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).
@RhysSullivan
RhysSullivan merged commit d2ce9d7 into main Apr 9, 2026
2 checks passed
RhysSullivan added a commit that referenced this pull request May 31, 2026
* 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).
@RhysSullivan
RhysSullivan deleted the 04-09-errors_sweep_strip_internal_details_from_http_errors branch June 11, 2026 08:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant