A library for ATProtocol identities.

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 4 //! with support for DPoP (Demonstration of Proof-of-Possession) authentication. 5 5 6 6 use crate::errors::{ClientError, DPoPError}; 7 - use anyhow::{Context, Result}; 7 + use anyhow::Result; 8 8 use atproto_identity::key::KeyData; 9 9 use atproto_oauth::dpop::{DpopRetry, request_dpop}; 10 10 use bytes::Bytes; ··· 77 77 .get(url) 78 78 .headers(additional_headers.clone()) 79 79 .send() 80 - .instrument(tracing::info_span!("get_json_with_headers", url = %url)) 81 80 .await 82 81 .map_err(|error| ClientError::HttpRequestFailed { 83 82 url: url.to_string(), ··· 140 139 .get(url) 141 140 .headers(additional_headers.clone()) 142 141 .send() 143 - .instrument(tracing::info_span!("get_bytes_with_headers", url = %url)) 144 142 .await 145 143 .map_err(|error| ClientError::HttpRequestFailed { 146 144 url: url.to_string(), 147 145 error, 148 146 })?; 149 - http_response 147 + Ok(http_response 150 148 .bytes() 151 149 .await 152 - .context("failed streaming bytes") 150 + .map_err(|error| ClientError::ByteStreamFailed { 151 + url: url.to_string(), 152 + error, 153 + })?) 153 154 } 154 155 155 156 /// Performs a DPoP-authenticated HTTP GET request and parses the response as JSON. ··· 230 231 ) 231 232 .header("DPoP", &dpop_proof_token) 232 233 .send() 233 - .instrument(tracing::info_span!("dpop_get_request_with_options", url = %url)) 234 234 .await 235 - .inspect_err(|err| { 236 - println!("response error: {err:?}"); 237 - }) 238 235 .map_err(|error| DPoPError::HttpRequestFailed { 239 236 url: url.to_string(), 240 237 error, 241 238 })?; 242 239 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 240 let value = http_response 250 241 .json::<serde_json::Value>() 251 242 .await 252 - .inspect_err(|err| { 253 - println!("json error: {err:?}"); 254 - }) 255 243 .map_err(|error| DPoPError::JsonParseFailed { 256 244 url: url.to_string(), 257 245 error, ··· 376 364 .header("DPoP", &dpop_proof_token) 377 365 .json(&record) 378 366 .send() 379 - .instrument(tracing::info_span!("dpop_post_request", url = %url)) 380 367 .await 381 368 .map_err(|error| DPoPError::HttpRequestFailed { 382 369 url: url.to_string(), ··· 447 434 .headers(additional_headers.clone()) 448 435 .json(&data) 449 436 .send() 450 - .instrument(tracing::info_span!("post_json_with_headers", url = %url)) 451 437 .await 452 438 .map_err(|error| ClientError::HttpRequestFailed { 453 439 url: url.to_string(), ··· 523 509 .get(url) 524 510 .headers(headers) 525 511 .send() 526 - .instrument(tracing::info_span!("get_apppassword_json_with_headers", url = %url)) 527 512 .await 528 513 .map_err(|error| ClientError::HttpRequestFailed { 529 514 url: url.to_string(), ··· 604 589 .headers(headers) 605 590 .json(&data) 606 591 .send() 607 - .instrument(tracing::info_span!("post_apppassword_json_with_headers", url = %url)) 608 592 .await 609 593 .map_err(|error| ClientError::HttpRequestFailed { 610 594 url: url.to_string(), ··· 621 605 622 606 Ok(value) 623 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 75 /// The underlying parse error 76 76 error: reqwest::Error, 77 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 + }, 78 87 } 79 88 80 89 /// Error types that can occur during DPoP authentication operations.