feature: Added post app-password bytes functions to atproto-client

Signed-off-by: Nick Gerakines <nick.gerakines@gmail.com>

Changed files
+115 -22
crates
atproto-client
+106 -22
crates/atproto-client/src/client.rs
··· 4 //! with support for DPoP (Demonstration of Proof-of-Possession) authentication. 5 6 use crate::errors::{ClientError, DPoPError}; 7 - use anyhow::{Context, Result}; 8 use atproto_identity::key::KeyData; 9 use atproto_oauth::dpop::{DpopRetry, request_dpop}; 10 use bytes::Bytes; ··· 77 .get(url) 78 .headers(additional_headers.clone()) 79 .send() 80 - .instrument(tracing::info_span!("get_json_with_headers", url = %url)) 81 .await 82 .map_err(|error| ClientError::HttpRequestFailed { 83 url: url.to_string(), ··· 140 .get(url) 141 .headers(additional_headers.clone()) 142 .send() 143 - .instrument(tracing::info_span!("get_bytes_with_headers", url = %url)) 144 .await 145 .map_err(|error| ClientError::HttpRequestFailed { 146 url: url.to_string(), 147 error, 148 })?; 149 - http_response 150 .bytes() 151 .await 152 - .context("failed streaming bytes") 153 } 154 155 /// Performs a DPoP-authenticated HTTP GET request and parses the response as JSON. ··· 230 ) 231 .header("DPoP", &dpop_proof_token) 232 .send() 233 - .instrument(tracing::info_span!("dpop_get_request_with_options", url = %url)) 234 .await 235 - .inspect_err(|err| { 236 - println!("response error: {err:?}"); 237 - }) 238 .map_err(|error| DPoPError::HttpRequestFailed { 239 url: url.to_string(), 240 error, 241 })?; 242 243 - let status_code = http_response.status(); 244 - println!("status_code {status_code:?}"); 245 - 246 - let headers = http_response.headers(); 247 - println!("headers {headers:?}"); 248 - 249 let value = http_response 250 .json::<serde_json::Value>() 251 .await 252 - .inspect_err(|err| { 253 - println!("json error: {err:?}"); 254 - }) 255 .map_err(|error| DPoPError::JsonParseFailed { 256 url: url.to_string(), 257 error, ··· 376 .header("DPoP", &dpop_proof_token) 377 .json(&record) 378 .send() 379 - .instrument(tracing::info_span!("dpop_post_request", url = %url)) 380 .await 381 .map_err(|error| DPoPError::HttpRequestFailed { 382 url: url.to_string(), ··· 447 .headers(additional_headers.clone()) 448 .json(&data) 449 .send() 450 - .instrument(tracing::info_span!("post_json_with_headers", url = %url)) 451 .await 452 .map_err(|error| ClientError::HttpRequestFailed { 453 url: url.to_string(), ··· 523 .get(url) 524 .headers(headers) 525 .send() 526 - .instrument(tracing::info_span!("get_apppassword_json_with_headers", url = %url)) 527 .await 528 .map_err(|error| ClientError::HttpRequestFailed { 529 url: url.to_string(), ··· 604 .headers(headers) 605 .json(&data) 606 .send() 607 - .instrument(tracing::info_span!("post_apppassword_json_with_headers", url = %url)) 608 .await 609 .map_err(|error| ClientError::HttpRequestFailed { 610 url: url.to_string(), ··· 621 622 Ok(value) 623 }
··· 4 //! with support for DPoP (Demonstration of Proof-of-Possession) authentication. 5 6 use crate::errors::{ClientError, DPoPError}; 7 + use anyhow::Result; 8 use atproto_identity::key::KeyData; 9 use atproto_oauth::dpop::{DpopRetry, request_dpop}; 10 use bytes::Bytes; ··· 77 .get(url) 78 .headers(additional_headers.clone()) 79 .send() 80 .await 81 .map_err(|error| ClientError::HttpRequestFailed { 82 url: url.to_string(), ··· 139 .get(url) 140 .headers(additional_headers.clone()) 141 .send() 142 .await 143 .map_err(|error| ClientError::HttpRequestFailed { 144 url: url.to_string(), 145 error, 146 })?; 147 + Ok(http_response 148 .bytes() 149 .await 150 + .map_err(|error| ClientError::ByteStreamFailed { 151 + url: url.to_string(), 152 + error, 153 + })?) 154 } 155 156 /// Performs a DPoP-authenticated HTTP GET request and parses the response as JSON. ··· 231 ) 232 .header("DPoP", &dpop_proof_token) 233 .send() 234 .await 235 .map_err(|error| DPoPError::HttpRequestFailed { 236 url: url.to_string(), 237 error, 238 })?; 239 240 let value = http_response 241 .json::<serde_json::Value>() 242 .await 243 .map_err(|error| DPoPError::JsonParseFailed { 244 url: url.to_string(), 245 error, ··· 364 .header("DPoP", &dpop_proof_token) 365 .json(&record) 366 .send() 367 .await 368 .map_err(|error| DPoPError::HttpRequestFailed { 369 url: url.to_string(), ··· 434 .headers(additional_headers.clone()) 435 .json(&data) 436 .send() 437 .await 438 .map_err(|error| ClientError::HttpRequestFailed { 439 url: url.to_string(), ··· 509 .get(url) 510 .headers(headers) 511 .send() 512 .await 513 .map_err(|error| ClientError::HttpRequestFailed { 514 url: url.to_string(), ··· 589 .headers(headers) 590 .json(&data) 591 .send() 592 .await 593 .map_err(|error| ClientError::HttpRequestFailed { 594 url: url.to_string(), ··· 605 606 Ok(value) 607 } 608 + 609 + /// Performs an app password-authenticated HTTP GET request and returns the response as bytes. 610 + /// 611 + /// # Arguments 612 + /// 613 + /// * `http_client` - The HTTP client to use for the request 614 + /// * `app_auth` - App password authentication credentials 615 + /// * `url` - The URL to request 616 + /// * `additional_headers` - Additional HTTP headers to include in the request 617 + /// 618 + /// # Returns 619 + /// 620 + /// The response body as bytes 621 + /// 622 + /// # Errors 623 + /// 624 + /// Returns `ClientError::HttpRequestFailed` if the HTTP request fails, 625 + /// or an error if streaming the response bytes fails. 626 + pub async fn get_apppassword_bytes_with_headers( 627 + http_client: &reqwest::Client, 628 + app_auth: &AppPasswordAuth, 629 + url: &str, 630 + additional_headers: &HeaderMap, 631 + ) -> Result<Bytes> { 632 + let mut headers = additional_headers.clone(); 633 + headers.insert( 634 + reqwest::header::AUTHORIZATION, 635 + reqwest::header::HeaderValue::from_str(&format!("Bearer {}", app_auth.access_token))?, 636 + ); 637 + let http_response = http_client 638 + .get(url) 639 + .headers(headers) 640 + .send() 641 + .await 642 + .map_err(|error| ClientError::HttpRequestFailed { 643 + url: url.to_string(), 644 + error, 645 + })?; 646 + Ok(http_response 647 + .bytes() 648 + .await 649 + .map_err(|error| ClientError::ByteStreamFailed { 650 + url: url.to_string(), 651 + error, 652 + })?) 653 + } 654 + 655 + 656 + /// Performs an app password-authenticated HTTP POST request with JSON body and returns the response as bytes. 657 + /// 658 + /// This is useful when the server returns binary data such as images, CAR files, 659 + /// or other non-JSON content in response to authenticated POST requests. 660 + /// 661 + /// # Arguments 662 + /// 663 + /// * `http_client` - The HTTP client to use for the request 664 + /// * `app_auth` - App password authentication credentials 665 + /// * `url` - The URL to request 666 + /// * `record` - The JSON data to send in the request body 667 + /// * `additional_headers` - Additional HTTP headers to include in the request 668 + /// 669 + /// # Returns 670 + /// 671 + /// The response body as bytes 672 + /// 673 + /// # Errors 674 + /// 675 + /// Returns `ClientError::HttpRequestFailed` if the HTTP request fails, 676 + /// or an error if streaming the response bytes fails. 677 + pub async fn post_apppassword_bytes_with_headers( 678 + http_client: &reqwest::Client, 679 + app_auth: &AppPasswordAuth, 680 + url: &str, 681 + record: serde_json::Value, 682 + additional_headers: &HeaderMap, 683 + ) -> Result<Bytes> { 684 + let mut headers = additional_headers.clone(); 685 + headers.insert( 686 + reqwest::header::AUTHORIZATION, 687 + reqwest::header::HeaderValue::from_str(&format!("Bearer {}", app_auth.access_token))?, 688 + ); 689 + let http_response = http_client 690 + .post(url) 691 + .headers(headers) 692 + .json(&record) 693 + .send() 694 + .instrument(tracing::info_span!("get_apppassword_bytes_with_headers", url = %url)) 695 + .await 696 + .map_err(|error| ClientError::HttpRequestFailed { 697 + url: url.to_string(), 698 + error, 699 + })?; 700 + Ok(http_response 701 + .bytes() 702 + .await 703 + .map_err(|error| ClientError::ByteStreamFailed { 704 + url: url.to_string(), 705 + error, 706 + })?) 707 + }
+9
crates/atproto-client/src/errors.rs
··· 75 /// The underlying parse error 76 error: reqwest::Error, 77 }, 78 } 79 80 /// Error types that can occur during DPoP authentication operations.
··· 75 /// The underlying parse error 76 error: reqwest::Error, 77 }, 78 + 79 + /// Occurs when streaming response bytes fails 80 + #[error("error-atproto-client-http-3 Failed to stream response bytes: {url} {error}")] 81 + ByteStreamFailed { 82 + /// The URL that was requested 83 + url: String, 84 + /// The underlying streaming error 85 + error: reqwest::Error, 86 + }, 87 } 88 89 /// Error types that can occur during DPoP authentication operations.