Skip to content

Commit dff6b84

Browse files
authored
Merge pull request #134 from chrivers/chrivers/apiv1-error-handling-rework
Rework api V1 error handling. Previously, any non-successful api operation on the legacy api would yield incorrect error responses. Yes, even the errors were in error! This did not help application compatibility, but the problem is now fixed consistently.
2 parents b3aeb29 + 829e996 commit dff6b84

6 files changed

Lines changed: 169 additions & 39 deletions

File tree

crates/hue/src/error.rs

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,6 @@ pub enum HueError {
3838
#[error("Failed to decode Hue Zigbee Update: Unknown flags {0:04x}")]
3939
HueZigbeeUnknownFlags(u16),
4040

41-
#[error("State changes not supported for: {0:?}")]
42-
UpdateUnsupported(RType),
43-
4441
#[error("Resource {0} not found")]
4542
NotFound(uuid::Uuid),
4643

@@ -57,4 +54,59 @@ pub enum HueError {
5754
Undiffable,
5855
}
5956

57+
/// Error types for Hue Bridge v1 API
58+
#[derive(Error, Debug, Clone, Copy)]
59+
pub enum HueApiV1Error {
60+
/// Type 1
61+
#[error("Unauthorized")]
62+
UnauthorizedUser = 1,
63+
64+
/// Type 2
65+
#[error("Body contains invalid JSON")]
66+
BodyContainsInvalidJson = 2,
67+
68+
/// Type 3
69+
#[error("Resource not found")]
70+
ResourceNotfound = 3,
71+
72+
/// Type 4
73+
#[error("Method not available for resource")]
74+
MethodNotAvailableForResource = 4,
75+
76+
/// Type 5
77+
#[error("Missing parameters in body")]
78+
MissingParametersInBody = 5,
79+
80+
/// Type 6
81+
#[error("Parameter not available")]
82+
ParameterNotAvailable = 6,
83+
84+
/// Type 7
85+
#[error("Invalid value for parameter")]
86+
InvalidValueForParameter = 7,
87+
88+
/// Type 8
89+
#[error("Parameter not modifiable")]
90+
ParameterNotModifiable = 8,
91+
92+
/// Type 11
93+
#[error("Too many items in list")]
94+
TooManyItemsInList = 11,
95+
96+
/// Type 12
97+
#[error("Portal connection is required")]
98+
PortalConnectionIsRequired = 12,
99+
100+
/// Type 901
101+
#[error("Internal bridge error")]
102+
BridgeInternalError = 901,
103+
}
104+
105+
impl HueApiV1Error {
106+
#[must_use]
107+
pub const fn error_code(&self) -> u32 {
108+
*self as u32
109+
}
110+
}
111+
60112
pub type HueResult<T> = Result<T, HueError>;

crates/hue/src/legacy_api.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ pub struct NewUser {
117117
#[derive(Debug, Serialize, Deserialize)]
118118
pub struct NewUserReply {
119119
pub username: String,
120+
#[serde(skip_serializing_if = "Option::is_none")]
120121
pub clientkey: Option<String>,
121122
}
122123

src/error.rs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ use tokio::task::JoinError;
88

99
use bifrost_api::backend::BackendRequest;
1010
use hue::event::EventBlock;
11-
use hue::legacy_api::ApiResourceType;
1211
use svc::error::SvcError;
1312

1413
#[derive(Error, Debug)]
@@ -120,10 +119,6 @@ pub enum ApiError {
120119
#[error("Unexpected z2m message: {0:?}")]
121120
UnexpectedZ2mReply(tokio_tungstenite::tungstenite::Message),
122121

123-
/* hue api v1 errors */
124-
#[error("Cannot create resources of type: {0:?}")]
125-
V1CreateUnsupported(ApiResourceType),
126-
127122
/* hue api v2 errors */
128123
#[error("Failed to get firmware version reply from update server")]
129124
NoUpdateInformation,

src/routes/api.rs

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ use std::collections::HashMap;
22

33
use axum::Router;
44
use axum::extract::{Path, State};
5-
use axum::response::IntoResponse;
65
use axum::routing::{get, post, put};
76
use bytes::Bytes;
87
use chrono::Utc;
98
use log::{info, warn};
9+
use serde::Serialize;
1010
use serde_json::{Value, json};
1111
use tokio::sync::MutexGuard;
1212
use uuid::Uuid;
@@ -17,7 +17,7 @@ use hue::api::{
1717
GroupedLightUpdate, Light, LightUpdate, RType, Resource, ResourceLink, Room, Scene,
1818
SceneActive, SceneStatus, SceneUpdate, V1Reply,
1919
};
20-
use hue::error::{HueError, HueResult};
20+
use hue::error::{HueApiV1Error, HueError, HueResult};
2121
use hue::legacy_api::{
2222
ApiGroup, ApiGroupActionUpdate, ApiGroupUpdate2, ApiLight, ApiLightStateUpdate,
2323
ApiResourceType, ApiScene, ApiSceneAppData, ApiSceneType, ApiSceneVersion, ApiSensor,
@@ -28,13 +28,14 @@ use crate::error::{ApiError, ApiResult};
2828
use crate::resource::Resources;
2929
use crate::routes::auth::STANDARD_CLIENT_KEY;
3030
use crate::routes::extractor::Json;
31+
use crate::routes::{ApiV1Error, ApiV1Result};
3132
use crate::server::appstate::AppState;
3233

33-
async fn get_api_config(State(state): State<AppState>) -> impl IntoResponse {
34+
async fn get_api_config(State(state): State<AppState>) -> Json<impl Serialize> {
3435
Json(state.api_short_config().await)
3536
}
3637

37-
async fn post_api(bytes: Bytes) -> ApiResult<impl IntoResponse> {
38+
async fn post_api(bytes: Bytes) -> ApiV1Result<Json<impl Serialize>> {
3839
info!("post: {bytes:?}");
3940
let json: NewUser = serde_json::from_slice(&bytes)?;
4041

@@ -105,7 +106,7 @@ fn get_groups(res: &MutexGuard<Resources>, group_0: bool) -> ApiResult<HashMap<S
105106
Ok(rooms)
106107
}
107108

108-
pub fn get_scene(res: &Resources, owner: String, scene: &Scene) -> ApiResult<ApiScene> {
109+
pub fn get_scene(res: &Resources, owner: String, scene: &Scene) -> ApiV1Result<ApiScene> {
109110
let lights = scene
110111
.actions
111112
.iter()
@@ -121,7 +122,7 @@ pub fn get_scene(res: &Resources, owner: String, scene: &Scene) -> ApiResult<Api
121122
ApiLightStateUpdate::from(sae.action.clone()),
122123
))
123124
})
124-
.collect::<ApiResult<_>>()?;
125+
.collect::<ApiV1Result<_>>()?;
125126

126127
let room_id = res.get_id_v1_index(scene.group.rid)?;
127128

@@ -146,7 +147,7 @@ pub fn get_scene(res: &Resources, owner: String, scene: &Scene) -> ApiResult<Api
146147
})
147148
}
148149

149-
fn get_scenes(owner: &str, res: &MutexGuard<Resources>) -> ApiResult<HashMap<String, ApiScene>> {
150+
fn get_scenes(owner: &str, res: &MutexGuard<Resources>) -> ApiV1Result<HashMap<String, ApiScene>> {
150151
let mut scenes = HashMap::new();
151152

152153
for rr in res.get_resources_by_type(RType::Scene) {
@@ -165,7 +166,7 @@ fn get_scenes(owner: &str, res: &MutexGuard<Resources>) -> ApiResult<HashMap<Str
165166
async fn get_api_user(
166167
state: State<AppState>,
167168
Path(username): Path<String>,
168-
) -> ApiResult<impl IntoResponse> {
169+
) -> ApiV1Result<Json<impl Serialize>> {
169170
let lock = state.res.lock().await;
170171

171172
Ok(Json(ApiUserConfig {
@@ -183,7 +184,7 @@ async fn get_api_user(
183184
async fn get_api_user_resource(
184185
State(state): State<AppState>,
185186
Path((username, artype)): Path<(String, ApiResourceType)>,
186-
) -> ApiResult<Json<Value>> {
187+
) -> ApiV1Result<Json<Value>> {
187188
let lock = &state.res.lock().await;
188189
match artype {
189190
ApiResourceType::Config => Ok(Json(json!(state.api_config(username).await?))),
@@ -201,26 +202,26 @@ async fn get_api_user_resource(
201202
async fn post_api_user_resource(
202203
Path((_username, resource)): Path<(String, ApiResourceType)>,
203204
Json(req): Json<Value>,
204-
) -> ApiResult<Json<Value>> {
205+
) -> ApiV1Result<Json<Value>> {
205206
warn!("POST v1 user resource unsupported");
206207
warn!("Request: {req:?}");
207-
Err(ApiError::V1CreateUnsupported(resource))
208+
Err(ApiV1Error::V1CreateUnsupported(resource))
208209
}
209210

210211
async fn put_api_user_resource(
211212
Path((_username, _resource)): Path<(String, String)>,
212213
Json(req): Json<Value>,
213-
) -> impl IntoResponse {
214+
) -> ApiV1Result<Json<impl Serialize>> {
214215
warn!("PUT v1 user resource {req:?}");
215216
//Json(format!("user {username} resource {resource}"))
216-
Json(vec![HueApiResult::Success(req)])
217+
Ok(Json(vec![HueApiResult::Success(req)]))
217218
}
218219

219220
#[allow(clippy::significant_drop_tightening)]
220221
async fn get_api_user_resource_id(
221222
State(state): State<AppState>,
222223
Path((username, resource, id)): Path<(String, ApiResourceType, u32)>,
223-
) -> ApiResult<impl IntoResponse> {
224+
) -> ApiV1Result<Json<impl Serialize>> {
224225
log::debug!("GET v1 username={username} resource={resource:?} id={id}");
225226
let result = match resource {
226227
ApiResourceType::Lights => {
@@ -260,7 +261,7 @@ async fn put_api_user_resource_id(
260261
State(state): State<AppState>,
261262
Path((username, artype, id)): Path<(String, ApiResourceType, u32)>,
262263
Json(req): Json<Value>,
263-
) -> ApiResult<impl IntoResponse> {
264+
) -> ApiV1Result<Json<Value>> {
264265
log::debug!("PUT v1 username={username} resource={artype:?} id={id}");
265266
log::debug!("JSON: {req:?}");
266267
match artype {
@@ -292,15 +293,15 @@ async fn put_api_user_resource_id(
292293
| ApiResourceType::Scenes
293294
| ApiResourceType::Schedules
294295
| ApiResourceType::Sensors
295-
| ApiResourceType::Capabilities => Err(ApiError::V1CreateUnsupported(artype)),
296+
| ApiResourceType::Capabilities => Err(ApiV1Error::V1CreateUnsupported(artype)),
296297
}
297298
}
298299

299300
async fn put_api_user_resource_id_path(
300301
State(state): State<AppState>,
301302
Path((_username, artype, id, path)): Path<(String, ApiResourceType, u32, String)>,
302303
Json(req): Json<Value>,
303-
) -> ApiResult<Json<Value>> {
304+
) -> ApiV1Result<Json<Value>> {
304305
match artype {
305306
ApiResourceType::Lights => {
306307
log::debug!("req: {}", serde_json::to_string_pretty(&req)?);
@@ -349,7 +350,7 @@ async fn put_api_user_resource_id_path(
349350
V1Reply::for_group_path(id, &path).with_light_state_update(&upd)?
350351
}
351352
ApiGroupActionUpdate::GroupUpdate(upd) => {
352-
let scene_id = upd.scene.parse()?;
353+
let scene_id = upd.scene.parse().map_err(ApiError::ParseIntError)?;
353354
let scene_uuid = lock.from_id_v1(scene_id)?;
354355
let rlink = RType::Scene.link_to(scene_uuid);
355356
let updv2 = SceneUpdate::new().with_recall_action(Some(SceneStatus {
@@ -404,7 +405,7 @@ async fn put_api_user_resource_id_path(
404405
| ApiResourceType::Scenes
405406
| ApiResourceType::Schedules
406407
| ApiResourceType::Sensors
407-
| ApiResourceType::Capabilities => Err(ApiError::V1CreateUnsupported(artype)),
408+
| ApiResourceType::Capabilities => Err(ApiV1Error::V1CreateUnsupported(artype)),
408409
}
409410
}
410411

@@ -414,8 +415,8 @@ async fn put_api_user_resource_id_path(
414415
/// even though this does not seem to ever have been a valid hue endpoint.
415416
///
416417
/// 2025-01-24: This response has been confirmed to work by Alexa and Peter Miller on discord.
417-
pub async fn workaround_iconnect_hue() -> impl IntoResponse {
418-
Json(json!([{"error":{"type":1,"address":"/","description":"unauthorized user"}}]))
418+
pub async fn workaround_iconnect_hue() -> ApiV1Result<()> {
419+
Err(HueApiV1Error::UnauthorizedUser)?
419420
}
420421

421422
pub fn router() -> Router<AppState> {

src/routes/clip/mod.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ use entertainment_configuration as ent_conf;
99

1010
use axum::Router;
1111
use axum::extract::{Path, State};
12-
use axum::response::IntoResponse;
1312
use axum::routing::{delete, get, post, put};
1413
use hue::api::{RType, ResourceLink};
1514
use serde::{Deserialize, Serialize};
@@ -51,7 +50,7 @@ impl<T: Serialize> V2Reply<T> {
5150
}
5251
}
5352

54-
async fn get_all_resources(State(state): State<AppState>) -> impl IntoResponse {
53+
async fn get_all_resources(State(state): State<AppState>) -> ApiV2Result {
5554
let lock = state.res.lock().await;
5655
let res = lock.get_resources();
5756
drop(lock);
@@ -69,7 +68,7 @@ async fn post_resource(
6968
State(state): State<AppState>,
7069
Path(rtype): Path<RType>,
7170
Json(req): Json<Value>,
72-
) -> impl IntoResponse {
71+
) -> ApiV2Result {
7372
log::info!("POST {rtype:?}");
7473
log::debug!("Json data:\n{}", serde_json::to_string_pretty(&req)?);
7574

0 commit comments

Comments
 (0)