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 } } }
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) } }