···2828 AccountDID *syntax.DID
2929}
30303131-func NewPublicClient(host string) *APIClient {
3131+// Creates a simple APIClient for the provided host. This is appropriate for use with unauthenticated ("public") atproto API endpoints, or to use as a base client to add authentication.
3232+//
3333+// Uses the default stdlib http.Client, and sets a default User-Agent.
3434+func NewAPIClient(host string) *APIClient {
3235 return &APIClient{
3336 Client: http.DefaultClient,
3437 Host: host,
···4245//
4346// Does not work with all API endpoints. For more control, use the Do() method with APIRequest.
4447func (c *APIClient) Get(ctx context.Context, endpoint syntax.NSID, params url.Values, out any) error {
4545- hdr := map[string][]string{
4646- "Accept": []string{"application/json"},
4747- }
4848- req := APIRequest{
4949- Method: http.MethodGet,
5050- Endpoint: endpoint,
5151- Body: nil,
5252- QueryParams: params,
5353- Headers: hdr,
5454- }
4848+4949+ req := NewAPIRequest(http.MethodGet, endpoint, nil)
5050+ req.QueryParams = params
5151+ req.Headers.Set("Accept", "application/json")
5252+5553 resp, err := c.Do(ctx, req)
5654 if err != nil {
5755 return err
···8482 if err != nil {
8583 return err
8684 }
8787- hdr := map[string][]string{
8888- "Accept": []string{"application/json"},
8989- "Content-Type": []string{"application/json"},
9090- }
9191- req := APIRequest{
9292- Method: http.MethodPost,
9393- Endpoint: endpoint,
9494- Body: bytes.NewReader(bodyJSON),
9595- QueryParams: nil,
9696- Headers: hdr,
9797- }
8585+8686+ req := NewAPIRequest(http.MethodPost, endpoint, bytes.NewReader(bodyJSON))
8787+ req.Headers.Set("Accept", "application/json")
8888+ req.Headers.Set("Content-Type", "application/json")
8989+9890 resp, err := c.Do(ctx, req)
9991 if err != nil {
10092 return err
···119111 return nil
120112}
121113122122-// Full-power method for atproto API requests.
114114+// Full-featured method for atproto API requests.
123115//
124116// NOTE: this does not currently parse error response JSON body, thought it might in the future.
125125-func (c *APIClient) Do(ctx context.Context, req APIRequest) (*http.Response, error) {
117117+func (c *APIClient) Do(ctx context.Context, req *APIRequest) (*http.Response, error) {
118118+119119+ if c.Client == nil {
120120+ c.Client = http.DefaultClient
121121+ }
122122+126123 httpReq, err := req.HTTPRequest(ctx, c.Host, c.Headers)
127124 if err != nil {
128125 return nil, err
129129- }
130130-131131- // NOTE: this updates the client object itself
132132- if c.Client == nil {
133133- c.Client = http.DefaultClient
134126 }
135127136128 var resp *http.Response
···149141//
150142// To configure service proxying without creating a copy, simply set the "Atproto-Proxy" header.
151143func (c *APIClient) WithService(ref string) *APIClient {
152152- hdr := make(http.Header)
153153- for k := range c.Headers {
154154- for _, v := range c.Headers.Values(k) {
155155- hdr.Add(k, v)
156156- }
157157- }
158158-144144+ hdr := c.Headers.Clone()
159145 hdr.Set("Atproto-Proxy", ref)
160146 out := APIClient{
161147 Client: c.Client,
+54-15
atproto/client/api_request.go
···11package client
2233import (
44+ "bytes"
45 "context"
56 "fmt"
67 "io"
···1920)
20212122type APIRequest struct {
2222- // HTTP method, eg "GET" (required)
2323+ // HTTP method as a string (eg "GET") (required)
2324 Method string
24252526 // atproto API endpoint, as NSID (required)
2627 Endpoint syntax.NSID
27282828- // optional request body. if this is provided, then 'Content-Type' header should be specified
2929- Body io.Reader
2929+ // Optional request body (may be nil). If this is provided, then 'Content-Type' header should be specified
3030+ Body io.ReadCloser
30313131- // XXX:
3232- //GetBody func() (io.ReadCloser, error)
3232+ // Optional function to return new reader for request body; used for retries. strongly recommended if Body is defined. Body still needs to be defined, even if this function is provided.
3333+ GetBody func() (io.ReadCloser, error)
33343434- // optional query parameters. These will be encoded as provided.
3535+ // Optional query parameters (field may be nil). These will be encoded as provided.
3536 QueryParams url.Values
36373737- // optional HTTP headers. Only the first value will be included for each header key ("Set" behavior).
3838+ // Optional HTTP headers (field bay be nil). Only the first value will be included for each header key ("Set" behavior).
3839 Headers http.Header
3940}
40414141-// Turns the API request in to an `http.Request`.
4242+// Initializes a new request struct. Initializes Headers and QueryParams so they can be manipulated immediately.
4343+//
4444+// If body is provided (it can be nil), will try to turn it in to the most retry-able form (and wrap as io.ReadCloser).
4545+func NewAPIRequest(method string, endpoint syntax.NSID, body io.Reader) *APIRequest {
4646+ req := APIRequest{
4747+ Method: method,
4848+ Endpoint: endpoint,
4949+ Headers: map[string][]string{},
5050+ QueryParams: map[string][]string{},
5151+ }
5252+5353+ // logic to turn "whatever io.Reader we are handed" in to something relatively re-tryable (using GetBody)
5454+ if body != nil {
5555+ switch v := body.(type) {
5656+ case *bytes.Buffer:
5757+ req.Body = io.NopCloser(v)
5858+ req.GetBody = func() (io.ReadCloser, error) {
5959+ return io.NopCloser(v), nil
6060+ }
6161+ case io.Seeker:
6262+ req.Body = io.NopCloser(body)
6363+ req.GetBody = func() (io.ReadCloser, error) {
6464+ v.Seek(0, 0)
6565+ return io.NopCloser(body), nil
6666+ }
6767+ case io.ReadCloser:
6868+ req.Body = v
6969+ case io.Reader:
7070+ req.Body = io.NopCloser(body)
7171+ }
7272+ }
7373+ return &req
7474+}
7575+7676+// Creates an `http.Request` for this API request.
4277//
4343-// `host` parameter should be a URL prefix: schema, hostname, port.
4444-// `headers` parameters are treated as client-level defaults. Only a single value is allowed per key ("Set" behavior), and will be clobbered by any request-level header values.
4545-func (r *APIRequest) HTTPRequest(ctx context.Context, host string, headers http.Header) (*http.Request, error) {
7878+// `host` parameter should be a URL prefix: schema, hostname, port (required)
7979+// `headers` parameters are treated as client-level defaults. Only a single value is allowed per key ("Set" behavior), and will be clobbered by any request-level header values. (optional; may be nil)
8080+func (r *APIRequest) HTTPRequest(ctx context.Context, host string, clientHeaders http.Header) (*http.Request, error) {
4681 u, err := url.Parse(host)
4782 if err != nil {
4883 return nil, err
···5893 }
5994 u.Path = "/xrpc/" + r.Endpoint.String()
6095 u.RawQuery = ""
6161- if r.QueryParams != nil {
9696+ if r.QueryParams != nil && len(r.QueryParams) > 0 {
6297 u.RawQuery = r.QueryParams.Encode()
6398 }
6499 httpReq, err := http.NewRequestWithContext(ctx, r.Method, u.String(), r.Body)
···66101 return nil, err
67102 }
68103104104+ if r.GetBody != nil {
105105+ httpReq.GetBody = r.GetBody
106106+ }
107107+69108 // first set default headers...
7070- if headers != nil {
7171- for k := range headers {
7272- httpReq.Header.Set(k, headers.Get(k))
109109+ if clientHeaders != nil {
110110+ for k := range clientHeaders {
111111+ httpReq.Header.Set(k, clientHeaders.Get(k))
73112 }
74113 }
75114
+14-7
atproto/client/password_auth.go
···3535 }
36363737 // on success, or most errors, just return HTTP response
3838- if resp.StatusCode != 400 || !strings.HasPrefix(resp.Header.Get("Content-Type"), "application/json") {
3838+ if resp.StatusCode != http.StatusBadRequest || !strings.HasPrefix(resp.Header.Get("Content-Type"), "application/json") {
3939 return resp, nil
4040 }
4141···5454 return nil, err
5555 }
56565757- // XXX: review "retry" logic (HTTP body, headers, etc)
5858- req.Header.Set("Authorization", "Bearer "+a.Session.AccessToken)
5959- resp, err = c.Do(req)
5757+ retry := req.Clone(req.Context())
5858+ if req.GetBody != nil {
5959+ retry.Body, err = req.GetBody()
6060+ if err != nil {
6161+ return nil, fmt.Errorf("API request retry GetBody failed: %w", err)
6262+ }
6363+ }
6464+6565+ retry.Header.Set("Authorization", "Bearer "+a.Session.AccessToken)
6666+ retryResp, err := c.Do(retry)
6067 if err != nil {
6168 return nil, err
6269 }
6363- // XXX: handle auth error here?
6464- return resp, err
7070+ // TODO: could handle auth failure as special error type here
7171+ return retryResp, err
6572}
66736774// TODO: need a "Logout" method as well? which takes the refresh token (not access token)
···127134 return nil, fmt.Errorf("account does not have PDS registered")
128135 }
129136130130- c := NewPublicClient(host)
137137+ c := NewAPIClient(host)
131138 reqBody := comatproto.ServerCreateSession_Input{
132139 Identifier: ident.DID.String(),
133140 Password: password,