Highly ambitious ATProtocol AppView service and sdks
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

update error handling in dynamic handlers to support status 400 and a json response

Chad Miller b9fc3ca4 0d07d66b

+54 -48
+54 -31
api/src/handler_xrpc_dynamic.rs
··· 12 12 use crate::models::{ListRecordsOutput, Record}; 13 13 use crate::AppState; 14 14 15 + // Helper function to convert StatusCode errors to JSON error responses 16 + fn status_to_error_response(status: StatusCode) -> (StatusCode, Json<serde_json::Value>) { 17 + let message = match status { 18 + StatusCode::UNAUTHORIZED => "Authentication required", 19 + StatusCode::FORBIDDEN => "Access forbidden", 20 + StatusCode::NOT_FOUND => "Resource not found", 21 + StatusCode::BAD_REQUEST => "Invalid request", 22 + StatusCode::INTERNAL_SERVER_ERROR => "Internal server error", 23 + _ => "Request failed", 24 + }; 25 + 26 + (status, Json(serde_json::json!({ 27 + "error": status.as_str(), 28 + "message": message 29 + }))) 30 + } 31 + 15 32 // Custom deserializer to handle comma-separated strings as Vec<String> 16 33 fn deserialize_comma_separated<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error> 17 34 where ··· 88 105 State(state): State<AppState>, 89 106 headers: HeaderMap, 90 107 Json(body): Json<serde_json::Value>, 91 - ) -> Result<Json<serde_json::Value>, StatusCode> { 108 + ) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> { 92 109 93 110 // Handle dynamic collection methods (e.g., social.grain.gallery.createRecord) 94 111 if method.ends_with(".createRecord") { ··· 101 118 let collection = method.trim_end_matches(".deleteRecord").to_string(); 102 119 dynamic_collection_delete_impl(state, headers, body, collection).await 103 120 } else { 104 - Err(StatusCode::NOT_FOUND) 121 + Err(status_to_error_response(StatusCode::NOT_FOUND)) 105 122 } 106 123 } 107 124 ··· 275 292 headers: HeaderMap, 276 293 body: serde_json::Value, 277 294 collection: String, 278 - ) -> Result<Json<serde_json::Value>, StatusCode> { 295 + ) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> { 279 296 // Extract and verify OAuth token 280 - let token = extract_bearer_token(&headers)?; 281 - let user_info = verify_oauth_token(&token, &state.config.auth_base_url).await?; 297 + let token = extract_bearer_token(&headers).map_err(status_to_error_response)?; 298 + let user_info = verify_oauth_token(&token, &state.config.auth_base_url).await 299 + .map_err(status_to_error_response)?; 282 300 283 301 // Get AT Protocol DPoP auth and PDS URL 284 - let (dpop_auth, pds_url) = get_atproto_auth_for_user(&token, &state.config.auth_base_url).await?; 302 + let (dpop_auth, pds_url) = get_atproto_auth_for_user(&token, &state.config.auth_base_url).await 303 + .map_err(status_to_error_response)?; 285 304 286 305 // Extract the repo DID from user info 287 306 let repo = user_info.did.unwrap_or(user_info.sub); ··· 292 311 // Extract slice URI, rkey, and record value from structured body 293 312 let slice_uri = body.get("slice") 294 313 .and_then(|v| v.as_str()) 295 - .ok_or(StatusCode::BAD_REQUEST)? 314 + .ok_or_else(|| status_to_error_response(StatusCode::BAD_REQUEST))? 296 315 .to_string(); 297 316 298 317 let record_key = body.get("rkey") ··· 301 320 .map(|s| s.to_string()); 302 321 303 322 let record_data = body.get("record") 304 - .ok_or(StatusCode::BAD_REQUEST)? 323 + .ok_or_else(|| status_to_error_response(StatusCode::BAD_REQUEST))? 305 324 .clone(); 306 325 307 326 ··· 323 342 } 324 343 325 344 if let Err(e) = validator.validate_record(&collection, &record_data) { 326 - let error_response = serde_json::json!({ 345 + return Err((StatusCode::BAD_REQUEST, Json(serde_json::json!({ 327 346 "error": "ValidationError", 328 347 "message": format!("Lexicon validation failed: {}", e) 329 - }); 330 - return Ok(Json(error_response)); 348 + })))); 331 349 } 332 350 }, 333 351 Err(e) => { ··· 351 369 let result = create_record(&http_client, &dpop_auth, &pds_url, create_request) 352 370 .await 353 371 .map_err(|_e| { 354 - StatusCode::INTERNAL_SERVER_ERROR 372 + status_to_error_response(StatusCode::INTERNAL_SERVER_ERROR) 355 373 })?; 356 374 357 375 // Extract URI and CID from the response enum ··· 360 378 (uri, cid) 361 379 }, 362 380 CreateRecordResponse::Error(_e) => { 363 - return Err(StatusCode::INTERNAL_SERVER_ERROR); 381 + return Err(status_to_error_response(StatusCode::INTERNAL_SERVER_ERROR)); 364 382 }, 365 383 }; 366 384 ··· 392 410 headers: HeaderMap, 393 411 body: serde_json::Value, 394 412 collection: String, 395 - ) -> Result<Json<serde_json::Value>, StatusCode> { 413 + ) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> { 396 414 // Extract and verify OAuth token 397 - let token = extract_bearer_token(&headers)?; 398 - let user_info = verify_oauth_token(&token, &state.config.auth_base_url).await?; 415 + let token = extract_bearer_token(&headers).map_err(status_to_error_response)?; 416 + let user_info = verify_oauth_token(&token, &state.config.auth_base_url).await 417 + .map_err(status_to_error_response)?; 399 418 400 419 // Get AT Protocol DPoP auth and PDS URL 401 - let (dpop_auth, pds_url) = get_atproto_auth_for_user(&token, &state.config.auth_base_url).await?; 420 + let (dpop_auth, pds_url) = get_atproto_auth_for_user(&token, &state.config.auth_base_url).await 421 + .map_err(status_to_error_response)?; 402 422 403 423 // Extract slice URI, rkey, and record value from structured body 404 424 let slice_uri = body.get("slice") 405 425 .and_then(|v| v.as_str()) 406 - .ok_or(StatusCode::BAD_REQUEST)? 426 + .ok_or_else(|| status_to_error_response(StatusCode::BAD_REQUEST))? 407 427 .to_string(); 408 428 409 429 let rkey = body.get("rkey") 410 430 .and_then(|v| v.as_str()) 411 - .ok_or(StatusCode::BAD_REQUEST)? 431 + .ok_or_else(|| status_to_error_response(StatusCode::BAD_REQUEST))? 412 432 .to_string(); 413 433 414 434 let record_data = body.get("record") 415 - .ok_or(StatusCode::BAD_REQUEST)? 435 + .ok_or_else(|| status_to_error_response(StatusCode::BAD_REQUEST))? 416 436 .clone(); 417 437 418 438 // Extract repo from user info ··· 422 442 match LexiconValidator::for_slice(&state.database, &slice_uri).await { 423 443 Ok(validator) => { 424 444 if let Err(e) = validator.validate_record(&collection, &record_data) { 425 - let error_response = serde_json::json!({ 445 + return Err((StatusCode::BAD_REQUEST, Json(serde_json::json!({ 426 446 "error": "ValidationError", 427 447 "message": format!("Lexicon validation failed: {}", e) 428 - }); 429 - return Ok(Json(error_response)); 448 + })))); 430 449 } 431 450 }, 432 451 Err(e) => { ··· 451 470 452 471 let result = put_record(&http_client, &dpop_auth, &pds_url, put_request) 453 472 .await 454 - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; 473 + .map_err(|_| status_to_error_response(StatusCode::INTERNAL_SERVER_ERROR))?; 455 474 456 475 // Extract URI and CID from the response enum 457 476 let (uri, cid) = match result { 458 477 PutRecordResponse::StrongRef { uri, cid, .. } => (uri, cid), 459 - PutRecordResponse::Error(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR), 478 + PutRecordResponse::Error(_) => return Err(status_to_error_response(StatusCode::INTERNAL_SERVER_ERROR)), 460 479 }; 461 480 462 481 // Also update in local database for indexing ··· 485 504 headers: HeaderMap, 486 505 body: serde_json::Value, 487 506 collection: String, 488 - ) -> Result<Json<serde_json::Value>, StatusCode> { 507 + ) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> { 489 508 // Extract and verify OAuth token 490 - let token = extract_bearer_token(&headers)?; 491 - let user_info = verify_oauth_token(&token, &state.config.auth_base_url).await?; 509 + let token = extract_bearer_token(&headers).map_err(status_to_error_response)?; 510 + let user_info = verify_oauth_token(&token, &state.config.auth_base_url).await 511 + .map_err(status_to_error_response)?; 492 512 493 513 // Get AT Protocol DPoP auth and PDS URL 494 - let (dpop_auth, pds_url) = get_atproto_auth_for_user(&token, &state.config.auth_base_url).await?; 514 + let (dpop_auth, pds_url) = get_atproto_auth_for_user(&token, &state.config.auth_base_url).await 515 + .map_err(status_to_error_response)?; 495 516 496 517 // Extract repo and rkey from body 497 518 let repo = user_info.did.unwrap_or(user_info.sub); 498 - let rkey = body["rkey"].as_str().ok_or(StatusCode::BAD_REQUEST)?.to_string(); 519 + let rkey = body["rkey"].as_str() 520 + .ok_or_else(|| status_to_error_response(StatusCode::BAD_REQUEST))? 521 + .to_string(); 499 522 500 523 // Create HTTP client 501 524 let http_client = reqwest::Client::new(); ··· 511 534 512 535 delete_record(&http_client, &dpop_auth, &pds_url, delete_request) 513 536 .await 514 - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; 537 + .map_err(|_| status_to_error_response(StatusCode::INTERNAL_SERVER_ERROR))?; 515 538 516 539 // Also delete from local database 517 540 let uri = format!("at://{}/{}/{}", repo, collection, rkey);
-17
frontend/src/routes/slices.tsx
··· 364 364 lexiconRecord 365 365 ); 366 366 367 - // Check if the result contains a validation error (API returns 200 with error object) 368 - if ( 369 - result && 370 - typeof result === "object" && 371 - "error" in result && 372 - result.error === "ValidationError" 373 - ) { 374 - const html = render( 375 - <LexiconErrorMessage 376 - error={result.message || "Lexicon validation failed"} 377 - /> 378 - ); 379 - return new Response(html, { 380 - status: 200, // Return 200 so HTMX displays the error 381 - headers: { "content-type": "text/html" }, 382 - }); 383 - } 384 367 385 368 const html = render( 386 369 <LexiconSuccessMessage