blob: a0fc5175a384625b78c22a36cceb4df6db88f569 [file] [log] [blame]
// Copyright 2016 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package prpc
// This file implements encoding of RPC results to HTTP responses.
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"sort"
"strconv"
"github.com/golang/protobuf/jsonpb"
"github.com/golang/protobuf/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/grpc/grpcutil"
)
const (
headerAccept = "Accept"
)
// responseFormat returns the format to be used in a response.
// Can return only FormatBinary (preferred), FormatJSONPB or FormatText.
// In case of an error, format is undefined.
func responseFormat(acceptHeader string) (Format, *protocolError) {
if acceptHeader == "" {
return FormatBinary, nil
}
parsed, err := parseAccept(acceptHeader)
if err != nil {
return FormatBinary, errorf(http.StatusBadRequest, "Accept header: %s", err)
}
formats := make(acceptFormatSlice, 0, len(parsed))
for _, at := range parsed {
f, err := FormatFromMediaType(at.MediaType, at.MediaTypeParams)
if err != nil {
// Ignore invalid format. Check further.
continue
}
formats = append(formats, acceptFormat{f, at.QualityFactor})
}
if len(formats) == 0 {
return FormatBinary, errorf(
http.StatusNotAcceptable,
"Accept header: specified media types are not not supported. Supported types: %q, %q, %q, %q.",
FormatBinary.MediaType(),
FormatJSONPB.MediaType(),
FormatText.MediaType(),
ContentTypeJSON,
)
}
sort.Sort(formats) // order by quality factor and format preference.
return formats[0].Format, nil
}
// writeMessage writes msg to w in the specified format.
// c is used to log errors.
// panics if msg is nil.
func writeMessage(c context.Context, w http.ResponseWriter, msg proto.Message, format Format) {
if msg == nil {
panic("msg is nil")
}
var body []byte
var err error
switch format {
case FormatBinary:
body, err = proto.Marshal(msg)
case FormatJSONPB:
var buf bytes.Buffer
buf.WriteString(JSONPBPrefix)
m := jsonpb.Marshaler{}
err = m.Marshal(&buf, msg)
if err == nil {
_, err = buf.WriteRune('\n')
}
body = buf.Bytes()
case FormatText:
var buf bytes.Buffer
err = proto.MarshalText(&buf, msg)
body = buf.Bytes()
default:
panic(fmt.Errorf("impossible: invalid format %d", format))
}
if err != nil {
writeError(c, w, withCode(err, codes.Internal))
return
}
w.Header().Set(HeaderGRPCCode, strconv.Itoa(int(codes.OK)))
w.Header().Set(headerContentType, format.MediaType())
w.Header().Set("X-Content-Type-Options", "nosniff")
if _, err := w.Write(body); err != nil {
logging.WithError(err).Errorf(c, "prpc: failed to write response body")
}
}
// errorCode returns a most appropriate gRPC code for an error
func errorCode(err error) codes.Code {
switch errors.Unwrap(err) {
case context.DeadlineExceeded:
return codes.DeadlineExceeded
case context.Canceled:
return codes.Canceled
default:
return grpc.Code(err)
}
}
// writeError writes err to w and logs it.
func writeError(c context.Context, w http.ResponseWriter, err error) {
var code codes.Code
var httpStatus int
var msg string
if perr, ok := err.(*protocolError); ok {
code = codes.InvalidArgument
msg = perr.err.Error()
httpStatus = perr.status
} else {
code = errorCode(err)
msg = grpc.ErrorDesc(err)
httpStatus = grpcutil.CodeStatus(code)
}
body := msg
level := logging.Warning
if httpStatus >= 500 {
level = logging.Error
// Hide potential implementation details from the user.
body = http.StatusText(httpStatus)
}
logging.Logf(c, level, "prpc: responding with %s error: %s", code, msg)
w.Header().Set(HeaderGRPCCode, strconv.Itoa(int(code)))
w.Header().Set(headerContentType, "text/plain")
w.WriteHeader(httpStatus)
if _, err := io.WriteString(w, body); err != nil {
logging.WithError(err).Errorf(c, "prpc: failed to write response body")
// The header is already written. There is nothing more we can do.
return
}
io.WriteString(w, "\n")
}
func withCode(err error, c codes.Code) error {
return status.Error(c, err.Error())
}