KEMBAR78
Implement RFC9457 compliant messaging by doddi · Pull Request #16199 · astral-sh/uv · GitHub
Skip to content

Conversation

doddi
Copy link
Contributor

@doddi doddi commented Oct 9, 2025

Summary

HTTP1.1 RFC 9112 - HTTP/1.1 section 4 defines the response status code to optionally include a text description (human readable) of the reason for the status code.

RFC9113 - HTTP/2 is the HTTP2 protocol standard and the response status only considers the status code and not the reason phrase, and as such important information can be lost in helping the client determine a route cause of a failure.

As per discussion on this PR the current feeling is that implementing the RFC9457 standard might be the preferred route. This PR makes those changes to aid the discussion which has also been moved to the PEP board

Test Plan

Pulling components that violate our policy over HTTP2 and without any RFC9457 implementation the following message is presented to the user:
image

With the RFC9457 standard implemented, below you can see the advantage in the extra context as to why the component has been blocked:
image

@zanieb
Copy link
Member

zanieb commented Oct 9, 2025

Cool!

cc @woodruffw

@zanieb zanieb requested a review from konstin October 9, 2025 13:43
@woodruffw woodruffw self-requested a review October 9, 2025 13:44
Copy link
Member

@woodruffw woodruffw left a comment

Choose a reason for hiding this comment

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

Thanks @doddi! I did a review pass mostly based on my read of the RFC; I believe @konstin will do one more for uv's idioms (in which case any of his feedback on idioms overrides mine) 🙂

