Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 4 additions & 3 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4380,7 +4380,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 @@ -4394,9 +4395,9 @@ 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 simple index URL!).
///
/// Note that this typically differs from the index URL.
/// Note that there are typically different URLs for index access ("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 @@ -573,18 +577,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 @@ -597,10 +615,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 @@ -7186,9 +7186,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 simple 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 (&quot;simple&quot;) and index upload.</p>

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

Expand Down