func getQuery(req cmds.Request) (string, error) { query := url.Values{} for k, v := range req.Options() { str := fmt.Sprintf("%v", v) query.Set(k, str) } args := req.Arguments() argDefs := req.Command().Arguments argDefIndex := 0 for _, arg := range args { argDef := argDefs[argDefIndex] // skip ArgFiles for argDef.Type == cmds.ArgFile { argDefIndex++ argDef = argDefs[argDefIndex] } query.Add("arg", arg) if len(argDefs) > argDefIndex+1 { argDefIndex++ } } return query.Encode(), nil }
// read json objects off of the given stream, and write the objects out to // the 'out' channel func readStreamedJson(req cmds.Request, rr io.Reader, out chan<- interface{}) { defer close(out) dec := json.NewDecoder(rr) outputType := reflect.TypeOf(req.Command().Type) ctx := req.Context() for { v, err := decodeTypedVal(outputType, dec) if err != nil { if err != io.EOF { // log.Error(err) } return } select { case <-ctx.Done(): return case out <- v: } } }
func (c *client) Send(req cmds.Request) (cmds.Response, error) { if req.Context() == nil { // log.Warningf("no context set in request") if err := req.SetRootContext(context.TODO()); err != nil { return nil, err } } // save user-provided encoding previousUserProvidedEncoding, found, err := req.Option(cmds.EncShort).String() if err != nil { return nil, err } // override with json to send to server req.SetOption(cmds.EncShort, cmds.JSON) // stream channel output req.SetOption(cmds.ChanOpt, "true") query, err := getQuery(req) if err != nil { return nil, err } var fileReader *MultiFileReader var reader io.Reader if req.Files() != nil { fileReader = NewMultiFileReader(req.Files(), true) reader = fileReader } else { // if we have no file data, use an empty Reader // (http.NewRequest panics when a nil Reader is used) reader = strings.NewReader("") } path := strings.Join(req.Path(), "/") url := fmt.Sprintf(ApiUrlFormat, c.serverAddress, ApiPath, path, query) httpReq, err := http.NewRequest("POST", url, reader) if err != nil { return nil, err } // TODO extract string consts? if fileReader != nil { httpReq.Header.Set(contentTypeHeader, "multipart/form-data; boundary="+fileReader.Boundary()) httpReq.Header.Set(contentDispHeader, "form-data: name=\"files\"") } else { httpReq.Header.Set(contentTypeHeader, applicationOctetStream) } ec := make(chan error, 1) rc := make(chan cmds.Response, 1) dc := req.Context().Done() go func() { httpRes, err := c.httpClient.Do(httpReq) if err != nil { ec <- err return } // using the overridden JSON encoding in request res, err := getResponse(httpRes, req) if err != nil { ec <- err return } rc <- res }() for { select { case <-dc: // log.Debug("Context cancelled, cancelling HTTP request...") tr := http.DefaultTransport.(*http.Transport) tr.CancelRequest(httpReq) dc = nil // Wait for ec or rc case err := <-ec: return nil, err case res := <-rc: if found && len(previousUserProvidedEncoding) > 0 { // reset to user provided encoding after sending request // NB: if user has provided an encoding but it is the empty string, // still leave it as JSON. req.SetOption(cmds.EncShort, previousUserProvidedEncoding) } return res, nil } } }
// getResponse decodes a http.Response to create a cmds.Response func getResponse(httpRes *http.Response, req cmds.Request) (cmds.Response, error) { var err error res := cmds.NewResponse(req) contentType := httpRes.Header.Get(contentTypeHeader) contentType = strings.Split(contentType, ";")[0] lengthHeader := httpRes.Header.Get(contentLengthHeader) if len(lengthHeader) > 0 { length, err := strconv.ParseUint(lengthHeader, 10, 64) if err != nil { return nil, err } res.SetLength(length) } rr := &httpResponseReader{httpRes} res.SetCloser(rr) if contentType != applicationJson { // for all non json output types, just stream back the output res.SetOutput(rr) return res, nil } else if len(httpRes.Header.Get(channelHeader)) > 0 { // if output is coming from a channel, decode each chunk outChan := make(chan interface{}) go readStreamedJson(req, rr, outChan) res.SetOutput((<-chan interface{})(outChan)) return res, nil } dec := json.NewDecoder(rr) // If we ran into an error if httpRes.StatusCode >= http.StatusBadRequest { e := cmds.Error{} switch { case httpRes.StatusCode == http.StatusNotFound: // handle 404s e.Message = "Command not found." e.Code = cmds.ErrClient case contentType == plainText: // handle non-marshalled errors mes, err := ioutil.ReadAll(rr) if err != nil { return nil, err } e.Message = string(mes) e.Code = cmds.ErrNormal default: // handle marshalled errors err = dec.Decode(&e) if err != nil { return nil, err } } res.SetError(e, e.Code) return res, nil } outputType := reflect.TypeOf(req.Command().Type) v, err := decodeTypedVal(outputType, dec) if err != nil && err != io.EOF { return nil, err } res.SetOutput(v) return res, nil }
func sendResponse(w http.ResponseWriter, r *http.Request, res cmds.Response, req cmds.Request) { mime, err := guessMimeType(res) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } status := http.StatusOK // if response contains an error, write an HTTP error status code if e := res.Error(); e != nil { if e.Code == cmds.ErrClient { status = http.StatusBadRequest } else { status = http.StatusInternalServerError } // NOTE: The error will actually be written out by the reader below } out, err := res.Reader() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } h := w.Header() if res.Length() > 0 { h.Set(contentLengthHeader, strconv.FormatUint(res.Length(), 10)) } if _, ok := res.Output().(io.Reader); ok { // we don't set the Content-Type for streams, so that browsers can MIME-sniff the type themselves // we set this header so clients have a way to know this is an output stream // (not marshalled command output) mime = "" h.Set(streamHeader, "1") } // if output is a channel and user requested streaming channels, // use chunk copier for the output _, isChan := res.Output().(chan interface{}) if !isChan { _, isChan = res.Output().(<-chan interface{}) } streamChans, _, _ := req.Option("stream-channels").Bool() if isChan { h.Set(channelHeader, "1") if streamChans { // streaming output from a channel will always be json objects mime = applicationJson } } if mime != "" { h.Set(contentTypeHeader, mime) } h.Set(transferEncodingHeader, "chunked") if r.Method == "HEAD" { // after all the headers. return } if err := writeResponse(status, w, out); err != nil { if strings.Contains(err.Error(), "broken pipe") { // log.Info("client disconnect while writing stream ", err) return } // log.Error("error while writing stream ", err) } }