Skip to content

Commit

Permalink
Request options, better errors
Browse files Browse the repository at this point in the history
  • Loading branch information
marcinwyszynski committed Feb 3, 2023
1 parent a77b50e commit d3a0cfc
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 20 deletions.
46 changes: 46 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package graphql

import "fmt"

// RequestError represents an error building a request.
type RequestError struct{ Err error }

func (e *RequestError) Error() string { return fmt.Sprintf("request error: %v", e.Err) }
func (e *RequestError) Unwrap() error { return e.Err }

// OptionError represents an error modifiying a request.
type OptionError struct{ Err error }

func (e *OptionError) Error() string { return fmt.Sprintf("request option error: %v", e.Err) }
func (e *OptionError) Unwrap() error { return e.Err }

// ResponseError represents a response error, either with getting a response
// from the server (eg. network error), or reading its body.
type ResponseError struct{ Err error }

func (e *ResponseError) Error() string { return fmt.Sprintf("request option error: %v", e.Err) }
func (e *ResponseError) Unwrap() error { return e.Err }

// ServerError indicates that the server returned a response but it was not what
// we consider a successful one.
type ServerError struct {
Body []byte
Status string
}

func (e *ServerError) Error() string {
return fmt.Sprintf("non-200 OK status code: %v body: %q", e.Status, e.Body)
}

// BodyError indicates that the server responded with the right status code but
// the body was unexpected and it did not parse as a valid GraphQL response.
type BodyError struct {
Body []byte
Err error
}

func (e *BodyError) Error() string {
return fmt.Sprintf("could not parse the body: %v, body: %q", e.Err, e.Body)
}

func (e *BodyError) Unwrap() error { return e.Err }
65 changes: 45 additions & 20 deletions graphql.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"

Expand All @@ -14,45 +13,48 @@ import (

// Client is a GraphQL client.
type Client struct {
url string // GraphQL server URL.
httpClient *http.Client
url string // GraphQL server URL.
httpClient *http.Client
requestOptions []RequestOption
}

// NewClient creates a GraphQL client targeting the specified GraphQL server URL.
// If httpClient is nil, then http.DefaultClient is used.
func NewClient(url string, httpClient *http.Client) *Client {
func NewClient(url string, httpClient *http.Client, opts ...RequestOption) *Client {
if httpClient == nil {
httpClient = http.DefaultClient
}
return &Client{
url: url,
httpClient: httpClient,
url: url,
httpClient: httpClient,
requestOptions: opts,
}
}

// Query executes a single GraphQL query request,
// with a query derived from q, populating the response into it.
// q should be a pointer to struct that corresponds to the GraphQL schema.
func (c *Client) Query(ctx context.Context, q interface{}, variables map[string]interface{}) error {
return c.do(ctx, queryOperation, q, variables)
func (c *Client) Query(ctx context.Context, q interface{}, variables map[string]interface{}, opts ...RequestOption) error {
return c.do(ctx, queryOperation, q, variables, opts)
}

// Mutate executes a single GraphQL mutation request,
// with a mutation derived from m, populating the response into it.
// m should be a pointer to struct that corresponds to the GraphQL schema.
func (c *Client) Mutate(ctx context.Context, m interface{}, variables map[string]interface{}) error {
return c.do(ctx, mutationOperation, m, variables)
func (c *Client) Mutate(ctx context.Context, m interface{}, variables map[string]interface{}, opts ...RequestOption) error {
return c.do(ctx, mutationOperation, m, variables, opts)
}

// do executes a single GraphQL operation.
func (c *Client) do(ctx context.Context, op operationType, v interface{}, variables map[string]interface{}) error {
func (c *Client) do(ctx context.Context, op operationType, v interface{}, variables map[string]interface{}, opts []RequestOption) error {
var query string
switch op {
case queryOperation:
query = constructQuery(v, variables)
case mutationOperation:
query = constructMutation(v, variables)
}

in := struct {
Query string `json:"query"`
Variables map[string]interface{} `json:"variables,omitempty"`
Expand All @@ -65,32 +67,55 @@ func (c *Client) do(ctx context.Context, op operationType, v interface{}, variab
if err != nil {
return err
}
resp, err := ctxhttp.Post(ctx, c.httpClient, c.url, "application/json", &buf)

req, err := http.NewRequest(http.MethodPost, c.url, &buf)
if err != nil {
return err
return &RequestError{Err: err}
}
req.Header.Set("Content-Type", "application/json")

var allOpts []RequestOption
allOpts = append(allOpts, c.requestOptions...)
allOpts = append(allOpts, opts...)

for _, opt := range allOpts {
if err := opt(req); err != nil {
return &OptionError{Err: err}
}
}

resp, err := ctxhttp.Do(ctx, c.httpClient, req)
if err != nil {
return &ResponseError{Err: err}
}
defer resp.Body.Close()

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return &ResponseError{Err: err}
}

if resp.StatusCode != http.StatusOK {
body, _ := ioutil.ReadAll(resp.Body)
return fmt.Errorf("non-200 OK status code: %v body: %q", resp.Status, body)
return &ServerError{
Body: body,
Status: resp.Status,
}
}

var out struct {
Data *json.RawMessage
Errors errors
Extensions interface{}
}

err = json.NewDecoder(resp.Body).Decode(&out)
if err != nil {
// TODO: Consider including response body in returned error, if deemed helpful.
return err
return &BodyError{Err: err, Body: body}
}

if out.Data != nil {
err := jsonutil.UnmarshalGraphQL(*out.Data, v)
if err != nil {
// TODO: Consider including response body in returned error, if deemed helpful.
return err
return &BodyError{Err: err, Body: body}
}
}

Expand Down
15 changes: 15 additions & 0 deletions request_option.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package graphql

import "net/http"

// RequestOption allows you to modify the request before sending it to the
// server.
type RequestOption func(*http.Request) error

// WithHeader sets a header on the request.
func WithHeader(key, value string) RequestOption {
return func(r *http.Request) error {
r.Header.Set(key, value)
return nil
}
}

0 comments on commit d3a0cfc

Please sign in to comment.