// checkHTTPStatus examines an HTTP response and returns an error if // it is not successful. func checkHTTPStatus(resp *http.Response) error { if len(resp.Status) > 0 && resp.Status[0] == '2' { return nil } // Always collect the entire body; we will need it as a fallback // and can only parse it once. var body []byte var err error if resp.Body != nil { body, err = ioutil.ReadAll(resp.Body) if err != nil { return err } } // Take a shot at decoding it as a better error var errResp restdata.ErrorResponse contentType := resp.Header.Get("Content-Type") err2 := restdata.Decode(contentType, bytes.NewReader(body), &errResp) if err2 == nil { // Given that we decoded that successfully, return the // server-provided error return errResp.ToError() } return ErrorHTTP{Response: resp, Body: string(body)} }
// Do performs some HTTP action. If in is non-nil, the request data is // serialized and sent as the body of, for instance, a POST request. // If out is non-nil, the response data (if any) is deserialized into // this object, which must be of pointer type. func (r *resource) Do(method string, url *url.URL, in, out interface{}) (err error) { json := &codec.JsonHandle{} // Set up the body as serialized JSON, if there is one var body io.Reader if in != nil { reader, writer := io.Pipe() encoder := codec.NewEncoder(writer, json) finished := make(chan error) go func() { err := encoder.Encode(in) err = firstError(err, writer.Close()) finished <- err }() defer func() { err = firstError(err, <-finished) }() body = reader } // Create the request and set headers req, err := http.NewRequest(method, url.String(), body) if err != nil { return err } if in != nil { req.Header.Set("Content-Type", restdata.V1JSONMediaType) } if out != nil { req.Header.Set("Accept", restdata.V1JSONMediaType) } // Actually do the request resp, err := http.DefaultClient.Do(req) if err != nil { return err } // If the response included a body, clean up afterwards if resp.Body != nil { defer func() { err = firstError(err, resp.Body.Close()) }() } // Check the response code if err = checkHTTPStatus(resp); err != nil { return err } // If there is both a body and a requested output, // decode it if resp.Body != nil && out != nil { contentType := resp.Header.Get("Content-Type") err = restdata.Decode(contentType, resp.Body, out) } return err // may be nil }
func (h *resourceHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { var ( ctx *context in, out interface{} err error status int responseType string ) // Recover from panics by sending an HTTP error. defer func() { if recovered := recover(); recovered != nil { response := restdata.ErrorResponse{} response.FromPanic(recovered) writeAResponse(resp, http.StatusInternalServerError, restdata.V1JSONMediaType, response, toJSON) } }() // Start by trying to come up with a response type, even before // trying to parse the input. This determines what format an // error message could be sent back as. if err == nil { // Errors here by default are in the header setup status = http.StatusBadRequest responseType, err = negotiateResponse(req) if err != nil { // Gotta pick something responseType = restdata.V1JSONMediaType } } // Get bits from URL parameters if err == nil { ctx, err = h.Context(req) } // Read the (JSON?) body, if it's there if err == nil && (req.Method == "PUT" || req.Method == "POST") { // Make a new object of the same type as h.In in = reflect.Zero(reflect.TypeOf(h.Representation)).Interface() // Then decode the message body into that object contentType := req.Header.Get("Content-Type") err = restdata.Decode(contentType, req.Body, &in) } // Actually call the handler method if err == nil { // We will return this if the method is unexpected or // we don't have a handler for it err = errMethodNotAllowed{Method: req.Method} // If anything else goes wrong here, it's an error in // client code status = http.StatusInternalServerError switch req.Method { case "GET", "HEAD": if h.Get != nil { out, err = h.Get(ctx) } case "PUT": if h.Put != nil { out, err = h.Put(ctx, in) } case "POST": if h.Post != nil { out, err = h.Post(ctx, in) } case "DELETE": if h.Delete != nil { out, err = h.Delete(ctx) } } } // Fix up the final result based on what we know. if err != nil { // Pick a better status code if we know of one if errS, hasStatus := err.(restdata.ErrorStatus); hasStatus { status = errS.HTTPStatus() } resp := restdata.ErrorResponse{Error: "error", Message: err.Error()} resp.FromError(err) // Remap well-known coordinate errors out = resp } else if out == nil { status = http.StatusNoContent } else if created, isCreated := out.(responseCreated); isCreated { status = http.StatusCreated if created.Location != "" { resp.Header().Set("Location", created.Location) } if req.Method == "HEAD" { out = nil } else { out = created.Body } } else { status = http.StatusOK if req.Method == "HEAD" { out = nil } } // Come up with a function to write the response. If setting // this up fails it could produce another error. :-/ It is // also possible for the actual writer to fail, but by the // point this happens we've already written an HTTP status // line, so we're not necessarily doing better than panicking. responseWriters := map[string]func(interface{}) ([]byte, error){ restdata.V1JSONMediaType: toJSON, } responseWriter, understood := responseWriters[typeMap[responseType]] if !understood { // We shouldn't get here, because it implies response // type negotiation failed...but here we are responseWriter = responseWriters[restdata.V1JSONMediaType] status = http.StatusInternalServerError out = restdata.ErrorResponse{Error: "error", Message: "Invalid response type " + responseType} } writeAResponse(resp, status, responseType, out, responseWriter) }