Skip to content

Take the default answer at the downloader's retry prompt when stdin is closed - #3965

Closed
Thanatos9404 wants to merge 15 commits into
nltk:developfrom
Thanatos9404:fix-downloader-eof-noninteractive
Closed

Thanatos9404 wants to merge 15 commits into
nltk:developfrom
Thanatos9404:fix-downloader-eof-noninteractive

Conversation

@Thanatos9404

Copy link
Copy Markdown

Fixes #3167.
Refs #3144, which reports the same crash from a Heroku build. The issue map in #3883 lists #3144 as "#3167 plus #2162"; the RuntimeWarning from #2162 is a separate defect and is not touched here.

What and why

When a download fails and download() runs with halt_on_error=False, the downloader prints Error installing package. Retry? [n/y/e] and calls input(). python -m nltk.downloader <package> runs it that way. In the Docker build of #3167 and the Heroku build of #3144, stdin is closed, so input() raises EOFError (nltk/downloader.py:1614 on develop). The command then dies with a traceback instead of reporting the failed package.

How

EOFError from that input() is now taken as the answer an empty line gives, n. The downloader does not retry and carries on with the remaining packages, which is also what --quiet does. Answers typed at a terminal or piped in work as before. Only the case that crashed changes.

Behaviour change: exit status

With stdin closed, python -m nltk.downloader <package> now prints the error, skips the package and exits with status 0. Before, it crashed with EOFError and exit status 1. Status 0 after a failed download is what the command line already gives today when the user answers n or e, or passes -q or -e.

So a script that relied on the crash to stop, such as the Heroku buildpack in #3144 (its log ends with "Could not download NLTK Corpora: exit status 1"), will no longer fail at that point. It will carry on without the package.

If you want a failed download to make the command exit non-zero, I can do that as a follow-up. It would change the n, e, -q and -e cases too, so it is your call. For context: #1825 (2017–2019) asked to skip this prompt when no TTY is attached. A maintainer pointed to --exit-on-error (-e) and --quiet (-q), and the issue was closed in 2019 without a code change.

If you would rather have EOF mean e (stop and return False), it is a one-word change. The exit status would still be 0.

