Skip to content

fix(api): treat empty OPENAI_API_KEY/BASE_URL as unset (#4083) - #4438

Open
SomSamantray wants to merge 4 commits into
firecrawl:mainfrom
SomSamantray:fix/4083-empty-openai-env
Open

fix(api): treat empty OPENAI_API_KEY/BASE_URL as unset (#4083)#4438
SomSamantray wants to merge 4 commits into
firecrawl:mainfrom
SomSamantray:fix/4083-empty-openai-env

Conversation

@SomSamantray

@SomSamantray SomSamantray commented Aug 28, 2026

Copy link
Copy Markdown

Summary

Self-hosted deployments that pass OPENAI_API_KEY / OPENAI_BASE_URL through docker-compose ${VAR} passthrough receive empty strings when the variables are unset in .env. The config schema accepted "", so createOpenAI received a literal empty apiKey/baseURL, and formats: ["json"] requests failed with Failed to parse URL from /responses — returning success: true with no json field while still billing credits.

Fixes #4083.

What changed

  • emptyStringAsUndefined keeps its exact-"" semantics. A new emptyOrWhitespaceStringAsUndefined helper also coerces whitespace-only strings, and is used only where a whitespace-only value is never meaningful: OPENAI_API_KEY, OPENAI_BASE_URL, and the FIREBRAIN_TRACKS_* pair (which already had inline trim behavior).
  • Credential and URL fields that must fail closed at boot (FIREBILL_URL, FIREBILL_SECRET, KEYLESS_CONVERSION_HMAC_SECRET, MCP_DELEGATED_CREDENTIAL_SECRET, etc.) are untouched — a whitespace-only FIREBILL_URL still fails validation at startup instead of silently disabling billing routing.
  • Config-layer coercion alone was not enough: @ai-sdk/openai re-reads process.env at request time via loadApiKey / loadOptionalSetting when the createOpenAI option is undefined. So config.ts now deletes OPENAI_API_KEY / OPENAI_BASE_URL from process.env after parsing when they were coerced to undefined, and warns at boot. The SDK then raises the intended missing-key error and falls back to the default base URL.
  • The regression test moved into src/__tests__/snips/v2/ so CI's pnpm harness pnpm test:snips lane executes it, and now also asserts process.env deletion, FIREBRAIN behavior, and whitespace-padded passthrough.

Verification

  • vitest run src/__tests__/snips/v2/config.test.ts — 8/8 pass (empty, whitespace-only, real, unset, process.env deletion, FIREBRAIN, padded passthrough).
  • Empirically verified against the installed @ai-sdk/openai 3.0.71: before, env "" produced request URL "/responses" (the ERR_INVALID_URL failure); after, the resolved URL uses the default https://api.openai.com/v1 base.
  • vitest run src/search/highlights.test.ts src/lib/threat-protection/config.test.ts — pass (45 tests total).
  • tsc --noEmit — no errors in changed files (remaining repo-wide errors are pre-existing @mendable/firecrawl-rs native-module resolution, built by CI).

Impact

  • No migration, no deployment changes, no SDK/API surface changes.
  • Operators with a set-but-empty OpenAI key now see a boot warning instead of silent per-request failures that still bill credits.
  • Behavior for all other emptyStringAsUndefined users is unchanged (exact-"" semantics preserved).

Summary by cubic

Fixes self-hosted deployments where docker-compose ${VAR} passthrough exposes empty OPENAI_API_KEY and OPENAI_BASE_URL strings. Empty and whitespace-only values are now treated as unset, so @ai-sdk/openai raises the intended missing-key error and falls back to the default base URL instead of failing per-request and still billing credits.

Bug Fixes

  • Added emptyOrWhitespaceStringAsUndefined for OPENAI_* and FIREBRAIN_TRACKS_* vars; real values are trimmed so padded keys and URLs reach the SDK normalized.
  • Deletes coerced-to-undefined OpenAI keys from process.env after config parse, since the SDK re-reads env at request time. Boot warnings differentiate: an empty key makes the OpenAI provider unavailable, while an empty base URL falls back to the default.
  • Other fail-closed fields (FIREBILL_*, KEYLESS_*, MCP_DELEGATED_*) keep exact-"" semantics, so whitespace-only values still fail validation at startup.
  • Regression tests moved to src/__tests__/snips/v2/ so CI's snips lane runs them; coverage includes empty, whitespace-only, real, unset, trimming, and process.env deletion cases.

Written for commit 0fdb590. Summary will update on new commits.

Review in cubic

Docker Compose passes unset .env vars through as empty strings. The
config schema kept them, so createOpenAI received apiKey:"" and
baseURL:"", producing "Failed to parse URL from /responses" and a
silent json-format failure that still billed credits.

Apply the existing emptyStringAsUndefined helper (now also trimming
whitespace-only values) to OPENAI_API_KEY and OPENAI_BASE_URL so the
intended missing-key error and default base URL apply.

Adds a regression test covering empty, whitespace-only, real, and
unset values.
…wl#4083)

Review found the config-layer coercion alone was a no-op: @ai-sdk/openai
re-reads process.env at request time via loadApiKey/loadOptionalSetting
when the createOpenAI option is undefined, so docker-compose's '' was
resurrected and the 'Failed to parse URL from /responses' failure
persisted.

- Delete OPENAI_API_KEY/OPENAI_BASE_URL from process.env after parse when
  coerced to undefined, so the SDK env fallback sees unset and the
  intended missing-key error / default base URL apply. Warn at boot when
  a set-but-empty key is dropped (console.warn: importing the winston
  logger here would create a config <-> logger cycle).
- Keep emptyStringAsUndefined at exact-'' semantics and add
  emptyOrWhitespaceStringAsUndefined for the fields that need it
  (OPENAI_*, FIREBRAIN_TRACKS_*), so whitespace-only FIREBILL_URL and
  secrets still fail closed at boot instead of silently disabling billing.
- Move the regression test into src/__tests__/snips/v2/ so CI's
  'pnpm harness pnpm test:snips' lane actually executes it, and assert
  process.env deletion, FIREBRAIN behavior, and padded passthrough.

@cubic-dev-ai cubic-dev-ai Bot left a comment

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.

All reported issues were addressed across 2 files

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread apps/api/src/config.ts Outdated
Comment thread apps/api/src/__tests__/snips/v2/config.test.ts
Comment thread apps/api/src/config.ts Outdated
- Trim OPENAI_API_KEY/OPENAI_BASE_URL values so whitespace-padded keys
  and URLs are normalized before reaching createOpenAI (a padded key
  would otherwise auth with trailing spaces and 401 with no warning).
- Differentiate boot warnings: an empty API key makes the OpenAI
  provider unavailable; an empty base URL merely falls back to the
  default https://api.openai.com/v1 (the provider still works when a
  key is present).
- Restore FIREBRAIN_TRACKS_URL/API_KEY in the config test afterAll so
  the env-mutation test does not leak into other tests.
@SomSamantray

Copy link
Copy Markdown
Author

All 3 review findings addressed in 0fdb590:

  1. P2 (config.ts:600) — Boot warnings now differentiate: an empty OPENAI_API_KEY makes the provider unavailable; an empty OPENAI_BASE_URL only falls back to the default https://api.openai.com/v1 (provider still works when a key is present).
  2. P3 (config.test.ts)FIREBRAIN_TRACKS_URL / FIREBRAIN_TRACKS_API_KEY are now captured and restored in afterAll, matching the audio-routing pattern; no env leak.
  3. P3 (config.ts)OPENAI_API_KEY / OPENAI_BASE_URL are now z.string().trim()-normalized, so whitespace-padded values are trimmed before reaching createOpenAI (verified: generic-ai passes the trimmed config value; the SDK uses it directly).

Verified: 8/8 config tests pass (incl. trimmed-padded assertions), 45 tests total, typecheck clean for changed files.

@SomSamantray

Copy link
Copy Markdown
Author

CI status

All code-side checks are green and all review feedback is resolved. The PR is blocked only on maintainer-side approvals that an external contributor cannot perform:

  1. Server Test Suite (test-server.yml) and Audit NPM Packages — triggered but waiting for a maintainer to approve the workflow run for this fork PR (action_required). Approving requires admin rights (verified 403).
  2. Vercel deployment — "Authorization required to deploy"; a maintainer must click Approve and deploy in Vercel for the fork PR.
  3. Human review — no maintainer review yet.

Resolved this round:

  • cubic AI review: all 3 findings addressed (differentiated boot warnings, FIREBRAIN env restore in tests, trim-normalized OpenAI values); cubic confirms "All reported issues were addressed".
  • Contributor trust: ✅ · Superagent Security Scan: ✅ · Vercel Agent Review: neutral/skipped.

Once a maintainer approves the workflow runs, the real test suite will execute against 0fdb590.

@cubic-dev-ai cubic-dev-ai Bot left a comment

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.

0 issues found across 2 files (changes from recent commits).

Re-trigger cubic

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.

Empty OPENAI_API_KEY / OPENAI_BASE_URL bypass zod handling: json scrape format fails silently while still consuming credits

1 participant