// Try to read the response body and parse it as problem details
match response.bytes().await {
Ok(bytes) => ProblemDetails::from_json(&bytes).ok(),
Err(_) => None,
Copy link
Member

Choose a reason for hiding this comment

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

Thinking out loud: is ignoring a malformed details response the right thing here? I think it is, but maybe it makes sense to emit a warning? I could see it being useful to notify upstream servers that they're emitting a malformed payload.

Copy link
Member

Choose a reason for hiding this comment

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

I probably wouldn't add a "warning" since it's not something the user can fix, but it might be worth logging.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

.headers()
.get("content-type")
.and_then(|ct| ct.to_str().ok())
.map(|ct| ct.starts_with("application/problem+json"))
Copy link
Member

Choose a reason for hiding this comment

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

Should this be an equality check rather than starts_with? RFC 9457 says that problem detail responses are identified by exactly the application/problem+json content-type, so we probably shouldn't accept application/problem+json+abc or similar.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comment on lines 32 to 34
if !content_type.starts_with("application/problem+json") {
return None;
}
Copy link
Member

Choose a reason for hiding this comment

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

IMO it's okay to remove this check, since we do it at the callsite. We can document that a caller-upheld invariant of this API is that the content-type should be right.

(Alternatively we could keep this check, but remove it from the callsite. But that might be a little harder with the lifetimes/consumption.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comment on lines 49 to 52
/// Parse Problem Details from JSON bytes
pub fn from_json(bytes: &[u8]) -> Result<Self, serde_json::Error> {
serde_json::from_slice(bytes)
}
Copy link
Member

Choose a reason for hiding this comment

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

My 0.02c would be for removing this, and just calling serde_json::from_slice directly wherever we use it 🙂

Copy link
Contributor Author

Choose a reason for hiding this comment

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

pub extensions: std::collections::HashMap<String, serde_json::Value>,
}

/// Default problem type URI as per RFC 9457
Copy link
Member

Choose a reason for hiding this comment

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

Nitpick, nonblocking: serde defaults sometimes benefit from inlining:

Suggested change
/// Default problem type URI as per RFC 9457
/// Default problem type URI as per RFC 9457
#[inline]

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comment on lines 38 to 40
/// Additional problem-specific extension fields
#[serde(flatten)]
pub extensions: std::collections::HashMap<String, serde_json::Value>,
Copy link
Member

Choose a reason for hiding this comment

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

serde will ignore unknown fields by default, so my 0.02c would be for removing this until we actually need to process extension fields. It'll also make the parsing slightly faster (not that that matters much on the error path):

Suggested change
/// Additional problem-specific extension fields
#[serde(flatten)]
pub extensions: std::collections::HashMap<String, serde_json::Value>,

(The tests should confirm that this works 🙂)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comment on lines 56 to 59
if let Some(detail) = &self.detail {
detail.clone()
} else if let Some(title) = &self.title {
title.clone()
Copy link
Member

Choose a reason for hiding this comment

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

Should we render both of these, if present? I can imagine {title}: {detail} being useful.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

} else if let Some(title) = &self.title {
title.clone()
} else {
format!("HTTP error {}", self.status.unwrap_or(0))
Copy link
Member

Choose a reason for hiding this comment

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

I think we want to specialize here too -- if status isn't given we should probably take it from the parent request, or maybe we could say "unknown error (server provided no information)" and rely on the error layer above us rendering the status code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@konstin konstin added the enhancement New feature or improvement to existing functionality label Oct 13, 2025
Copy link
Member

@konstin konstin left a comment

Choose a reason for hiding this comment

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

Can you add a test that shows the error message in context? index_has_no_requires_python and the network.rs have reference structures.

}
_ => {
// If no detail, title, or status is provided, return a generic message
"An error occurred".to_string()
Copy link
Member

Choose a reason for hiding this comment

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

We should return None for this case and fall back to the default case in WrappedReqwestError instead of using our own string.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

if let Ok(bytes) = response.bytes().await {
serde_json::from_slice(&bytes).ok()
} else {
warn!("Failed to read response body for problem details");
Copy link
Member

@konstin konstin Oct 14, 2025

Choose a reason for hiding this comment

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

We should show the inner error in this warning (at least for serde), so it's clear why e.g. the data didn't match the schema.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

f.write_str("Could not connect, are you offline?")
} else if let Some(problem_details) = &self.problem_details {
// Show problem details if available
write!(f, "{}", problem_details.description())
Copy link
Member

@konstin konstin Oct 14, 2025

Choose a reason for hiding this comment

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

Here we need to add something like Server says: so it's clear that that's from the remote and not uv

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@konstin
Copy link
Member

konstin commented Oct 14, 2025

For the clippy failure, we can box the large variant.

@doddi
Copy link
Contributor Author

doddi commented Oct 16, 2025

Add an it test

936ff82

For the clippy failure, we can box the large variant.

7ed1074 although I see the CI still failing for clippy error on windows for a different area. Can someone help me understand this?

@konstin
Copy link
Member

konstin commented Oct 16, 2025

Some OS types are larger on Windows than on Unix, which can push the error type size over the clippy threshold just for Windows. You can run Windows clippy locally with cargo-xwin and cargo xwin clippy, but usually just Boxing the struct that grew in the change in an error enum and rerunning CI does the trick.

@doddi doddi force-pushed the RFC9457-problem-messages branch from 7ed1074 to b9175d5 Compare October 16, 2025 15:44
@konstin
Copy link
Member

konstin commented Oct 16, 2025

I fixed the linter errors, I have experience with them.

Copy link
Member

@konstin konstin left a comment

Choose a reason for hiding this comment

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

Thank you!

@konstin
Copy link
Member

konstin commented Oct 16, 2025

I've changed the boxing to a different location because is_transient_network_error relies on having specific error types in the error chains and it considers Boxed type different from the base type there.

@konstin konstin enabled auto-merge (squash) October 16, 2025 19:42
@doddi
Copy link
Contributor Author

doddi commented Oct 16, 2025

I've changed the boxing to a different location because is_transient_network_error relies on having specific error types in the error chains and it considers Boxed type different from the base type there.

Thank you @konstin, I was struggling to understand this error being new to the code base

@konstin konstin merged commit c12e8bb into astral-sh:main Oct 16, 2025
99 checks passed
@woodruffw
Copy link
Member

Thanks @doddi!

tmeijn pushed a commit to tmeijn/dotfiles that referenced this pull request Oct 22, 2025
This MR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [astral-sh/uv](https://github.com/astral-sh/uv) | patch | `0.9.3` -> `0.9.5` |

MR created with the help of [el-capitano/tools/renovate-bot](https://gitlab.com/el-capitano/tools/renovate-bot).

**Proposed changes to behavior should be submitted there as MRs.**

---

### Release Notes

<details>
<summary>astral-sh/uv (astral-sh/uv)</summary>

### [`v0.9.5`](https://github.com/astral-sh/uv/blob/HEAD/CHANGELOG.md#095)

[Compare Source](astral-sh/uv@0.9.4...0.9.5)

Released on 2025-10-21.

This release contains an upgrade to `astral-tokio-tar`, which addresses a vulnerability in tar extraction on malformed archives with mismatching size information between the ustar header and PAX extensions. While the `astral-tokio-tar` advisory has been graded as "high" due its potential broader impact, the *specific* impact to uv is **low** due to a lack of novel attacker capability. Specifically, uv only processes tar archives from source distributions, which already possess the capability for full arbitrary code execution by design, meaning that an attacker gains no additional capabilities through `astral-tokio-tar`.

Regardless, we take the hypothetical risk of parser differentials very seriously. Out of an abundance of caution, we have assigned this upgrade an advisory: <GHSA-w476-p2h3-79g9>

##### Security

- Upgrade `astral-tokio-tar` to 0.5.6 to address a parsing differential ([#&#8203;16387](astral-sh/uv#16387))

##### Enhancements

- Add required environment marker example to hint ([#&#8203;16244](astral-sh/uv#16244))
- Fix typo in MissingTopLevel warning ([#&#8203;16351](astral-sh/uv#16351))
- Improve 403 Forbidden error message to indicate package may not exist ([#&#8203;16353](astral-sh/uv#16353))
- Add a hint on `uv pip install` failure if the `--system` flag is used to select an externally managed interpreter ([#&#8203;16318](astral-sh/uv#16318))

##### Bug fixes

- Fix backtick escaping for PowerShell ([#&#8203;16307](astral-sh/uv#16307))

##### Documentation

- Document metadata consistency expectation ([#&#8203;15683](astral-sh/uv#15683))
- Remove outdated aarch64 musl note ([#&#8203;16385](astral-sh/uv#16385))

### [`v0.9.4`](https://github.com/astral-sh/uv/blob/HEAD/CHANGELOG.md#094)

[Compare Source](astral-sh/uv@0.9.3...0.9.4)

Released on 2025-10-17.

##### Enhancements

- Add CUDA 13.0 support ([#&#8203;16321](astral-sh/uv#16321))
- Add auto-detection for Intel GPU on Windows ([#&#8203;16280](astral-sh/uv#16280))
- Implement display of RFC 9457 HTTP error contexts ([#&#8203;16199](astral-sh/uv#16199))

##### Bug fixes

- Avoid obfuscating pyx tokens in `uv auth token` output ([#&#8203;16345](astral-sh/uv#16345))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever MR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this MR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this MR, check this box

---

This MR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNTEuMSIsInVwZGF0ZWRJblZlciI6IjQxLjE1Mi45IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJSZW5vdmF0ZSBCb3QiXX0=-->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or improvement to existing functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants