Skip to content

chore: use Response.json in edge functions, drop two dead declarations - #36

Merged
Programcode merged 1 commit into
mainfrom
chore/edge-function-stdlib-cleanup
Sep 7, 2026
Merged

Programcode merged 1 commit into
mainfrom
chore/edge-function-stdlib-cleanup

Conversation

@Programcode

@Programcode Programcode commented Sep 6, 2026 •

Copy link
Copy Markdown
Owner

Third PR from the over-engineering audit. Scoped to supabase/functions/. Independent of #34, #35, #37, and #38: no file overlap, based directly on current main.

Response.json

Three functions hand-rolled the same wrapper: stringify a body, spread CORS headers, add Content-Type: application/json. Response.json does the content type itself, so each wrapper now delegates. Call sites are untouched.

  • ai-stream/index.ts — jsonResponse
  • import-source/index.ts — jsonResponse
  • announcements/index.ts — json

Where this is not a drop-in, and why it is safe here

Response.json differs from the old form in two ways. Both were checked rather than assumed.

1. Response.json(undefined) throws.

$ deno eval 'try { Response.json(undefined) } catch (e) { console.log("THROWS:", e.constructor.name) }
             const old = new Response(JSON.stringify(undefined)); console.log("old body:", JSON.stringify(await old.text()))'
THROWS: TypeError
old body: ""

The old form returned an empty body; the new one would be an unhandled 500. Both remaining wrappers are typed unknown, so this matters. Every call site passes an object literal:

$ rg -n 'jsonResponse\(' supabase/functions/import-source/index.ts   # 12 call sites, all { ... } literals
$ rg -n '\bjson\(' supabase/functions/announcements/index.ts          # 3 call sites: { announcements: [] }, { announcements }, { success: true }

No call site can pass undefined. ai-stream's wrapper is typed Record<string, unknown> and cannot.

2. Header precedence. The old wrappers spread CORS headers first and then forced Content-Type, overriding any caller value. Response.json defers to a caller-supplied content-type. Unreachable here: no getCorsHeaders in this repo returns one.

3. Content type is identical. Verified rather than assumed:

$ deno eval 'const r = Response.json({ok:true},{status:201,headers:{"Access-Control-Allow-Origin":"https://x.dev"}}); ...'
content-type: "application/json"
status: 201
cors kept: https://x.dev

An earlier revision of this description claimed Response.json appends ;charset=UTF-8 and blamed Node/undici. That was simply wrong: the WHATWG spec sets exactly application/json, and Node 22 agrees (node -e 'console.log(Response.json({}).headers.get("content-type"))' → application/json). There is no charset difference on any platform.

