Skip to content

Commit

Permalink
Hint at wrong endpoint in publish (astral-sh#7872)
Browse files Browse the repository at this point in the history
Improve hints when using the simple index URL instead of the upload URL
in `uv publish`. This is the most common confusion when publishing, so
we give it some extra care and put it more centrally in the CLI help.

Fixes astral-sh#7860

---------

Co-authored-by: Zanie Blue <[email protected]>
  • Loading branch information
konstin and zanieb authored Oct 8, 2024
1 parent 9e98055 commit 282fab5
Show file tree
Hide file tree
Showing 3 changed files with 34 additions and 14 deletions.
8 changes: 5 additions & 3 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4449,7 +4449,8 @@ pub struct DisplayTreeArgs {
#[arg(long)]
pub no_dedupe: bool,

/// Show the reverse dependencies for the given package. This flag will invert the tree and display the packages that depend on the given package.
/// Show the reverse dependencies for the given package. This flag will invert the tree and
/// display the packages that depend on the given package.
#[arg(long, alias = "reverse")]
pub invert: bool,
}
Expand All @@ -4463,9 +4464,10 @@ pub struct PublishArgs {
#[arg(default_value = "dist/*")]
pub files: Vec<String>,

/// The URL of the upload endpoint.
/// The URL of the upload endpoint (not the index URL).
///
/// Note that this typically differs from the index URL.
/// Note that there are typically different URLs for index access (e.g., `https:://.../simple`)
/// and index upload.
///
/// Defaults to PyPI's publish URL (<https://upload.pypi.org/legacy/>).
///
Expand Down
36 changes: 27 additions & 9 deletions crates/uv-publish/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,12 @@ pub enum PublishSendError {
ReqwestMiddleware(#[from] reqwest_middleware::Error),
#[error("Upload failed with status {0}")]
StatusNoBody(StatusCode, #[source] reqwest::Error),
#[error("Upload failed with status code {0}: {1}")]
#[error("Upload failed with status code {0}. Server says: {1}")]
Status(StatusCode, String),
#[error("POST requests are not supported by the endpoint, are you using the simple index URL instead of the upload URL?")]
MethodNotAllowedNoBody,
#[error("POST requests are not supported by the endpoint, are you using the simple index URL instead of the upload URL? Server says: {0}")]
MethodNotAllowed(String),
/// The registry returned a "403 Forbidden".
#[error("Permission denied (status code {0}): {1}")]
PermissionDenied(StatusCode, String),
Expand Down Expand Up @@ -577,18 +581,32 @@ async fn handle_response(registry: &Url, response: Response) -> Result<bool, Pub
.get(reqwest::header::CONTENT_TYPE)
.and_then(|content_type| content_type.to_str().ok())
.map(ToString::to_string);
let upload_error = response
.bytes()
.await
.map_err(|err| PublishSendError::StatusNoBody(status_code, err))?;
let upload_error = response.bytes().await.map_err(|err| {
if status_code == StatusCode::METHOD_NOT_ALLOWED {
PublishSendError::MethodNotAllowedNoBody
} else {
PublishSendError::StatusNoBody(status_code, err)
}
})?;
let upload_error = String::from_utf8_lossy(&upload_error);

trace!("Response content for non-200 for {registry}: {upload_error}");
trace!("Response content for non-200 response for {registry}: {upload_error}");

debug!("Upload error response: {upload_error}");

// That's most likely the simple index URL, not the upload URL.
if status_code == StatusCode::METHOD_NOT_ALLOWED {
return Err(PublishSendError::MethodNotAllowed(
PublishSendError::extract_error_message(
upload_error.to_string(),
content_type.as_deref(),
),
));
}

// Detect existing file errors the way twine does.
// https://github.com/pypa/twine/blob/c512bbf166ac38239e58545a39155285f8747a7b/twine/commands/upload.py#L34-L72
if status_code == 403 {
if status_code == StatusCode::FORBIDDEN {
if upload_error.contains("overwrite artifact") {
// Artifactory (https://jfrog.com/artifactory/)
Ok(false)
Expand All @@ -601,10 +619,10 @@ async fn handle_response(registry: &Url, response: Response) -> Result<bool, Pub
),
))
}
} else if status_code == 409 {
} else if status_code == StatusCode::CONFLICT {
// conflict, pypiserver (https://pypi.org/project/pypiserver)
Ok(false)
} else if status_code == 400
} else if status_code == StatusCode::BAD_REQUEST
&& (upload_error.contains("updating asset") || upload_error.contains("already been taken"))
{
// Nexus Repository OSS (https://www.sonatype.com/nexus-repository-oss)
Expand Down
4 changes: 2 additions & 2 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -7214,9 +7214,9 @@ uv publish [OPTIONS] [FILES]...

<p>This setting has no effect when used in the <code>uv pip</code> interface.</p>

</dd><dt><code>--publish-url</code> <i>publish-url</i></dt><dd><p>The URL of the upload endpoint.</p>
</dd><dt><code>--publish-url</code> <i>publish-url</i></dt><dd><p>The URL of the upload endpoint (not the index URL).</p>

<p>Note that this typically differs from the index URL.</p>
<p>Note that there are typically different URLs for index access (e.g., <code>https:://.../simple</code>) and index upload.</p>

<p>Defaults to PyPI&#8217;s publish URL (&lt;https://upload.pypi.org/legacy/&gt;).</p>

Expand Down

0 comments on commit 282fab5

Please sign in to comment.