Tests

  • test_retry_prompt_at_eof_answers_no replays both reports offline. The index fetch fails with the reported error ([Errno 99] Cannot assign requested address for punkt in urlopen error [Errno 99] Cannot assign requested address> #3167, [Errno 104] Connection reset by peer for stopwords in using nltk.txt for downloading stopwords #3144), and stdin is at EOF. On develop, both cases fail with EOFError at nltk/downloader.py:1614.
  • test_retry_prompt_typed_answers_are_unchanged checks that answers typed or piped in behave as before: n and an empty line go on, e stops, y retries. It passes on develop and on this branch.

All 6 cases pass on Windows with Python 3.10, 3.13 and 3.14. The downloader test files (test_downloader.py, test_downloader_*.py and test_attack_downloader_*.py, with the network-only live_index test deselected) were also run with a guard that blocks every non-loopback connection and DNS lookup. On all three versions: 640 passed, 83 skipped, 3 failed. The 3 failures are the existing tests that download real data (test_downloader_using_existing_parent_download_dir, test_downloader_using_non_existing_parent_download_dir, test_downloader_redownload), whose lookup of raw.githubusercontent.com the guard blocks. develop fails the same 3 the same way. The skips are POSIX-only cases (permission bits, symlinks, dir_fd), archives that are not installed, and one test marked as needing the network. black, isort, ruff, pyupgrade and the repository's guard hooks pass on the changed files.

Related

#1825 (2017, closed 2019) reported the same prompt hanging in a pipeline and asked to skip it when no TTY is attached. This PR is narrower: it changes only the case where there is no input at all.

Transparency (per #3883)

  • Tool and model: Claude Code with Claude Opus 5.5 (Anthropic). I (Yashvardhan Thanvi) directed the work and authorized submitting it. The diff, the tests and this description were checked in a separate AI review pass. The tests were run locally by the agent on Windows (Python 3.10, 3.13 and 3.14). I'm responsible for the change and will follow up on review.
  • Prompt: the instructions the agent was given, for this PR and the one for Tunnel connection failed: 407 Proxy Authentification Required #3109:
Prompt

TARGETS (two separate fixes, two separate branches, two separate PR drafts) in nltk/nltk: #3167 — in non-interactive runs, the downloader's retry prompt (choice = input().strip() around nltk/downloader.py:1614 on develop) raises EOFError. #3109 — "407 Proxy Authentification Required": nltk.set_proxy(proxy, user=, password=) does not authenticate HTTPS CONNECT tunnels; nltk/util.py only installs ProxyBasicAuthHandler, which urllib does not apply to CONNECT. (Verify; consider embedding credentials in the proxy URL handed to ProxyHandler, which urllib uses for CONNECT, and URL-quoting them.) PROJECT NORMS — READ FIRST: maintainer issue #3883 sets rules for AI-assisted contributions (read the whole thread) … If the rules require a plan comment BEFORE a PR even for these, do NOT post anything — draft the comment text and report that requirement. HARD RULES: NO GitHub writes of any kind … Tests must be offline … PROCESS (for each issue): 1. Re-verify unclaimed … 2. Reproduce with a failing test in nltk's test layout/style … 3. Smallest correct fix in nltk's style; for #3167 decide sensible non-interactive behavior (e.g. treat EOF as the default/quit choice) consistent with the surrounding prompt code; for #3109 make sure credentials with special characters are quoted and HTTP (non-CONNECT) proxy auth still works. 4. Run the targeted tests and the repo's lint/format config … 5. Self-review as a skeptical maintainer; fix findings. 6. Commit on branches … 7. Write PR drafts … including the AI disclosure and audit trail #3883 requires.

(local paths redacted; "…" marks omitted boilerplate about local paths and tooling)

When a download fails and nobody can answer "Retry? [n/y/e]" because
stdin is closed, as in a Docker or Heroku build that runs
`python -m nltk.downloader <package>`, input() raised EOFError and the
downloader aborted with a traceback. Treat end of input like an empty
answer, "n": do not retry and carry on, which is also what the
downloader does with --quiet. Interactive answers are unchanged.

The regression test replays the failures reported in nltk#3167
(Errno 99) and nltk#3144 (Errno 104) offline, with stdin at EOF. A second
test checks that typed answers still behave as before: "n" or an empty
line goes on, "e" stops, and "y" retries.

Fixes nltk#3167. Refs nltk#3144 (the RuntimeWarning in that log is nltk#2162).

@alvations alvations 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.

Verdict. The change is correct, small and safe: with stdin at EOF the retry prompt now takes the answer an empty line gives (n), so python -m nltk.downloader <package> in a Docker or Heroku build reports the failed package and goes on instead of dying with EOFError. I reproduced the crash on the parent commit (77b6d7a, exit 1, EOFError at nltk/downloader.py:1614) and confirmed it gone on the head (9857b2b, exit 0, error reported), both with a mocked urlopen and against the live index. Two things before merge: the same "nobody can answer" condition with a missing stdin (0<&-, a service without a console) still crashes on this branch, and the test matrix has not run yet because the workflow is waiting for approval.

Findings

1. should fix (nltk/downloader.py:1615 to 1620). With fd 0 closed rather than at EOF, input() raises RuntimeError: lost sys.stdin, not EOFError, and the head still exits 1 with a traceback. Ran on the head: python3.13 -W ignore::RuntimeWarning -m nltk.downloader -d ~/scratch punkt 0<&- with urlopen patched to raise URLError(OSError(99, ...)); output ends RuntimeError: lost sys.stdin, exit status 1. This is the same case as the PR describes (no one at the prompt), only with Python's other way of saying so, so the handler should take it too. Python raises this one only from input() when sys.stdin is None, so catching it here is narrow.

                        try:
                            choice = input().strip()
                        except (EOFError, RuntimeError):
                            # Nobody can answer: stdin is at EOF or missing, as
                            # in a Docker or CI build (#3167). Take the answer
                            # an empty line gives, "n": do not retry.
                            choice = "n"

A test for it is one line next to the EOF one: monkeypatch.setattr("sys.stdin", None).

2. should fix, pre-existing and adjacent (nltk/downloader.py:1031 and 1622 to 1623). In the reporters' scenario the index itself fails to load, and incr_download yields ErrorMessage(None, ...) (line 1031). A person who then types y at the prompt hits msg.package.id on None and gets AttributeError: 'NoneType' object has no attribute 'id'. Ran on the head and on the parent with printf 'y\nn\n' piped in: both crash the same way, exit status 1. Not introduced by this PR, but the new typed-answer test deliberately steps around it (its comment: "The package itself fails (not the index), so y has a package to retry"), which leaves the one path the reporters would take untested. Since the y branch is inside the lines this PR edits, it should retry what was asked for when there is no package:

                        if choice in ["y", "Y"]:
                            # An index failure carries no package: retry the
                            # id that was asked for.
                            retry = msg.package.id if msg.package else info_or_id
                            if not self.download(
                                retry,

and the typed-answer test should gain an index-failure case for y (the urlopen patch the EOF test already uses, with "y\nn\n" as the answers, expecting two prompts and True). If you would rather keep this PR to the EOF fix, say so and it can go in a follow-up.

3. not a code finding, blocks merge (CI). gh pr checks 3965 shows only the labeler; the ci-workflow run 38258196396 for 9857b2b is action_required, so the matrix has not run. A maintainer has to approve the workflow for this first-time contribution. Locally on python3.13 (macOS) the PR's tests and the downloader suites are green, see the table.

4. nit (PR description, "Behaviour change: exit status"). The description is accurate: __main__ never calls sys.exit on a failed package (if not rv and options.halt_on_error: break, line 3910), so n, e, -q and -e all exit 0 today. Verified each on the head with a failing urlopen and stdin at /dev/null: -q exit 0, -e exit 0, typed e exit 0 (then the next package prompts again and takes EOF as n). No change requested; if the maintainers want a non-zero status after any failed package, that is the separate follow-up the description offers, and #1825 is the earlier thread for it.

What was verified

What Command or method Result
Branch state git fetch origin pull/3965/head, merge-base with origin/develop 1 commit on top of 77b6d7a, 0 behind, no conflicts
New tests on head python3.13 -m pytest nltk/test/unit/test_downloader.py -k retry_prompt 6 passed
New tests on parent same, with nltk/downloader.py from origin/develop 2 EOF cases fail with EOFError at line 1614, 4 typed-answer cases pass
CLI crash on parent runpy.run_module("nltk.downloader") with nltk.pathsec.urlopen raising URLError(OSError(99, ...)), stdin </dev/null, packages punkt stopwords traceback, EOFError, exit 1
CLI on head, same as above both errors printed, two prompts, exit 0
CLI on head and parent, live index python3.13 -m nltk.downloader -d <scratch> zzz_not_a_package </dev/null (the STR of #1825) parent: EOFError, exit 1; head: "not found in index", exit 0
fd 0 closed on head same CLI with 0<&- RuntimeError: lost sys.stdin, exit 1 (finding 1)
y after an index failure, head and parent same CLI with printf 'y\nn\n' piped in AttributeError on msg.package.id, exit 1 (finding 2)
Typed e, -q, -e on head same CLI, stdin </dev/null or piped each exit 0, matching the description
Downloader suites, network allowed pytest nltk/test/unit/test_downloader*.py nltk/test/unit/test_attack_downloader*.py (12 files) 724 passed, 1 skipped, 0 failed (5 min 32 s)
Downloader suites, network blocked same, with a plugin that refuses every non-loopback connect() and DNS lookup, -k "not live_index" 718 passed, 2 skipped, 4 failed: the 3 tests that fetch real data (the same 3 the description names) and test_concurrent_downloads_cooperate, whose multiprocessing Unix socket the guard also refused (a guard artifact, not a defect)
Security probes python3.13 -m nltk.test.unit.security_probes 65 probes, 65 FIXED
Hooks python3.13 -m pre_commit run --files AUTHORS.md nltk/downloader.py nltk/test/unit/test_downloader.py all passed (black, isort, ruff, pyupgrade, and the sandbox, print, regex, pickle, spawn, eval, csv and json guards)
Chokepoints grep -n urlopen nltk/downloader.py urlopen is nltk.pathsec.urlopen (line 186); the PR adds no open, load, save, spawn or print call
Style rules grep of the diff for double hyphens; comment block length; commit trailer none; 3 lines; no attribution trailer
CI gh pr checks 3965, gh api repos/nltk/nltk/actions/runs?head_sha=9857b2bad labeler passed; ci-workflow 38258196396 action_required (finding 3)
Cross references #3167, #3144, #1825, #2162, the #3883 issue map #3144 is listed there as "#3167 plus #2162"; this PR fixes the #3167 half, as the description says

Transparency, per #3883.

  • Tool and model: Claude Code with Claude Fable 5.1, operated and reviewed by the maintainer.
  • Prompt (the maintainer's instructions, verbatim): "Launch an agent to review the #3965 with fable model and post the reply on the PR"
  • Audit trail:
    1. Read the PR (1 commit, 3 files, +75/-2), its description, every comment and review (none yet), issues #3167, #3144, #1825, #2162 and the #3883 issue map entries for #3167 and #3144.
    2. Fetched the head into a detached worktree and a second worktree at the parent commit; python3.13; data roots read-only behind an empty scratch root (it stayed empty).
    3. Ran the 6 new test cases on the head (6 passed) and against the parent's downloader.py (2 failed with EOFError, 4 passed).
    4. Reproduced the crash at the command line on the parent and showed it gone on the head, with a mocked urlopen and against the live index; probed the 0<&-, y after index failure, typed e, -q and -e shapes and recorded each exit status.
    5. Ran the 12 downloader test files twice: with the network allowed (724 passed, 1 skipped, 0 failed) and under a loopback-only socket guard (718 passed, 4 failed, each explained by the guard).
    6. Ran the 65 security probes (65 FIXED) and the pre-commit hooks on the 3 changed files (all passed).
    7. Checked the workflow runs for the head SHA: the matrix is action_required and has not run.

@alvations

Copy link
Copy Markdown
Contributor

Thanks for the proposal, thanks for following #3883 and as much as possible put effort to xhigh if you're using an agent to help you. Please address the comments above.

A process started with fd 0 closed, or a service without a console, has
sys.stdin set to None, and input() then raises RuntimeError ("lost
sys.stdin") rather than EOFError. It is the same situation as nltk#3167,
nobody can answer, so the prompt takes "n" for that too. input() raises
RuntimeError only for a missing stdin or stdout, so the catch stays
narrow. The test drives the real download() with sys.stdin None.
In the reports behind nltk#3167 and nltk#3144 it is the index fetch that fails,
and incr_download then yields ErrorMessage(None, ...); for a list
request it yields the id string. Answering "y" at the prompt then
crashed on msg.package.id with AttributeError. The retry now takes the
Package's id when there is one, else the id or request that failed.

The retried attempt also reports to print_error_to, where the first
attempt reported, instead of to sys.stderr.
A proxy's HTML error page, an empty body, a duplicate attribute, or a
package or collection element missing an attribute its constructor
needs made _update_index raise ElementTree.ParseError or TypeError.
Neither is caught by the (OSError, ValueError) handler around the
lookup, so download() ended in a traceback, and a non-interactive run
such as the Docker build of nltk#3167 died on it. Both are now a ValueError
with the sanitised reason, reported like a refused URL and raised only
under raise_on_error.

The index is also assigned only after every entry parsed, so a refused
index is not cached as fresh: the same Downloader refetches on the next
request and installs once the server serves a good index. The test
drives each shape through a real local server and checks the error
line, the empty download directory and the refetch.
Attack-suite cases for the prompt against the local server: with stdin
at EOF or absent, a collection with one bad package installs the others
and reports the bad one once; "e" stops before the next package; "y"
refetches the failed package once and goes on; "y" after an unusable
index refetches the index. Each is judged by what is on disk afterwards
and by the captured lines staying clean.
ProxyHandler adds the credentials of a proxy URL, such as
HTTPS_PROXY=http://user:password@proxy:3128, to each request it routes
through that proxy as an ordinary Proxy-Authorization header, and
HTTPRedirectHandler copies ordinary headers onto the redirected request.
When that request is not proxied (its host is in NO_PROXY or the system
bypass list), the credentials went to its host.

Add _ProxyCredentialsRedirectHandler, which drops the header from the
redirected request, and make _ValidatingRedirectHandler a subclass of
it: pathsec.urlopen validates the new URL first, then drops the header.
ProxyHandler adds it again when the redirected request is proxied.

The SSRF attack harness gets cases that run pathsec.urlopen against
stub servers on 127.0.0.1, with the credentials in HTTP_PROXY and
HTTPS_PROXY, for each redirect code: a redirect to a NO_PROXY host must
not receive the header, and a redirect the proxy carries must still
authenticate.

This comes first because the fix for nltk#3109 makes nltk.set_proxy put its
user and password in the proxy URL too.

Refs nltk#3109.
nltk.set_proxy(proxy, user=..., password=...) only installed
ProxyBasicAuthHandler, which answers a 407 to a plain HTTP request. An
HTTPS request goes through a CONNECT tunnel that http.client opens
before any handler sees a response, so the proxy got no credentials and
nltk.download failed with "Tunnel connection failed: 407 Proxy
Authentication Required".

Put the credentials in the proxy URL given to ProxyHandler,
percent-encoded so that an "@", ":" or "/" in them stays data. urllib
sends them as Proxy-Authorization with every request it routes through
the proxy, and urllib.request.AbstractHTTPHandler.do_open moves that
header onto the CONNECT and keeps it from the origin server.
pathsec.urlopen already copies the proxy URL from the installed opener,
so the downloader picks them up. The basic and digest handlers stay for
plain HTTP.

The scheme and host come from urllib.request._parse_proxy, the parser
ProxyHandler itself uses, so the credentials go to the host the
requests go to, also when the proxy URL holds another URL in its path,
query or fragment. Credentials already in the URL are replaced, and a
proxy without a scheme still works.

ProxyHandler adds the credentials as an ordinary header, which urllib's
redirect handler copies onto a redirected request. So the opener that
set_proxy installs gets pathsec's _ProxyCredentialsRedirectHandler from
the previous commit: a redirect to a host that bypasses the proxy
(NO_PROXY) must not receive the credentials, also through a plain
urllib.request.urlopen, and ProxyHandler adds them again when the
redirected request is proxied.

password=None is treated like the default empty password. The
(user, password) tuple that web/data.rst passed as the second argument
is still accepted; a user or password that is not exactly a str now
raises TypeError instead of being sent in its str() form. Also fix the
web/data.rst example to pass user= and password=.

The SSRF attack harness gets the set_proxy cases: the redirect cases
of the previous commit, the CONNECT of an HTTPS request (with the
credentials from set_proxy or from HTTPS_PROXY), hostile credential
strings, and proxy URLs with another URL after the host.

Fixes nltk#3109.
@Thanatos9404

Copy link
Copy Markdown
Author

Thanks @alvations for the review and for pushing the follow-up commits (missing stdin, y after an index failure, reporting an index the downloader cannot use, and the collection test); they cover both findings. Nothing further from my side, and I am happy to adjust if anything else comes up.

(Drafted with Claude Code / Claude Opus 5.5 and posted on my behalf, per #3883.)

@alvations

alvations commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Run a review using your agents and see what it catches.

Read #3883 carefully and try to solve the issues as a cluster not individual band-aids. Thank you in advance!

…er branch

nltk#3109 and nltk#3167 are the two code defects in the download failures
cluster of nltk#3883, so they are reviewed and fixed together here.
Under a proxy, pathsec.urlopen left the pinning handlers out because they
could not open a CONNECT tunnel. The operator's opt in vouches for the
proxy, but urllib still makes some connections itself: to a host in
NO_PROXY or the system bypass list, to a redirect target that leaves the
proxy, and to the proxy a caller's Request names. Those went out by host
name after validate_network_url had resolved it once, so a name that
validated as public could rebind to loopback before the connection.

The pinning handlers are now always installed. They leave a connection to
urllib only when ProxyHandler routed the request to one of the configured
proxies: a plain HTTP request to it, or the CONNECT of a tunnel through
it. The proxy hosts are read with urllib.request._parse_proxy, the parser
ProxyHandler uses, and the environment proxies are read once and handed
to the ProxyHandler that uses them.

The SSRF harness gets section 21: a rebinding name behind a redirect off
the proxy (credentials from the environment or from set_proxy, 302 and
307, http and https), the same name as the first hop under set_proxy,
and a Request that names its own proxy. It also pins what
_ProxyCredentialsRedirectHandler is still for, a redirect to a second
proxy, with a teeth case that puts urllib's own redirect handling back
and sees the first proxy's credentials on the second proxy's CONNECT.

test_env_proxy_opt_in_skips_pinning_handlers asserted the old design, no
pinning handlers under an opted in proxy. It is replaced by a test that
the environment's ProxyHandler keeps its proxies and that both pinning
handlers trust exactly that proxy.

Refs nltk#3109.
set_proxy percent-encodes the user name and password into the proxy URL,
but ProxyHandler unquotes them again and sends "user:password" in Basic
form. A ":" in the user name therefore moves the split: for the user
"[email protected]:8080/" the proxy reads the user "[email protected]" and a
password that starts with "8080/". The hostile credential cases compared
the header string, which hid this. RFC 7617 also allows no control
character in either part.

set_proxy now raises ValueError for a ":" in the user name or a control
character (C0 or DEL) in the credentials, before it replaces the
installed opener. The message does not repeat the credentials. A ":" in
the password is still fine.

The two harness cases that hit this, the colon in the user name and the
CR LF in the password, move to a test that they are refused and that the
proxy receives nothing, with a NUL case next to them. Their places in the
"reach the proxy as data" cases go to a user name with "@" and "/" and to
a password that reads as another proxy.

Refs nltk#3109.
The retry prompt was the only one of the downloader's eight input() calls
that survived a closed or missing stdin. python -m nltk.downloader with no
package, or nltk.download() with no argument, opens the text shell when
there is no display, as in a Docker or CI build, and each of its prompts
still ended in EOFError, or RuntimeError for a lost sys.stdin (nltk#3167).

Taking an empty line at EOF everywhere would not do: the main menu and
the config menu ask again on an empty line, so they would ask forever.
All eight prompts now go through _answer(prompt, at_eof). At EOF it gives
the answer an empty line gives, except "q" at the main menu and "m" in
the config menu. So the retry prompt goes on as before, the listing pages
on to the end, and every other prompt is left. The prompt goes through
sanitize_terminal like any other text the downloader writes.

The download harness gets section 15: the shell driven against a real
local server with stdin at EOF after each sequence a user could type, and
with no stdin at all. Its stdin fails the test instead of hanging if a
prompt keeps asking after the end. A test runs python -m nltk.downloader
in a child process with stdin at EOF and the text shell forced.

Refs nltk#3167, nltk#3144.
@Thanatos9404

Copy link
Copy Markdown
Author

Thanks @alvations. I reran the review on the whole branch, your four commits included, and started from #3883 this time.

In the #3883 map the download failures cluster (C1) has two code defects, #3167 and #3109, and #3144 is listed as #3167 plus #2162. So I am treating this PR and #3970 as one piece of work: I merged the #3970 branch and current develop into this branch and added the commits below on top. Nothing was rebased. If you would rather keep two PRs, say so and I will split it again.

What the review caught on the downloader side:

  1. The retry prompt was the only one of the downloader's eight input() calls that survived a closed or missing stdin. python -m nltk.downloader with no package, or nltk.download() with no argument, opens the text shell when there is no display, and every prompt there still ended in EOFError or RuntimeError: lost sys.stdin. Taking an empty line at EOF everywhere would not work, because the main menu and the config menu ask again on an empty line and would loop forever. All eight prompts now go through one helper, _answer(prompt, at_eof). At EOF it gives the empty line answer, except q at the main menu and m in the config menu. The retry prompt behaves exactly as with your commits. (c4b4c14)
    Runtime warning when using nltk.downloader from CLI #2162, the other half of using nltk.txt for downloading stopwords #3144 (runpy warning on every python -m nltk.downloader because import nltk imports nltk.downloader), is not in this PR. Loading the downloader lazily also moves the moment the default download directory is read from nltk.data.path, so it deserves its own PR where that change can be judged by itself.

The proxy side, including your thread on _ProxyCredentialsRedirectHandler, is answered on #3970.

Tests: in the five test files this branch touches, 29 tests fail on the merged head without these commits and pass with them. On Windows with Python 3.14 the five files give 617 passed and 31 skipped; the only 2 failures are symlink cases that need a Windows privilege this machine lacks, and they fail the same way without these commits. The security probes give the same verdicts with and without the commits (48 FIXED, 14 STATIC, 4 ERROR from the same symlink privilege), with none VULNERABLE. The shell cases run against a real local server, and the command line cases run a real child python -m nltk.downloader with stdin at EOF.

One question from the description is still open. With stdin closed, a failed package now makes the command exit 0, as n, e, -q and -e do today, while the Heroku buildpack in #3144 relied on the crash's exit 1 to fail the build. Should python -m nltk.downloader exit 1 whenever a requested package was not installed? That changes the n, e, -q and -e cases too, so I have left it until you say.

Drafted with Claude Code, posted on my behalf.

@alvations

alvations commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

AI;DR so what’s the conclusion on the initial exploit claim that the enforce is not working? Should we patch it? Or are the tests more tests on benign and not malicious since the enforce is doing what it should.

This PR has since moved away from the original patch for the enforce flag to some older ticket proxy issues that usually comes from universities or schools firewalls blocking proxies, so it’s best we don’t try to find ways of getting around those settings.

And on heroku deployment did you really try it there? Or just mock tests? I see that actual Linux instance deployment with enforce true working as expected.

The changes you made on the http proxies seem to unfortunately open new threat vectors to the downloader. If you don’t mind, do a human check on actual pentester playbook manually and work through it. Otherwise we might have to wait for another team member to verify your claims before proceeding with this and similar PRs. We want to avoid any possible new vectors intended the library, intended or unintended.

——

If this PR is still on the original enforce exploit, please do help and see if you can run Fable and on xhigh to validate your findings, esp. when we’re unable to replicate the claimed exploit. See #3981

To agent, put the related PRs together first and then once we get pass them as a coherent review, we split them up again before full matrix run. Independent of nltk, create a fork and then run the full-matrix tests first before trying to finalize your additional tests and patch. But first make sure if the proxy is even a relevant issue beyond users organization rules. Do this, deploy the windows and Linux and macOS instance using your human users GitHub action to actually verify the proxy issues and if the GitHub actions doesn’t give any errors without all the proxy related issues that an organization might put i.e. plain production deployment scenarios then it might not be a real problem to solve. If so, then remove all the code from this PR and make sure you recheck the claims and rework this PR.

This reverts commit 066afdc. The proxy changes are being taken out of this PR so that it only carries the downloader prompt fix; the proxy question stays with nltk#3970.
… a proxy"

This reverts commit 0af531e. The proxy changes are being taken out of this PR so that it only carries the downloader prompt fix; the proxy question stays with nltk#3970.
…er cluster branch"

This reverts merge commit f760989. The proxy changes are being taken out of this PR so that it only carries the downloader prompt fix; the proxy question stays with nltk#3970.
@Thanatos9404

Copy link
Copy Markdown
Author

Sorry for the length last time. Short answers:

Drafted with Claude Code, posted on my behalf.

@alvations

alvations commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

I give up… I don’t understand the PR already… @Thanatos9404 please rework your PR and instruct the agent clearly what you want to achieve, the context has probably drifted. I’ll leave it to other NLTK members to answer your PRs or close it.

The delta from the develop is now doing something entirely different and just doing sanitize terminal effectively. The agent context must have totally drifted.

Suggest you close this and the other PRs you’ve opened, please don’t just spam agents to just look at issue and send patches and follow up with little to no human supervision or specific goals.

@alvations alvations closed this Oct 19, 2026
@alvations alvations reopened this Oct 19, 2026
@Thanatos9404

Copy link
Copy Markdown
Author

Thanks for the time you spent on this, and sorry it became hard to follow. Closing this and #3970.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

urlopen error [Errno 99] Cannot assign requested address>

2 participants