Skip to content

Commit de4e310

Browse files
committed
fix: Fix error parsing in mod_http_oauth2
1 parent d80c3dd commit de4e310

File tree

16 files changed

+147
-60
lines changed

16 files changed

+147
-60
lines changed

api/crates/prosody-http/src/lib.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,26 +92,28 @@ pub mod error {
9292
/// ```
9393
#[derive(Debug, Deserialize, thiserror::Error)]
9494
#[error("{reason}", reason = error.text)]
95-
pub struct ProsodyHttpError<ExtraInfo = Option<DefaultExtraInfo>> {
95+
pub struct ProsodyHttpError<ExtraInfo = DefaultExtraInfo> {
9696
error: ProsodyHttpErrorDetails<ExtraInfo>,
9797
pub code: u16,
9898
}
9999

100100
impl<T> ProsodyHttpError<T> {
101101
#[inline]
102-
pub fn into_inner(self) -> T {
102+
pub fn into_inner(self) -> Option<T> {
103103
self.error.extra
104104
}
105105
}
106106

107107
/// See [`ProsodyHttpError`].
108108
#[derive(Debug, Deserialize)]
109109
pub struct ProsodyHttpErrorDetails<ExtraInfo> {
110-
pub source: Box<str>,
110+
#[serde(default)]
111+
pub source: Option<Box<str>>,
111112
pub text: Box<str>,
112113
pub condition: Box<str>,
113114
pub r#type: Box<str>,
114-
pub extra: ExtraInfo,
115+
#[serde(default = "Option::default")]
116+
pub extra: Option<ExtraInfo>,
115117
}
116118

117119
pub use ProsodyHttpErrorDefaultExtraInfo as DefaultExtraInfo;

api/crates/prosody-http/src/mod_http_oauth2.rs

Lines changed: 94 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -516,57 +516,105 @@ fn receive<Response: DeserializeOwned>(
516516
} else {
517517
// Read the reponse body, as `mod_http_oauth2` isn’t very
518518
// expressive with HTTP status codes.
519-
let error = response
519+
let json = response
520520
.body_mut()
521-
.read_json::<crate::Error<self::ApiError>>()
522-
.context("Could not decode Prosody OAuth 2.0 API error")?
523-
.into_inner();
524-
525-
match error.name.as_ref() {
526-
// Unauthorized.
527-
"not-authorized" | "expired_token" | "invalid_grant" | "login_required" => {
528-
tracing::debug!("{error}");
529-
Err(self::Error::Unauthorized(anyhow::Error::new(error)))
530-
}
531-
"invalid_request" if error.description.as_deref() == Some("invalid JID") => {
532-
tracing::debug!("{error}");
533-
Err(self::Error::Unauthorized(anyhow::Error::new(error)))
534-
}
521+
.read_json::<serde_json::Value>()
522+
.context("Prosody OAuth 2.0 API error is not valid JSON")?;
523+
524+
match serde_json::from_value::<self::ApiError>(json.clone()) {
525+
Ok(error) => match error.name.as_ref() {
526+
// Unauthorized.
527+
"expired_token" | "invalid_grant" | "login_required" => {
528+
tracing::debug!("{error}");
529+
Err(self::Error::Unauthorized(anyhow::Error::new(error)))
530+
}
531+
"invalid_request" if error.description.as_deref() == Some("invalid JID") => {
532+
tracing::debug!("{error}");
533+
Err(self::Error::Unauthorized(anyhow::Error::new(error)))
534+
}
535535

536-
// Forbidden.
537-
"forbidden" | "access_denied" => {
538-
tracing::warn!("{error}");
539-
Err(self::Error::Forbidden(anyhow::Error::new(error)))
540-
}
536+
// Forbidden.
537+
"access_denied" => {
538+
tracing::warn!("{error}");
539+
Err(self::Error::Forbidden(anyhow::Error::new(error)))
540+
}
541541

542-
// Internal errors.
543-
"internal-server-error"
544-
| "feature-not-implemented"
545-
| "invalid_client"
546-
| "invalid_client_metadata"
547-
| "invalid_redirect_uri"
548-
| "invalid_request"
549-
| "invalid_scope"
550-
| "temporarily_unavailable"
551-
| "unsupported_response_type" => {
552-
tracing::error!("{error}");
553-
Err(self::Error::Internal(anyhow::Error::new(error)))
554-
}
555-
"unauthorized_client" => {
556-
tracing::warn!(
557-
"OAuth 2.0 client unauthorized ({error}). \
558-
Make sure to register one before making calls."
559-
);
560-
Err(self::Error::Internal(anyhow::Error::new(error)))
561-
}
542+
// Internal errors.
543+
"invalid_client"
544+
| "invalid_client_metadata"
545+
| "invalid_redirect_uri"
546+
| "invalid_request"
547+
| "invalid_scope"
548+
| "temporarily_unavailable"
549+
| "unsupported_response_type" => {
550+
tracing::error!("{error}");
551+
Err(self::Error::Internal(anyhow::Error::new(error)))
552+
}
553+
"unauthorized_client" => {
554+
tracing::warn!(
555+
"OAuth 2.0 client unauthorized ({error}). \
556+
Make sure to register one before making calls."
557+
);
558+
Err(self::Error::Internal(anyhow::Error::new(error)))
559+
}
562560

563-
// Catch-all.
564-
_ => {
565-
tracing::error!("{error}");
566-
if cfg!(debug_assertions) {
567-
panic!("Unknown error")
561+
// Catch-all.
562+
_ => {
563+
tracing::error!("{error}");
564+
if cfg!(debug_assertions) {
565+
panic!("Unknown error")
566+
}
567+
Err(self::Error::Internal(anyhow::Error::new(error)))
568+
}
569+
},
570+
Err(_) => {
571+
// TODO: Factor this case so it’s shared between all mods.
572+
let error = serde_json::from_value::<crate::Error>(json)
573+
.context("Could not decode Prosody OAuth 2.0 API error")?;
574+
575+
match error.condition.as_ref() {
576+
// Unauthorized.
577+
"not-authorized" => {
578+
tracing::debug!("{error}");
579+
Err(self::Error::Unauthorized(anyhow::Error::new(error)))
580+
}
581+
582+
// Forbidden.
583+
"forbidden" => {
584+
tracing::warn!("{error}");
585+
Err(self::Error::Forbidden(anyhow::Error::new(error)))
586+
}
587+
588+
// Internal errors.
589+
"internal-server-error"
590+
| "feature-not-implemented"
591+
| "invalid_client"
592+
| "invalid_client_metadata"
593+
| "invalid_redirect_uri"
594+
| "invalid_request"
595+
| "invalid_scope"
596+
| "temporarily_unavailable"
597+
| "unsupported_response_type" => {
598+
tracing::error!("{error}");
599+
Err(self::Error::Internal(anyhow::Error::new(error)))
600+
}
601+
"unauthorized_client" => {
602+
tracing::warn!(
603+
"OAuth 2.0 client unauthorized ({error}). \
604+
Make sure to register one before making calls."
605+
);
606+
Err(self::Error::Internal(anyhow::Error::new(error)))
607+
}
608+
609+
// Catch-all.
610+
_ => {
611+
tracing::error!("{error}");
612+
if cfg!(debug_assertions) {
613+
panic!("Unknown error")
614+
}
615+
Err(self::Error::Internal(anyhow::Error::new(error)))
616+
}
568617
}
569-
Err(self::Error::Internal(anyhow::Error::new(error)))
570618
}
571619
}
572620
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# HG changeset patch
2+
# User Rémi Bardon <[email protected]>
3+
# Date 1762249606 -28800
4+
# Tue Nov 04 17:46:46 2025 +0800
5+
# Node ID 2d3c5aad5b534413bf84f8f41dfd0a13b48ee437
6+
# Parent c69edc9c19fff24752086dba7cfeb30f289b2165
7+
mod_http_oauth2: Coerce net.http errors more consistently
8+
9+
diff -r c69edc9c19ff -r 2d3c5aad5b53 mod_rest/mod_rest.lua
10+
--- a/mod_rest/mod_rest.lua Mon Nov 03 15:32:36 2025 +0800
11+
+++ b/mod_rest/mod_rest.lua Tue Nov 04 17:46:46 2025 +0800
12+
@@ -6,6 +6,7 @@
13+
14+
local encodings = require "util.encodings";
15+
local base64 = encodings.base64;
16+
+local code2err = require "net.http.errors".registry;
17+
local errors = require "util.error";
18+
local http = require "net.http";
19+
local id = require "util.id";
20+
@@ -532,8 +533,6 @@
21+
end
22+
end);
23+
24+
- local code2err = require "net.http.errors".registry;
25+
-
26+
local function handle_stanza(event)
27+
local stanza, origin = event.stanza, event.origin;
28+
local reply_allowed = stanza.attr.type ~= "error" and stanza.attr.type ~= "result";
29+
@@ -685,6 +684,11 @@
30+
module:hook_object_event(http_server, "http-error", function (event)
31+
local request, response = event.request, event.response;
32+
local response_as = decide_type(request and request.headers.accept or "", supported_errors);
33+
+
34+
+ if not event.error and code2err[event.code] then
35+
+ event.error = errors.new(event.code, nil, code2err);
36+
+ end
37+
+
38+
if response_as == "application/xmpp+xml" then
39+
if response then
40+
response.headers.content_type = "application/xmpp+xml";

plugins/community/.hg/dirstate

0 Bytes
Binary file not shown.
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,2 @@
1-
mod_http_admin_api: Accept `"application/json"` with parameters
2-
3-
Some HTTP clients set `"application/json; charset=utf-8"` for JSON by default.
4-
Instead of having to add client-side workarounds and cluttering call sites,
5-
we can just support it by not checking for an exact match.
1+
mod_http_oauth2: Coerce net.http errors more consistently
62

221 Bytes
Binary file not shown.
64 Bytes
Binary file not shown.
75 Bytes
Binary file not shown.
64 Bytes
Binary file not shown.
214 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)