Skip to content

fix(cloudflare): tag Turnstile missing widgets as NotFound#303

Open
agcty wants to merge 3 commits into
alchemy-run:mainfrom
agcty:codex/cloudflare-turnstile-not-found
Open

fix(cloudflare): tag Turnstile missing widgets as NotFound#303
agcty wants to merge 3 commits into
alchemy-run:mainfrom
agcty:codex/cloudflare-turnstile-not-found

Conversation

@agcty

@agcty agcty commented May 20, 2026

Copy link
Copy Markdown
Contributor

Summary

  • map Turnstile missing-widget 404 responses to a typed NotFound error
  • let per-operation error matchers run for non-JSON and non-envelope Cloudflare errors
  • add regression tests for the raw 404 shapes that showed up while testing the alchemy-effect Turnstile resource

This unblocks cleaning up alchemy-effect PR #370 so it can catch NotFound instead of checking for CloudflareHttpError + status 404.

Verification

  • bun --filter @distilled.cloud/cloudflare test -- test/client-api.test.ts
  • bun --filter @distilled.cloud/cloudflare typecheck
  • bun --filter @distilled.cloud/cloudflare check

Comment on lines +16 to +24
// Errors
// =============================================================================

export class NotFound extends Schema.TaggedErrorClass<NotFound>()("NotFound", {
message: Schema.String,
}) {}
// Turnstile returns a bare 404 for missing widgets instead of Cloudflare's
// normal error envelope, so match the HTTP status directly.
T.applyErrorMatchers(NotFound, [{ status: 404 }]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these are generated files do not modify them directly. add a patch and then rerun generate

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed: moved this into packages/cloudflare/patches/turnstile/{getWidget,deleteWidget}.json and reran bun scripts/generate.ts --service turnstile, so turnstile.ts is generated again.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we making changes to this file?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this change is needed because Turnstile returns bare/non-envelope 404 responses for missing widgets. the generated operation patch can now declare { status: 404 }, but api.ts has to try operation-specific matchers before falling back to generic CloudflareHttpError for those non-envelope responses. added comments and regression tests for both non-json and non-envelope cases.

@agcty agcty force-pushed the codex/cloudflare-turnstile-not-found branch from b1121fc to f497a5d Compare May 20, 2026 10:18
Comment thread packages/cloudflare/src/client/api.ts Outdated
@@ -256,10 +256,55 @@ function findMatchingError(
return bestMatch;
}

function matchOperationError(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, why is this necessary?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems to be to handle when there is no envelope from cloudflare; I found it weird when i first looked at this, but it seems fine

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a // comment above matchOperationError explaining the Cloudflare no-envelope case. The generated Turnstile operation patch declares the 404 matcher, but api.ts has to try operation-specific matchers before falling back to CloudflareHttpError when Cloudflare returns a bare HTTP failure. Re-ran bun --filter @distilled.cloud/cloudflare check and bun run vitest run test/client-api.test.ts from packages/cloudflare.

@agcty agcty force-pushed the codex/cloudflare-turnstile-not-found branch from f497a5d to 6750db7 Compare May 22, 2026 07:30
Comment thread packages/cloudflare/src/client/api.ts Outdated
Comment on lines +269 to +279
if (!errors || errors.length === 0) return undefined;

const errorSchemas = new Map<string, Schema.Top>();
for (const errorSchema of errors) {
const identifier = extractTagFromAst(
(errorSchema as unknown as Schema.Top).ast,
);
if (identifier) {
errorSchemas.set(identifier, errorSchema as unknown as Schema.Top);
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't do all this work on every request. Instead, it should create this map and return a function for matching, that way we cache the map in the closure once.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed, lmk what you think

status: number,
errorBody: unknown,
errors?: readonly ApiErrorClass[],
headers?: Record<string, string | undefined>,
): Effect.Effect<never, unknown> => {
const matchOperationError = getOperationErrorMatcher(errors);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not caching anything. It bulds the matcher on every request still

Comment on lines +285 to +286
const cached = operationErrorMatcherCache.get(errors);
if (cached) return cached;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a global map is a massive code smell

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked into, i can see why AI is doing that. API.make's matchError function is limiting. We really need to rework this part of the codebase.

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.

3 participants