kubo/client/httpapi/response.go
2019-02-04 19:44:48 +01:00

161 lines
3.3 KiB
Go

package httpapi
import (
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"strings"
files "github.com/ipfs/go-ipfs-files"
)
type trailerReader struct {
resp *http.Response
}
func (r *trailerReader) Read(b []byte) (int, error) {
n, err := r.resp.Body.Read(b)
if err != nil {
if e := r.resp.Trailer.Get("X-Stream-Error"); e != "" {
err = errors.New(e)
}
}
return n, err
}
func (r *trailerReader) Close() error {
return r.resp.Body.Close()
}
type Response struct {
Output io.ReadCloser
Error *Error
}
func (r *Response) Close() error {
if r.Output != nil {
// always drain output (response body)
//ioutil.ReadAll(r.Output) // TODO: might not be a good idea in case there is a lot of data
return r.Output.Close()
}
return nil
}
func (r *Response) Decode(dec interface{}) error {
defer r.Close()
if r.Error != nil {
return r.Error
}
n := 0
var err error
for {
err = json.NewDecoder(r.Output).Decode(dec)
if err != nil {
break
}
n++
}
if n > 0 && err == io.EOF {
err = nil
}
return err
}
type Error struct {
Command string
Message string
Code int
}
func (e *Error) Error() string {
var out string
if e.Code != 0 {
out = fmt.Sprintf("%s%d: ", out, e.Code)
}
return out + e.Message
}
func (r *Request) Send(c *http.Client) (*Response, error) {
url := r.getURL()
req, err := http.NewRequest("POST", url, r.Body)
if err != nil {
return nil, err
}
// Add any headers that were supplied via the RequestBuilder.
for k, v := range r.Headers {
req.Header.Add(k, v)
}
if fr, ok := r.Body.(*files.MultiFileReader); ok {
req.Header.Set("Content-Type", "multipart/form-data; boundary="+fr.Boundary())
req.Header.Set("Content-Disposition", "form-data: name=\"files\"")
}
resp, err := c.Do(req)
if err != nil {
return nil, err
}
contentType := resp.Header.Get("Content-Type")
parts := strings.Split(contentType, ";")
contentType = parts[0]
nresp := new(Response)
nresp.Output = &trailerReader{resp}
if resp.StatusCode >= http.StatusBadRequest {
e := &Error{
Command: r.Command,
}
switch {
case resp.StatusCode == http.StatusNotFound:
e.Message = "command not found"
case contentType == "text/plain":
out, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Fprintf(os.Stderr, "ipfs-shell: warning! response (%d) read error: %s\n", resp.StatusCode, err)
}
e.Message = string(out)
case contentType == "application/json":
if err = json.NewDecoder(resp.Body).Decode(e); err != nil {
fmt.Fprintf(os.Stderr, "ipfs-shell: warning! response (%d) unmarshall error: %s\n", resp.StatusCode, err)
}
default:
fmt.Fprintf(os.Stderr, "ipfs-shell: warning! unhandled response (%d) encoding: %s", resp.StatusCode, contentType)
out, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Fprintf(os.Stderr, "ipfs-shell: response (%d) read error: %s\n", resp.StatusCode, err)
}
e.Message = fmt.Sprintf("unknown ipfs-shell error encoding: %q - %q", contentType, out)
}
nresp.Error = e
nresp.Output = nil
// drain body and close
ioutil.ReadAll(resp.Body)
resp.Body.Close()
}
return nresp, nil
}
func (r *Request) getURL() string {
values := make(url.Values)
for _, arg := range r.Args {
values.Add("arg", arg)
}
for k, v := range r.Opts {
values.Add(k, v)
}
return fmt.Sprintf("%s/%s?%s", r.ApiBase, r.Command, values.Encode())
}