Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions fingerprint_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@

import requests

import env_config

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger("fingerprint_client")

Expand Down Expand Up @@ -325,8 +327,28 @@ def playwright_init_script(fp: dict) -> str:

def main() -> int:
p = argparse.ArgumentParser(description="Fetch a browser fingerprint from 2captcha")
p.add_argument("--key", default=os.environ.get("TWOCAPTCHA_KEY"),
help="API key. Defaults to $TWOCAPTCHA_KEY (safer than argv).")
# Reads `.env` as well as the exported variable, through the family's own
# loader. It used to read `os.environ` alone, which meant a key put in
# `.env` exactly as the README and .env.example instruct worked for every
# engine and failed HERE with "No API key" — a documented mechanism not
# applied on one path, which is the shape of half the defects §16 lists.
#
# `.env` has to be LOADED before it can be read: `env_value` looks at
# os.environ, and `load_env` is what fills that from the file. Calling it
# here rather than relying on an engine having called it is the whole
# point — this is a standalone entry point.
#
# And it goes through `env_value` rather than `os.environ.get` so the
# PLACEHOLDER rule applies. Measured both ways: with
# TWOCAPTCHA_KEY=your_2captcha_api_key_here exported, `os.environ.get`
# sends the placeholder to the API and the run reports "Fingerprint API
# rejected the key (401) — note this is a separate subscription", which
# sends the reader off to check a subscription when they simply never
# filled the key in. `env_value` says so instead, by name.
env_config.load_env()
p.add_argument("--key", default=env_config.env_value("TWOCAPTCHA_KEY"),
help="API key. Defaults to TWOCAPTCHA_KEY from the "
"environment or .env (safer than argv).")
# Measured against the live API on 2026-09-09, because the example this
Comment on lines +347 to 352

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

env_config.env_value("TWOCAPTCHA_KEY") is passed directly as default=, so it's evaluated eagerly the instant add_argument() runs — before parse_args() ever sees argv. Since env_value() (env_config.py#L144-L154) logs a warning whenever the value matches a .env.example placeholder, this means:

  • A bare --help invocation logs the placeholder warning even though no key resolution is actually happening.
  • A user who has the placeholder in .env but correctly passes --key sk_real... on the command line still gets the spurious "is still set to the placeholder... treating it as unset" warning, even though their explicit flag overrides the default and the placeholder is never actually used.

The sibling scraper_api_client.py avoids this exact problem: it keeps default=os.environ.get("TWOCAPTCHA_KEY") (side-effect-free) and instead calls env_config.apply(args, keys={"TWOCAPTCHA_KEY": "key", ...}) after parse_args() (scraper_api_client.py#L294-L298) — apply() only calls env_value() if the destination is still falsy (env_config.py#L166-L173), so an explicit --key short-circuits before the placeholder check ever runs.

Suggest following that same shape here: keep the default as a plain env read and defer the env_value()/placeholder check to after parse_args(), only applying it if args.key is still unset.

# file used to carry ("Windows,Chrome,Desktop") returns 400 every time:
# accepted -> Windows, Microsoft Windows, Android
Expand Down
41 changes: 41 additions & 0 deletions smoke_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2130,6 +2130,47 @@ class _NoProxyArgs:
and _m.group(1) in ("Windows", "Microsoft Windows",
"Android"))

# HOW fingerprint_client FINDS ITS KEY, which is a separate defect from
# which tags it sends. `--key` defaulted to `os.environ.get(
# "TWOCAPTCHA_KEY")` alone, so a key put in `.env` — exactly as the
# README and .env.example instruct — worked for every engine and failed
# HERE with "No API key". A documented mechanism not applied on one path,
# which is the shape of half the defects the family notes list. Found on
# a sibling repo's first live --fingerprint run and confirmed present
# here.
import fingerprint_client as _fpc
import env_config as _envc
_fp_src = inspect.getsource(_fpc.main)
ok &= check("fingerprint_client loads .env itself, rather than hoping an "
"engine did", "env_config.load_env()" in _fp_src)
ok &= check("...and reads the key through the family's loader",
'env_config.env_value("TWOCAPTCHA_KEY")' in _fp_src)
# Through `env_value` and NOT `os.environ.get`, because only the former
# applies the placeholder rule. Measured both ways with
# TWOCAPTCHA_KEY=your_2captcha_api_key_here exported: os.environ.get
# sends the placeholder to the API and the run reports "Fingerprint API
# rejected the key (401) — note this is a separate subscription", which
# sends the reader to check a subscription they never needed.
ok &= check("...not straight from os.environ, which skips the "
"placeholder rule",
'os.environ.get("TWOCAPTCHA_KEY")' not in _fp_src)
_saved = os.environ.get("TWOCAPTCHA_KEY")
try:
os.environ["TWOCAPTCHA_KEY"] = "your_2captcha_api_key_here"
_read_back = _envc.env_value("TWOCAPTCHA_KEY")
finally:
if _saved is None:
os.environ.pop("TWOCAPTCHA_KEY", None)
else:
os.environ["TWOCAPTCHA_KEY"] = _saved
ok &= check("a placeholder still reads as unset on this path",
_read_back is None)
# The default must never reach `--help`: argparse prints one only when
# the help string asks for it, so this is one substring away from
# printing a live credential to anyone who types --help.
ok &= check("the --key help text does not interpolate its default",
"%(default)s" not in _fp_src)

print()
if _skips:
print(f"{len(_skips)} group(s) of checks SKIPPED — an optional engine "
Expand Down
Loading