Dead declarations removed

  • _shared/cors.ts — a corsHeaders object with Access-Control-Allow-Origin: "*". No importer, no string-built specifier, no alias in supabase/functions/deno.json, no entry in supabase/config.toml. Every function defines its own getCorsHeaders(origin) with an explicit allowlist, so this file read like the project's CORS policy while being wired to nothing. Deleting it removes the trap; it changes no policy.

    _shared/native-cors.test.ts is unaffected, and the mechanism matters: it iterates a hardcoded list of 10 function names and reads ../<name>/index.ts as text, asserting each contains "capacitor://localhost". It performs no directory walk over supabase/functions/**, has no file-count assertion, and makes no "no wildcard anywhere" claim. Deleting a file in _shared cannot change what it enumerates.

  • type Source in ai/index.ts — never exported, never referenced. deno check on that entrypoint proves it.

Rejected during this pass

Two findings I tried and backed out, both because the "simpler" form was wrong:

  1. callbackOrigin(url) → url.origin in create-checkout/checkout-security.ts. Looks like a textbook stdlib swap. It is not: for the iOS app's capacitor://localhost scheme, url.origin returns the string "null" (opaque origin), while ${url.protocol}//${url.host} returns capacitor://localhost. The hand-rolled version is deliberately correct for non-special schemes. src/test/checkout-security.test.ts caught this immediately.
  2. Inlining narrowSparringDatabase(database: object) in ai/index.ts. Reads like a pointless one-line cast wrapper. Taking the parameter as object is load-bearing: it widens the type and breaks a deep instantiation chain. Casting sparringSupabase as SparringDatabase directly fails deno check with TS2589, "Type instantiation is excessively deep and possibly infinite."

Reported, not applied

getCorsHeaders is duplicated across 11 edge functions, 6 of them byte-identical. Consolidating it into _shared would be a real cut, but it edits the CORS trust boundary of every deployed function and would require redeploying all 11. That belongs in its own reviewed change, not a cleanup PR.

Verification

check result
npm run lint pass
npm run typecheck pass
npm test -- --coverage 95 files, 925 tests pass, coverage 90.45% stmts
npm run test:edge 121 pass
deno check on the 4 touched function entrypoints pass
npm run build pass
npm run test:e2e 126 pass

(The diff touches 4 function entrypoints plus the deleted _shared/cors.ts. An earlier revision of this table said "all 5 touched functions", which double-counted the deleted file as a function.)

Not run: the staging suite, which needs protected credentials, and the DB gate, since there are no migrations here. These functions are not deployed by this PR.

Summary by CodeRabbit

  • Refactor
    • Streamlined JSON responses across several backend services using the platform’s built-in response handling.
    • Removed unused internal definitions and shared configuration.

@coderabbitai

coderabbitai Bot commented Sep 6, 2026 •

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: d81d308e-7605-475e-b61c-d69c69e26d1f

📥 Commits

Reviewing files that changed from the base of the PR and between c7d3437 and 91353eb.

📒 Files selected for processing (5)
  • supabase/functions/_shared/cors.ts
  • supabase/functions/ai-stream/index.ts
  • supabase/functions/ai/index.ts
  • supabase/functions/announcements/index.ts
  • supabase/functions/import-source/index.ts
💤 Files with no reviewable changes (2)
  • supabase/functions/_shared/cors.ts
  • supabase/functions/ai/index.ts

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The changes standardize JSON response construction in three Supabase Edge Functions, remove an unused AI type, and remove the shared corsHeaders export.

Changes

Edge Function cleanup

Layer / File(s) Summary
Native JSON response helpers
supabase/functions/ai-stream/index.ts, supabase/functions/announcements/index.ts, supabase/functions/import-source/index.ts
JSON helpers now use Response.json while preserving applicable response status and CORS headers.
Shared CORS and AI type cleanup
supabase/functions/_shared/cors.ts, supabase/functions/ai/index.ts
The shared corsHeaders export and the unused local Source type are removed.

Estimated code review effort: 2 (Simple) | ~10 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Essentials by visiting https://app.coderabbit.ai/settings/billing.

Comment @coderabbitai help to get the list of available commands.

@vercel

vercel Bot commented Sep 6, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
debate-navigator Ready Ready Preview Sep 6, 2026 8:50pm UTC

@Programcode

Copy link
Copy Markdown
Owner Author

Interim independent review while CodeRabbit is rate limited (Free plan allows 1 review/hour).

Reviewer: Claude Opus 4.5, run locally against git diff origin/main..origin/<branch> with a deletion-safety and claim-accuracy brief. Full output:

**BLOCKERS:** none

**SHOULD-FIX:** none

**NITS:**
1. Commit message says "925 unit tests, 121 edge tests, ... 126 Chromium e2e tests" but PR description table shows the same counts with slightly different phrasing. Minor inconsistency: commit says "121 edge tests" while PR says "121 pass" for `test:edge`. Not a problem, just noting the counts match.

**VERDICT:** Approve.

The deletions are safe:
- `_shared/cors.ts`: Grep confirms zero importers. The PR correctly notes that `_shared/native-cors.test.ts` reads function sources as text strings rather than importing this module.
- `type Source` in `ai/index.ts`: Local type, not exported, no references in the diff context or elsewhere in the file.
- `Response.json` refactors: The wrapper functions preserve the same signature and behavior. `Response.json` sets `content-type: application/json` automatically; the explicit verification under Deno 2.9.6 confirming no charset suffix is thorough and correct.

The claims about Capacitor origin handling and the TS2589 deep-instantiation issue explain why those changes were rejected—good judgment documented. No syntax errors, no orphaned references, no test logic changes.

CodeRabbit will still review this PR when its hourly window resets; this does not replace it.

@Programcode

Copy link
Copy Markdown
Owner Author

Independent review, second pass, model upgraded to Claude Opus 5 (the earlier comment on this PR used Opus 4.5). Run locally against git diff origin/main..origin/<branch> through cliproxyapi, briefed on deletion safety and claim accuracy.

Every should-fix finding below has been addressed in the current head. Full output:

BLOCKERS: none

SHOULD-FIX:
1. `Response.json` is not a drop-in for `JSON.stringify` when the body can be `undefined`. `import-source/jsonResponse(body: unknown, ...)` and `announcements/json(data: unknown, ...)` both accept `unknown`. Old form: `JSON.stringify(undefined)` → `undefined` → a valid response with a null body. New form: `Response.json(undefined)` throws `TypeError` (data not JSON-serializable), i.e. an unhandled 500 instead of a 200/204-ish empty body. `ai-stream` is safe (`Record<string, unknown>`). Please post the call-site grep for those two wrappers showing every argument is a defined value (watch for `json(result, ...)` where `result` comes from an optional lookup or a function with an implicit-undefined return path). The PR's "no observable change" claim is only true for defined bodies.
2. Deletion safety evidence for `_shared/cors.ts` is asserted, not shown. A plain-identifier grep is insufficient here: also confirm (a) no `import("../_shared/" + name)` / string-built specifier, (b) no `deno.json`/`import_map.json` alias or `supabase/config.toml` entry naming the file, (c) `_shared/native-cors.test.ts` doesn't read it by path — the PR says it reads "deployed function sources as text", but if its file list is a directory walk of `supabase/functions/**` with a fixed file count or a "no `Access-Control-Allow-Origin: *` anywhere" assertion, deleting the file changes what that test enumerates. No test file is touched in the diff, so this rests entirely on the claim. Otherwise the reasoning (per-function `getCorsHeaders` allowlists, unimported file is not bundled into any deployed function) is sound and the removal is correct.
3. Stale count in the verification table: "`deno check` on all 5 touched functions". The diff touches 4 functions (`ai`, `ai-stream`, `announcements`, `import-source`); the fifth path is the deleted `_shared/cors.ts`, which is not a function and cannot be `deno check`ed post-deletion. Fix the number or name the five paths.

NITS:
1. "`Response.json` is documented as adding a charset parameter on some platforms… That is Node/undici behavior" is inaccurate. Per WHATWG spec `Response.json` sets exactly `application/json`, and undici follows it. The Deno verification is still valid; the justification for running it is not. Harmless, but it's a factual claim in the PR body.
2. Minor precedence change: the old wrappers forced `Content-Type` last, overriding anything in `corsHeaders`; `Response.json` defers to a caller-supplied `content-type`. Currently unreachable since no `getCorsHeaders` returns one, but it's a real semantic difference not covered by "no observable change".
3. Removal of the unexported, unreferenced `type Source` in `ai/index.ts` is correctly justified — `deno check` on that function is sufficient proof. No orphans left behind.

VERDICT: approve — the transformations and both deletions are sound in substance, but land the undefined-body call-site check and the `_shared/cors.ts` grep evidence, and correct the "5 touched functions" count, before merge.

Response.json sets the JSON content type itself, so three hand-rolled
wrappers that stringified a body and spread in a Content-Type header can
delegate to it. Call sites are unchanged.

Two behavioural differences exist in principle, and neither is reachable
here:

- Response.json(undefined) throws a TypeError, where the old form produced
  an empty body. Both remaining wrappers are typed `unknown`, so this was
  worth checking: every one of the 12 call sites in import-source and the
  3 in announcements passes an object literal, never a possibly-undefined
  value.
- Response.json defers to a caller-supplied content-type, where the old form
  always overrode it. No getCorsHeaders in this repo returns one.

Verified under Deno 2.9.6 that the content type is exactly "application/json"
with no charset parameter, matching the old form byte for byte.

Also deletes two declarations with no references:

- _shared/cors.ts, a wildcard Access-Control-Allow-Origin header set with
  no importer, no import-map alias, and no config.toml entry. Every function
  defines its own getCorsHeaders origin allowlist instead, so this file was a
  misleading leftover. _shared/native-cors.test.ts is unaffected: it iterates
  a hardcoded list of function names and reads ../<name>/index.ts as text, so
  it never enumerates _shared or asserts on this file.
- the local Source type in ai/index.ts, never exported and never used.

Verified on main fe679fd, from a clean npm ci: docs:check, lint, typecheck,
956 unit tests, 121 edge tests, deno check on the 4 touched function
entrypoints, build, and 126 Chromium e2e tests pass.
@Programcode
Programcode force-pushed the chore/edge-function-stdlib-cleanup branch from 91353eb to 4836ada Compare September 6, 2026 20:49
@Programcode
Programcode merged commit 66ba697 into main Sep 7, 2026
7 checks passed
@Programcode
Programcode deleted the chore/edge-function-stdlib-cleanup branch September 7, 2026 09:44

This branch was successfully deployed

1 active deployment
Preview — 4836adaf Deployed Sep 6, 2026 by vercel[bot]
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