Noreposts Feed

Return proper ATProto error response format

- Added ErrorResponse type with error and message fields
- Changed get_feed_skeleton to return Response for flexible error handling
- Return AuthenticationRequired error with descriptive message
- Follows ATProto error response format for proper client handling

+63 -26
+56 -26
src/main.rs
··· 2 2 use axum::{ 3 3 extract::{Query, State}, 4 4 http::{StatusCode, HeaderMap}, 5 - response::Json, 5 + response::{Json, IntoResponse, Response}, 6 6 routing::get, 7 7 Router, 8 8 }; ··· 133 133 headers: HeaderMap, 134 134 Query(params): Query<FeedSkeletonParams>, 135 135 State(state): State<AppState>, 136 - ) -> Result<Json<FeedSkeletonResponse>, StatusCode> { 136 + ) -> Response { 137 137 info!("Received feed skeleton request for feed: {}", params.feed); 138 138 139 - // Validate JWT if provided in Authorization header 140 - let requester_did = if let Some(auth_header) = headers.get("authorization") { 141 - let auth_str = auth_header.to_str().map_err(|_| { 139 + // This feed requires authentication since it's personalized 140 + let auth_header = match headers.get("authorization") { 141 + Some(h) => h, 142 + None => { 143 + warn!("Missing Authorization header - this feed requires authentication"); 144 + return ( 145 + StatusCode::UNAUTHORIZED, 146 + Json(types::ErrorResponse { 147 + error: "AuthenticationRequired".to_string(), 148 + message: "This feed shows posts from accounts you follow and requires authentication".to_string(), 149 + }) 150 + ).into_response(); 151 + } 152 + }; 153 + 154 + let auth_str = match auth_header.to_str() { 155 + Ok(s) => s, 156 + Err(_) => { 142 157 warn!("Invalid authorization header format"); 143 - StatusCode::UNAUTHORIZED 144 - })?; 158 + return ( 159 + StatusCode::UNAUTHORIZED, 160 + Json(types::ErrorResponse { 161 + error: "AuthenticationRequired".to_string(), 162 + message: "Invalid authorization header format".to_string(), 163 + }) 164 + ).into_response(); 165 + } 166 + }; 145 167 146 - // Remove "Bearer " prefix if present 147 - let token = auth_str.strip_prefix("Bearer ").unwrap_or(auth_str); 168 + // Remove "Bearer " prefix if present 169 + let token = auth_str.strip_prefix("Bearer ").unwrap_or(auth_str); 148 170 149 - info!("Validating JWT for request"); 150 - match validate_jwt(token, &state.service_did) { 151 - Ok(claims) => { 152 - info!("Authenticated request from DID: {}", claims.iss); 153 - Some(claims.iss) 154 - }, 155 - Err(e) => { 156 - warn!("JWT validation failed: {}", e); 157 - return Err(StatusCode::UNAUTHORIZED); 158 - } 171 + info!("Validating JWT for request"); 172 + let requester_did = match validate_jwt(token, &state.service_did) { 173 + Ok(claims) => { 174 + info!("Authenticated request from DID: {}", claims.iss); 175 + claims.iss 176 + }, 177 + Err(e) => { 178 + warn!("JWT validation failed: {}", e); 179 + return ( 180 + StatusCode::UNAUTHORIZED, 181 + Json(types::ErrorResponse { 182 + error: "AuthenticationRequired".to_string(), 183 + message: format!("JWT validation failed: {}", e), 184 + }) 185 + ).into_response(); 159 186 } 160 - } else { 161 - info!("Unauthenticated request (no Authorization header)"); 162 - None 163 187 }; 164 188 165 189 let feed_algorithm = FollowingNoRepostsFeed::new(Arc::clone(&state.db)); 166 190 167 - info!("Generating feed for requester: {:?}, limit: {:?}, cursor: {:?}", 191 + info!("Generating feed for requester: {}, limit: {:?}, cursor: {:?}", 168 192 requester_did, params.limit, params.cursor); 169 193 170 194 match feed_algorithm 171 - .generate_feed(requester_did, params.limit, params.cursor) 195 + .generate_feed(Some(requester_did), params.limit, params.cursor) 172 196 .await 173 197 { 174 198 Ok(response) => { 175 199 info!("Successfully generated feed with {} posts", response.feed.len()); 176 - Ok(Json(response)) 200 + Json(response).into_response() 177 201 }, 178 202 Err(e) => { 179 203 warn!("Feed generation error: {}", e); 180 - Err(StatusCode::INTERNAL_SERVER_ERROR) 204 + ( 205 + StatusCode::INTERNAL_SERVER_ERROR, 206 + Json(types::ErrorResponse { 207 + error: "InternalServerError".to_string(), 208 + message: format!("Failed to generate feed: {}", e), 209 + }) 210 + ).into_response() 181 211 } 182 212 } 183 213 }
+7
src/types.rs
··· 63 63 pub aud: String, // audience (feed generator DID) 64 64 pub exp: i64, // expiration time 65 65 } 66 + 67 + // ATProto Error Response 68 + #[derive(Debug, Serialize)] 69 + pub struct ErrorResponse { 70 + pub error: String, 71 + pub message: String, 72 + }