blob: fb18ed99fcf90240d24a15b728806b14dd5cd589 [file] [log] [blame]
// Copyright 2017 The Goma Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package httprpc
import (
"bytes"
"compress/flate"
"compress/gzip"
"context"
"io"
"io/ioutil"
"net/http"
"go.opencensus.io/trace"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
)
// Client is httprpc client.
type Client struct {
*http.Client
// endpoint URL.
URL string
// ContentEncoding of the request. "identity", "gzip" or "deflate".
// "deflate" uses "deflate" compressed data (RFC1951) without
// zlib header, different from RFC7230 says, for histrical reason.
// default is "deflate" for backward compatibility.
// TODO: change default to gzip?
ContentEncoding string
}
func serializeToHTTPRequest(ctx context.Context, url string, req proto.Message, contentEncoding string) (*http.Request, error) {
ctx, span := trace.StartSpan(ctx, "go.chromium.org/goma/server/httprpc.serializedToHTTPRequest")
defer span.End()
reqMsg, err := proto.Marshal(req)
if err != nil {
return nil, err
}
buf := bytes.NewBuffer(nil)
switch contentEncoding {
case "identity":
buf.Write(reqMsg)
case "deflate":
// RFC7230 says deflate coding is "zlib" (RFC1950) containing
// "deflate" compressed data (RFC1951).
// but goma client just used "deflate" compressed data
// for "Content-Encoding: deflate" wrongly.
// TODO: use gzip
w, err := flate.NewWriter(buf, flate.BestSpeed)
if err != nil {
return nil, err
}
_, err = w.Write(reqMsg)
if err != nil {
return nil, err
}
err = w.Close()
if err != nil {
return nil, err
}
case "gzip":
w, err := gzip.NewWriterLevel(buf, gzip.BestSpeed)
if err != nil {
return nil, err
}
_, err = w.Write(reqMsg)
if err != nil {
return nil, err
}
err = w.Close()
if err != nil {
return nil, err
}
default:
return nil, status.Errorf(codes.InvalidArgument, "unsupported content-encoding: %s", contentEncoding)
}
len := int64(len(buf.Bytes()))
post, err := http.NewRequest("POST", url, bytes.NewReader(buf.Bytes()))
if err != nil {
return nil, err
}
post = post.WithContext(ctx)
post.Header.Set("Content-Type", "binary/x-protocol-buffer")
post.ContentLength = len
post.Header.Set("Accept-Encoding", "gzip, deflate")
post.Header.Set("Content-Encoding", contentEncoding)
return post, nil
}
func fromHTTPStatus(code int) codes.Code {
// go/http-canonical-mapping
if code >= http.StatusOK && code < http.StatusMultipleChoices {
return codes.OK
}
if code >= http.StatusMultipleChoices && code < http.StatusBadRequest {
return codes.Unknown
}
switch code {
case http.StatusBadRequest:
return codes.InvalidArgument
case http.StatusUnauthorized:
return codes.Unauthenticated
case http.StatusForbidden:
return codes.PermissionDenied
case http.StatusNotFound:
return codes.NotFound
case http.StatusConflict:
return codes.Aborted
case http.StatusRequestedRangeNotSatisfiable:
return codes.OutOfRange
case http.StatusTooManyRequests:
return codes.ResourceExhausted
case 499: // Client closed request
return codes.Canceled
case http.StatusNotImplemented:
return codes.Unimplemented
case http.StatusServiceUnavailable:
return codes.Unavailable
case http.StatusGatewayTimeout:
return codes.DeadlineExceeded
}
if code >= http.StatusBadRequest && code < http.StatusInternalServerError {
return codes.FailedPrecondition
}
if code >= http.StatusInternalServerError {
return codes.Internal
}
return codes.Unknown
}
func parseFromHTTPResponse(ctx context.Context, response *http.Response, resp proto.Message) error {
defer response.Body.Close()
ctx, span := trace.StartSpan(ctx, "go.chromium.org/goma/server/httprpc.parseFromHTTPResponse")
defer span.End()
span.AddAttributes(trace.Int64Attribute("status_code", int64(response.StatusCode)))
if response.StatusCode != 200 {
span.AddAttributes(trace.StringAttribute("goma_error", response.Header.Get("X-Goma-Error")))
b, err := ioutil.ReadAll(response.Body)
return status.Errorf(fromHTTPStatus(response.StatusCode), "%d: %s: %s: %v", response.StatusCode, response.Header.Get("X-Goma-Error"), string(b), err)
}
var r io.Reader = response.Body
switch response.Header.Get("Content-Encoding") {
case "identity", "":
case "gzip":
var err error
r, err = gzip.NewReader(response.Body)
if err != nil {
return err
}
case "deflate":
// RFC7230 says deflate coding is "zlib" (RFC1950) containing
// "deflate" compressed data (RFC1951).
// but goma client just used "deflate" compressed data
// for "Content-Encoding: deflate" wrongly.
r = flate.NewReader(response.Body)
default:
return status.Errorf(codes.InvalidArgument, "unknown content-encoding: %s", response.Header.Get("Content-Encoding"))
}
respMsg, err := ioutil.ReadAll(r)
if err != nil {
return err
}
err = proto.Unmarshal(respMsg, resp)
if err != nil {
return err
}
return nil
}
// Call calls remote services over http.
func (c *Client) Call(ctx context.Context, req proto.Message, resp proto.Message) error {
client := c.Client
if client == nil {
client = http.DefaultClient
}
ctx, span := trace.StartSpan(ctx, "go.chromium.org/goma/server/httprpc.Client.Call")
defer span.End()
contentEncoding := c.ContentEncoding
if contentEncoding == "" {
contentEncoding = "deflate"
}
post, err := serializeToHTTPRequest(ctx, c.URL, req, contentEncoding)
if err != nil {
return err
}
response, err := client.Do(post)
if err != nil {
return err
}
err = parseFromHTTPResponse(ctx, response, resp)
if err != nil {
return err
}
return nil
}
// Call calls remote services over http.
func Call(ctx context.Context, client *http.Client, url string, req proto.Message, resp proto.Message) error {
c := Client{
Client: client,
URL: url,
}
return c.Call(ctx, req, resp)
}