marianoguerra/atproto/xrpc does not have a README file

    Transport

    pub(open) trait Transport {
    async fn send(Self, HttpRequest) -> HttpResponse
    fn describe(Self) -> String
    }

    A thing that can send an XRPC request and return what came back.

    Deliberately dumb. An implementation has exactly three obligations, and all three are things a hand-written one gets wrong:

    • Lower-case the response header names.
    • Do not forward content-length; most HTTP clients compute their own, and sending both risks two conflicting headers on the wire.
    • Do not let the underlying client's errors escape. Translate them, or a caller's catch sees a type from a library it never imported.

    XrpcError

    pub(all) suberror XrpcError {
    RequestFailed(String)
    Status(status~ : Int, error~ : String, message~ : String?, headers~ : Map[String, String])
    RateLimited(retry_after_millis~ : Int64?, error~ : String, message~ : String?)
    Decode(
    DecodeError
    )
    Encode(String)
    } derive(
    Debug
    )

    impl Show for XrpcError

    XrpcError::describe_error

    fn XrpcError::describe_error(self : XrpcError) -> String

    XrpcError::error_name

    fn XrpcError::error_name(self : XrpcError) -> String

    The machine-readable name: the error field of the body, or a name derived from the status when the server did not send one.

    XrpcError::is_expired_token

    fn XrpcError::is_expired_token(self : XrpcError) -> Bool

    The access token has expired and a refresh should be attempted.

    The condition is upstream's and it is not just "401": a PDS may answer 400 with ExpiredToken, and a client that only checked the status would log the user out instead of refreshing.

    XrpcError::message

    fn XrpcError::message(self : XrpcError) -> String?

    XrpcError::retry_after_millis

    fn XrpcError::retry_after_millis(self : XrpcError) -> Int64?

    How long to wait before retrying, if the server said.

    None means it did not, and the caller should pick its own backoff -- the reference implementation uses min(30s, 500ms * 2^attempt).

    XrpcError::should_retry

    fn XrpcError::should_retry(self : XrpcError) -> Bool

    Whether the same request is worth sending again.

    The status set is the reference implementation's RETRYABLE_HTTP_STATUS_CODES. A transport failure is retryable because it may never have reached the server; a 4xx other than these is not, because sending it again produces the same answer.

    XrpcError::status

    fn XrpcError::status(self : XrpcError) -> Int?

    Body

    pub(all) enum Body {
    Empty
    Json(
    LexValue
    )
    Blob(bytes~ : Bytes, mime_type~ : String)
    } derive(Eq,
    Debug
    )

    What goes in the request body.

    Credential

    pub(all) enum Credential {
    Anonymous
    Bearer(String)
    } derive(Eq,
    Debug
    )

    How the caller proves who it is.

    HttpRequest

    pub(all) struct HttpRequest {
    url : String
    http_method : String
    headers : Map[String, String]
    body : Bytes
    } derive(Eq,
    Debug
    )

    One outbound call, already reduced to bytes.

    HttpRequest::body_text

    fn HttpRequest::body_text(self : HttpRequest) -> String

    The body as text, for logging and tests. Lossy by design: a blob upload is not text and printing it should not fail.

    HttpRequest::content_length

    fn HttpRequest::content_length(self : HttpRequest) -> Int

    The UTF-8 byte count, which is what Content-Length must be -- and is not the string length, because MoonBit strings are UTF-16.

    HttpResponse

    pub(all) struct HttpResponse {
    status : Int
    headers : Map[String, String]
    body : Bytes
    } derive(Eq,
    Debug
    )

    HttpResponse::body_text

    fn HttpResponse::body_text(self : HttpResponse) -> String

    The body as text. Lossy, because a malformed body must still be reportable rather than turning into a second, less useful error.

    HttpResponse::content_type

    fn HttpResponse::content_type(self : HttpResponse) -> String?

    The media type, without parameters -- application/json from application/json; charset=utf-8.

    HttpResponse::header

    fn HttpResponse::header(self : HttpResponse, name : String) -> String?

    Method

    pub(all) enum Method {
    Query
    Procedure
    } derive(Eq,
    Debug
    )

    A query reads and a procedure writes, and that is the only thing that decides the HTTP method.

    Method::http_method

    fn Method::http_method(self : Method) -> String

    ParamValue

    pub(all) enum ParamValue {
    Str(String)
    Int(Int64)
    Bool(Bool)
    Strs(Array[String])
    Ints(Array[Int64])
    Bools(Array[Bool])
    } derive(Eq,
    Debug
    )

    Params

    pub(all) struct Params {
    entries : Array[(String, ParamValue)]
    } derive(Eq,
    Debug
    )

    An ordered list rather than a map, because two entries may share a key -- that is how arrays are spelled -- and because a stable order makes a request comparable in a test.

    Params::encode

    fn Params::encode(self : Params) -> String

    The query string, without a leading ?, or "" when there is nothing to send. An empty array contributes nothing at all, which matches the reference implementation -- ?tags= would mean a tag that is the empty string.

    Params::is_empty

    fn Params::is_empty(self : Params) -> Bool

    Params::length

    fn Params::length(self : Params) -> Int

    Params::new

    fn Params::new() -> Params

    Params::of

    fn Params::of(entries : Array[(String, ParamValue)]) -> Params

    Params::put

    fn Params::put(self : Params, key : String, value : ParamValue) -> Unit

    Params::put_bool

    fn Params::put_bool(self : Params, key : String, value : Bool?) -> Unit

    Params::put_int

    fn Params::put_int(self : Params, key : String, value : Int64?) -> Unit

    Params::put_opt

    fn Params::put_opt(self : Params, key : String, value : ParamValue?) -> Unit

    The whole reason this type exists rather than a Map: an absent optional argument must vanish, not become an empty string. Every generated call is a column of these.

    Params::put_string

    fn Params::put_string(self : Params, key : String, value : String?) -> Unit

    Params::put_strings

    fn Params::put_strings(self : Params, key : String, value : Array[String]?) -> Unit

    USER_AGENT

    let USER_AGENT : String

    build_request

    fn build_request(service : String, nsid :
    Nsid
    , kind : Method, params? : Params, body? : Body, credential? : Credential, accept? : String, extra_headers? : Map[String, String]) -> HttpRequest

    Assembles a request. Pure, and the reason almost all of this package can be tested without a transport.

    service is the origin to send to -- https://bsky.social, or the PDS from the account's DID document. A trailing slash on it is tolerated, because callers paste these from configuration and one showing up should not produce //xrpc/....

    interpret

    Turns a response into the decoded body, or into the right error.

    The order matters. A 429 is a rate limit whatever its body says; a non-2xx is an error even if the body happens to parse; and only then is a 2xx body worth decoding.

    interpret_bytes

    fn interpret_bytes(response : HttpResponse) -> Bytes raise XrpcError

    A 2xx whose body is not JSON: getBlob, getRepo, the video endpoints. Returns the bytes and lets the caller decide what they are.

    percent_encode

    fn percent_encode(text : String) -> String

    RFC 3986 percent-encoding of everything outside the unreserved set.

    Encoding goes through the UTF-8 bytes, not the string's UTF-16 code units. Getting that wrong is invisible until someone searches for a word with an accent in it, and then the query silently matches nothing.

    status_error_name

    fn status_error_name(status : Int) -> String

    The name to use when the server sent a status but no error field.

    The table is the reference implementation's ResponseType. Anything unlisted collapses to InvalidRequest below 500 and InternalServerError at or above, which is what upstream's fallback does.

    to_xrpc_error

    fn to_xrpc_error(e : Error) -> XrpcError

    Narrows a caught Error back to this taxonomy.

    Needed because variant patterns are the only way to match an error value -- the suberror TYPE name is not a pattern -- and they are only in scope inside this package. Anything from elsewhere becomes a transport failure, which is the honest reading: it came from the code that does the sending.