Skip to content

Add --exclude-newer-last-modified to support non-PyPI indexes via Last-Modified HTTP header#18823

Open
dzmitry-kankalovich wants to merge 2 commits intoastral-sh:mainfrom
dzmitry-kankalovich:feature/exclude-newer-last-modified
Open

Add --exclude-newer-last-modified to support non-PyPI indexes via Last-Modified HTTP header#18823
dzmitry-kankalovich wants to merge 2 commits intoastral-sh:mainfrom
dzmitry-kankalovich:feature/exclude-newer-last-modified

Conversation

@dzmitry-kankalovich
Copy link
Copy Markdown

@dzmitry-kankalovich dzmitry-kankalovich commented Apr 2, 2026

Summary

A follow-up on the discussion in #12449

Because of the recent surge of the supply chain attack, my company decided to enable guardrails against adding recently released dependencies - so basically leverage the exclude-newer feature of uv.

During our testing of the feature this worked like charm with PyPI, however JFrog's Artifactory - the intra-company index that we are using - seemingly lacks support of PEP 691 and PEP 700 entirely, which essentially prevents us from using exclude-newer. It just crunches through every version of the first dependency it grabs, fails to understand what was the upload date and fails:

warning: aiohttp-3.13.3-cp39-cp39-musllinux_1_2_x86_64.whl is missing an upload date, but user provided: 2024-01-01T00:00:00Z
warning: aiohttp-3.13.3-cp39-cp39-win32.whl is missing an upload date, but user provided: 2024-01-01T00:00:00Z
warning: aiohttp-3.13.3-cp39-cp39-win_amd64.whl is missing an upload date, but user provided: 2024-01-01T00:00:00Z

So far my research shows that PyPI is pretty much the only index that currently supports PEP 691 / 700.

On the other hand, on example with Artifactory at least, it seems that we can rely on Last-Modified HTTP header value to substitute absent upload-time.

The testing in my specific case on example of setuptools:

│ Source      │ Last-Modified header on file  │  PEP 691 JSON upload-time   │ HTML data-upload-time │
│ PyPI        │ Sun, 09 Mar 2025 03:18:46 GMT │ 2025-03-09T03:18:44.129789Z │ not present           │
│ Artifactory │ Sun, 09 Mar 2025 03:18:46 GMT │ 406 Not Acceptable          │ not present           │

This made me think of #12449 (comment) - basically implement an optional fallback for the situation when the index is not PEP 691/700 compliant, and basically rely on Last-Modified HTTP header value as the next best thing.

Unfortunate disadvantage here is that you need to do HEAD request for every version candidate during resolution phase.

And so I've tried to minimize the overhead by doing these requests only when resolver actually selects the candidates, and only when the exclude-newer set and exclude-newer-last-modified switched on.

The idea is that the code path is simply not active for the PyPI users, but kicks in for the poor souls that need to use something else, and I think the overhead of additional requests is a fine trade-off between having security this feature at all or not having it, and thus acceptable.

DISCLAIMER: the code below was written entirely by Claude Opus 4.6. I have steered it conceptually throughout code creation, but I haven't worked with Rust before, and am not familiar with uv codebase in general. I did my best to review the code below using my overall engineering experience, but IDK if I am doing here anything Rust-specific or uv-specific dumb.

Configuration:

  • CLI: --exclude-newer-last-modified / --no-exclude-newer-last-modified (wasn't sure about --no- flag, but rest of uv seem to follow this pattern)
  • Environment: UV_EXCLUDE_NEWER_LAST_MODIFIED
  • Config file: exclude-newer-last-modified = true (in [tool.uv], [tool.uv.pip])

Test Plan

Tested with the dev build in my specific case.

(Unfortunately couldn't find any public Artifactory that mirrors PyPI, so did the testing with our privately deployed one)

No flag:

$ uv/target/debug/uv lock --index-url https://[REDACTED]/artifactory/api/pypi/[REDACTED]/simple --exclude-newer 2024-01-01T00:00:00Z
warning: aiohttp-3.13.3-cp39-cp39-win_amd64.whl is missing an upload date, but user provided: 2024-01-01T00:00:00Z
  × No solution found when resolving dependencies:
  ╰─▶ Because aiobotocore==2.25.0 has no publish time and app depends on aiobotocore==2.25.0, we can conclude that app's requirements are unsatisfiable.
      And because your workspace requires app, we can conclude that your workspace's requirements are unsatisfiable.

With flag, but non-compliant dependencies (notice it mentions the exclude criteria Last-Modified header):

uv/target/debug/uv lock --index-url https://[REDACTED]/artifactory/api/pypi/[REDACTED]/simple --exclude-newer 2024-01-01T00:00:00Z --exclude-newer-last-modified
Resolving despite existing lockfile due to removal of exclude newer span
  × No solution found when resolving dependencies:
  ╰─▶ Because aiobotocore==2.25.0 was excluded by --exclude-newer (via Last-Modified header) and app depends on aiobotocore==2.25.0, we can conclude that app's requirements are unsatisfiable.
      And because your workspace requires app, we can conclude that your workspace's requirements are unsatisfiable.

With flag, compliant deps, happy path:

uv/target/debug/uv lock --index-url https://[REDACTED]/api/pypi/[REDACTED]/simple --exclude-newer "7 days" --exclude-newer-last-modified
Resolved 553 packages in 2m 56s

Integration tests

Added

…allback

When `--exclude-newer` is set and a package file lacks the PEP 691 `upload-time`
field (common with non-PyPI indexes like Artifactory), this opt-in flag makes a
HEAD request to the file URL and uses the `Last-Modified` header as a fallback
timestamp. HEAD requests are only issued for files the resolver actually selects
as candidates (typically 1-2 per package), not for all files on the index page.

Closes astral-sh#12449

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@zanieb
Copy link
Copy Markdown
Member

zanieb commented Apr 2, 2026

Interesting idea. I'll need to look closer to know if it makes sense.

On a high-level, if we support something like this, we might want to do it via an annotation on the index?

e.g.,

[[tool.uv.index]]
...
exclude-newer-source = "last-modified | upload-time"

- Fix `unparseable` → `unparsable` typo in doc comment
- Add `exclude-newer-last-modified` to unknown-field error snapshot
- Add `exclude_newer_last_modified` field to all show_settings snapshots

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@dzmitry-kankalovich
Copy link
Copy Markdown
Author

Interesting idea. I'll need to look closer to know if it makes sense.

On a high-level, if we support something like this, we might want to do it via an annotation on the index?

e.g.,

[[tool.uv.index]]
...
exclude-newer-source = "last-modified | upload-time"

yes, there was another idea floating around to add per-index support of exclude-newer (which is not the case right now I believe). I just didn't want to conflate two changes here:

  • last-modified fallback
  • per-index exclude-newer support

as for the the configuration - flag-enabled fallback or explicit exclude-newer-source = "last-modified | upload-time" - I like your suggestion in general. I think the latter is potentially more extendable... but on the other hand it would be maintenance hell to support should these possible values go more than 2. I think index should either be PEP 691 / 700 compliant or at the very least provide an adequate Last-Modified header value. If it does something else then it's definitely not uv problem.

Just let me know your preference here so I can rework (if needed).

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.

3 participants