Skip to content

Show a one-line error for an invalid CLOUDINARY_URL - #122

Merged
const-cloudinary merged 4 commits into
masterfrom
fix/invalid-cloudinary-url-error
Sep 30, 2026
Merged

const-cloudinary merged 4 commits into
masterfrom
fix/invalid-cloudinary-url-error

Conversation

@TalLevAmi

Copy link
Copy Markdown
Contributor

With an invalid CLOUDINARY_URL (for example CLOUDINARY_URL=garbage), every cld command printed a full Python traceback that ended in ValueError: Invalid CLOUDINARY_URL scheme. The SDK reads the variable when it is imported, so the error occurs before the error handling in main().

Now the output is:

error: Invalid CLOUDINARY_URL scheme. Expecting to start with 'cloudinary://'. Fix or unset the CLOUDINARY_URL environment variable.

Brief Summary of Changes

  • cloudinary_cli/__init__.py catches ValueError from import cloudinary and calls sys.exit with the message. The exit code is 1.
  • The new test runs the CLI in a subprocess, because the error occurs at import time.

What does this PR address?

  • GitHub issue (Add reference - #XX)
  • Refactoring
  • New feature
  • Bug fix
  • Adds more tests

Are tests included?

  • Yes
  • No

Reviewer, please note:

  • The CLI still stops when CLOUDINARY_URL is invalid and you select a saved config with -C. This is the same as before, because the SDK cannot be imported with that value.

Checklist:

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I ran the full test suite before pushing the changes and all the tests pass.

Local test run: I ran the full suite on Python 3.8 with Click 8.1.8. The new tests pass, and they fail without the fix. 8 tests in test_cli_agent.py and test_cli_config_oauth.py fail with stderr not separately captured on this branch and on master too. These tests need Click 8.2, which needs Python 3.10 or later. CI must confirm the full result.

🤖 Generated with Claude Code

With `CLOUDINARY_URL=garbage`, every command printed a Python traceback.
The SDK reads the variable when it is imported, before `main()` can
handle errors.

Catch the `ValueError` from `import cloudinary` and exit with one line
that tells the user to fix or unset the variable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@TalLevAmi
TalLevAmi marked this pull request as ready for review September 26, 2026 16:15

@const-cloudinary const-cloudinary left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@TalLevAmi , this is a nice bug.
Unfortunately, the fix is not quite complete.
We have a few ways to configure CLI, using cld -C or cld -c. They take precedence over ENV variable, so we should not crash in this case, even if ENV variable is broken.
And this PR does not address CLOUDINARY_ACCOUNT_URL, which produces similar error.

…ary-url-error

# Conflicts:
#	test/test_cli.py
The SDK reads both variables on import and raises ValueError when one is
invalid. The CLI now imports the SDK without them and then loads each one
again. An invalid variable stays unset, so -c and -C can select a config.
Without -c or -C, the CLI shows a one-line error that names the variable.

@const-cloudinary const-cloudinary left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@TalLevAmi thanks, the direction is right: the SDK raises during import, so the variables have to be held back around import cloudinary. Two things decide whether the environment is used at all, and the current version doesn't account for them yet:

Background

  • The CLI has a saved default config. Users save named configs with cld config -n <name> <url> and pick one as the default with cld config -d <name>, or with --set-default when saving. The choice is stored as __default__ in ~/.cloudinary-cli/config.json (defaults.py:29). resolve_cli_config checks sources in this order: -c, then -C, then the saved default, then the environment (config_resolver.py:57, 63, 71, 81). With a default set, CLOUDINARY_URL is never read, so an invalid value in it shouldn't matter.
  • The SDK can be configured with separate variables, not only CLOUDINARY_URL. When CLOUDINARY_CLOUD_NAME is set and not empty, the SDK loads every CLOUDINARY_* variable except CLOUDINARY_URL (CLOUDINARY_API_KEY, CLOUDINARY_API_SECRET, and so on) and ignores CLOUDINARY_URL completely (pycloudinary cloudinary/__init__.py:205-218). So CLOUDINARY_URL=garbage together with CLOUDINARY_CLOUD_NAME=demo is a valid setup and should keep working. If CLOUDINARY_CLOUD_NAME is missing or empty, the SDK parses the URL, and it can fail even when other variables such as CLOUDINARY_API_KEY are set.

What this means for the PR

  1. Precedence. The check at config_resolver.py:50 runs before the saved default is looked at, so with a default set and a bad CLOUDINARY_URL every command exits. The error should only fire where resolve_cli_config falls back to the environment (config_resolver.py:81).
  2. config_optional commands. cld config -n <name> <url> is blocked too, and that's how a user would fix the problem. Look at how warn_if_unconfigured is handled (cli_group.py:47).
  3. CLOUDINARY_ACCOUNT_URL is separate. Only cld provisioning uses it, and -c, -C and the saved default don't replace it. The SDK always parses it, whatever else is set (pycloudinary cloudinary/provisioning/account_config.py:26-28). Right now a bad value stops admin ping, and with -c it's silently dropped. Keep a separate error for each variable and check this one in provisioning.
  4. Catching only ValueError misses cases (__init__.py:21). Try CLOUDINARY_URL='cloudinary://1:s@demo?a=1&a[b]=2', which raises a TypeError. A failed load can also leave a half-filled config.
  5. Deleting from os.environ (__init__.py:22). Instead of deleting the variable, consider wrapping _load_config_from_env on the SDK config classes once, after import. Then every later Config() call is safe, the environment stays as it was, and the resolver can ask for the error when it needs it.
  6. Structure. Right now this logic sits in cloudinary_cli/__init__.py. It might be easier to read and test in a small helper under utils/. The only constraint is that the helper can't import the SDK at the top, because it has to run before import cloudinary. cloudinary_cli/__init__.py would then just call it.
  7. Tests. The subprocess tests read the developer's real ~/.cloudinary-cli/config.json. Set CLOUDINARY_HOME to a temp directory, and add cases for the saved default, -C, CLOUDINARY_CLOUD_NAME set together with a bad URL, and the query-key URL.

Move the SDK import into the new helper `utils/env_config.py`. The helper
holds back CLOUDINARY_URL and CLOUDINARY_ACCOUNT_URL during the import.
After the import, it wraps `_load_config_from_env` of the SDK config
classes. It does not delete the variables from the environment.

- The wrapper catches ValueError and TypeError. A failed load keeps no
  values from the failed load.
- The CLOUDINARY_URL error shows only when `resolve_cli_config` falls back
  to the environment. A saved default, -c and -C override it, and the
  commands that work without a config (such as `config -n`) ignore it.
- The CLOUDINARY_ACCOUNT_URL error shows only in `provisioning`.
- The subprocess tests use a temporary CLOUDINARY_HOME.
@const-cloudinary
const-cloudinary merged commit fe5359a into master Sep 30, 2026
8 checks passed
@const-cloudinary
const-cloudinary deleted the fix/invalid-cloudinary-url-error branch September 30, 2026 21:39
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.

2 participants