fix(auth): map 401/403 challenges on the SSE GET stream (#1152)
`post_message_with_max_sse_event_size` turns a 401 or 403 carrying a
`WWW-Authenticate` header into `StreamableHttpError::AuthRequired` /
`InsufficientScope`. `get_stream_with_max_sse_event_size`, in the same file,
handles only 405 and then falls through to `error_for_status()?`, so the same
challenge becomes an opaque `StreamableHttpError::Client`.
That matters because `AuthClient::get_stream` routes through
`call_reacting_to_challenges`, whose whole purpose is to catch `AuthRequired`,
run `try_refresh_or_reauth()`, and retry once. It never sees that variant from
this path, so an expired token on the standalone SSE stream is never refreshed —
the stream just fails, while the identical expiry on `post_message` recovers
silently.
Copies the two blocks verbatim from `post_message` in the same file.
`unix_socket.rs` already does the same thing in its own `get_stream`.
Adds four tests against an axum mock server: a 401 with a challenge maps to
`AuthRequired`, a 403 maps to `InsufficientScope` with the scope extracted, a
401 *without* a challenge is still not `AuthRequired`, and 405 keeps reporting
`ServerDoesNotSupportSse`. The first two fail before this change with
`Err(Client(reqwest::Error { kind: Status(401, None) }))`; the last two pass
either way and exist to catch an over-broad fix.
This commit is contained in:
parent
f8e63824d3
commit
c345078867
1 changed files with 136 additions and 0 deletions
|
|
@ -94,6 +94,37 @@ impl StreamableHttpClient for reqwest::Client {
|
|||
if response.status() == reqwest::StatusCode::METHOD_NOT_ALLOWED {
|
||||
return Err(StreamableHttpError::ServerDoesNotSupportSse);
|
||||
}
|
||||
if response.status() == reqwest::StatusCode::UNAUTHORIZED
|
||||
&& let Some(header) = response.headers().get(WWW_AUTHENTICATE)
|
||||
{
|
||||
let header = header
|
||||
.to_str()
|
||||
.map_err(|_| {
|
||||
StreamableHttpError::UnexpectedServerResponse(Cow::from(
|
||||
"invalid www-authenticate header value",
|
||||
))
|
||||
})?
|
||||
.to_string();
|
||||
return Err(StreamableHttpError::AuthRequired(AuthRequiredError {
|
||||
www_authenticate_header: header,
|
||||
}));
|
||||
}
|
||||
if response.status() == reqwest::StatusCode::FORBIDDEN
|
||||
&& let Some(header) = response.headers().get(WWW_AUTHENTICATE)
|
||||
{
|
||||
let header_str = header.to_str().map_err(|_| {
|
||||
StreamableHttpError::UnexpectedServerResponse(Cow::from(
|
||||
"invalid www-authenticate header value",
|
||||
))
|
||||
})?;
|
||||
let scope = extract_scope_from_header(header_str);
|
||||
return Err(StreamableHttpError::InsufficientScope(
|
||||
InsufficientScopeError {
|
||||
www_authenticate_header: header_str.to_string(),
|
||||
required_scope: scope,
|
||||
},
|
||||
));
|
||||
}
|
||||
let response = response.error_for_status()?;
|
||||
match response.headers().get(reqwest::header::CONTENT_TYPE) {
|
||||
Some(ct) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue