| Index: server/prpc/encoding.go
 | 
| diff --git a/server/prpc/encoding.go b/server/prpc/encoding.go
 | 
| index 2788e2e29d1d8b4a2a4b50380368de5e54399482..46dca855aae56e7c7ab37c85e182a7baa6d93230 100644
 | 
| --- a/server/prpc/encoding.go
 | 
| +++ b/server/prpc/encoding.go
 | 
| @@ -8,7 +8,6 @@ package prpc
 | 
|  
 | 
|  import (
 | 
|  	"bytes"
 | 
| -	"io"
 | 
|  	"net/http"
 | 
|  	"sort"
 | 
|  
 | 
| @@ -17,18 +16,17 @@ import (
 | 
|  	"golang.org/x/net/context"
 | 
|  	"google.golang.org/grpc"
 | 
|  	"google.golang.org/grpc/codes"
 | 
| -
 | 
| -	"github.com/luci/luci-go/common/logging"
 | 
|  )
 | 
|  
 | 
|  const (
 | 
|  	headerAccept = "Accept"
 | 
| +	csrfPrefix   = ")]}'\n"
 | 
|  )
 | 
|  
 | 
|  // 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 and the error has an HTTP status.
 | 
| -func responseFormat(acceptHeader string) (format, *httpError) {
 | 
| +// In case of an error, format is undefined.
 | 
| +func responseFormat(acceptHeader string) (format, *protocolError) {
 | 
|  	if acceptHeader == "" {
 | 
|  		return formatBinary, nil
 | 
|  	}
 | 
| @@ -37,7 +35,6 @@ func responseFormat(acceptHeader string) (format, *httpError) {
 | 
|  	if err != nil {
 | 
|  		return formatBinary, errorf(http.StatusBadRequest, "Accept header: %s", err)
 | 
|  	}
 | 
| -	assert(len(parsed) > 0)
 | 
|  	formats := make(acceptFormatSlice, 0, len(parsed))
 | 
|  	for _, at := range parsed {
 | 
|  		f, err := parseFormat(at.MediaType, at.MediaTypeParams)
 | 
| @@ -53,14 +50,10 @@ func responseFormat(acceptHeader string) (format, *httpError) {
 | 
|  		case formatUnspecified:
 | 
|  			f = formatBinary // prefer binary
 | 
|  
 | 
| -		case formatUnrecognized:
 | 
| -			continue
 | 
| -
 | 
|  		default:
 | 
| -			panicf("cannot happen")
 | 
| +			continue
 | 
|  		}
 | 
|  
 | 
| -		assert(f == formatBinary || f == formatJSONPB || f == formatText)
 | 
|  		formats = append(formats, acceptFormat{f, at.QualityFactor})
 | 
|  	}
 | 
|  	if len(formats) == 0 {
 | 
| @@ -77,41 +70,61 @@ func responseFormat(acceptHeader string) (format, *httpError) {
 | 
|  	return formats[0].Format, nil
 | 
|  }
 | 
|  
 | 
| -// writeMessage writes a protobuf message to response in the specified format.
 | 
| -func writeMessage(w http.ResponseWriter, msg proto.Message, format format) error {
 | 
| +// respondMessage encodes msg to a response in the specified format.
 | 
| +func respondMessage(msg proto.Message, format format) *response {
 | 
|  	if msg == nil {
 | 
| -		panic("msg is nil")
 | 
| +		return errResponse(codes.Internal, 0, "pRPC: responseMessage: msg is nil")
 | 
|  	}
 | 
| -	var (
 | 
| -		contentType string
 | 
| -		res         []byte
 | 
| -		err         error
 | 
| -	)
 | 
| +	res := response{header: http.Header{}}
 | 
| +	var err error
 | 
|  	switch format {
 | 
|  	case formatBinary:
 | 
| -		contentType = mtPRPCBinary
 | 
| -		res, err = proto.Marshal(msg)
 | 
| +		res.header.Set(headerContentType, mtPRPCBinary)
 | 
| +		res.body, err = proto.Marshal(msg)
 | 
|  
 | 
|  	case formatJSONPB:
 | 
| -		contentType = mtPRPCJSNOPB
 | 
| -		m := jsonpb.Marshaler{Indent: "\t"}
 | 
| +		res.header.Set(headerContentType, mtPRPCJSNOPB)
 | 
|  		var buf bytes.Buffer
 | 
| +		buf.WriteString(csrfPrefix)
 | 
| +		m := jsonpb.Marshaler{}
 | 
|  		err = m.Marshal(&buf, msg)
 | 
| -		buf.WriteString("\n")
 | 
| -		res = buf.Bytes()
 | 
| +		res.body = buf.Bytes()
 | 
| +		res.newLine = true
 | 
|  
 | 
|  	case formatText:
 | 
| -		contentType = mtPRPCText
 | 
| +		res.header.Set(headerContentType, mtPRPCText)
 | 
|  		var buf bytes.Buffer
 | 
|  		err = proto.MarshalText(&buf, msg)
 | 
| -		res = buf.Bytes()
 | 
| +		res.body = buf.Bytes()
 | 
| +
 | 
| +	default:
 | 
| +		return errResponse(codes.Internal, 0, "pRPC: responseMessage: invalid format %s", format)
 | 
| +
 | 
|  	}
 | 
|  	if err != nil {
 | 
| -		return err
 | 
| +		return errResponse(codes.Internal, 0, err.Error())
 | 
| +	}
 | 
| +
 | 
| +	return &res
 | 
| +}
 | 
| +
 | 
| +// respondProtocolError creates a response for a pRPC protocol error.
 | 
| +func respondProtocolError(err *protocolError) *response {
 | 
| +	return errResponse(codes.InvalidArgument, err.status, err.err.Error())
 | 
| +}
 | 
| +
 | 
| +// errorCode returns a most appropriate gRPC code for an error
 | 
| +func errorCode(err error) codes.Code {
 | 
| +	switch err {
 | 
| +	case context.DeadlineExceeded:
 | 
| +		return codes.DeadlineExceeded
 | 
| +
 | 
| +	case context.Canceled:
 | 
| +		return codes.Canceled
 | 
| +
 | 
| +	default:
 | 
| +		return grpc.Code(err)
 | 
|  	}
 | 
| -	w.Header().Set(headerContentType, contentType)
 | 
| -	_, err = w.Write(res)
 | 
| -	return err
 | 
|  }
 | 
|  
 | 
|  // codeToStatus maps gRPC codes to HTTP statuses.
 | 
| @@ -121,7 +134,6 @@ func writeMessage(w http.ResponseWriter, msg proto.Message, format format) error
 | 
|  var codeToStatus = map[codes.Code]int{
 | 
|  	codes.OK:                 http.StatusOK,
 | 
|  	codes.Canceled:           http.StatusNoContent,
 | 
| -	codes.Unknown:            http.StatusInternalServerError,
 | 
|  	codes.InvalidArgument:    http.StatusBadRequest,
 | 
|  	codes.DeadlineExceeded:   http.StatusServiceUnavailable,
 | 
|  	codes.NotFound:           http.StatusNotFound,
 | 
| @@ -130,78 +142,16 @@ var codeToStatus = map[codes.Code]int{
 | 
|  	codes.Unauthenticated:    http.StatusUnauthorized,
 | 
|  	codes.ResourceExhausted:  http.StatusServiceUnavailable,
 | 
|  	codes.FailedPrecondition: http.StatusPreconditionFailed,
 | 
| -	codes.Aborted:            http.StatusInternalServerError,
 | 
|  	codes.OutOfRange:         http.StatusBadRequest,
 | 
|  	codes.Unimplemented:      http.StatusNotImplemented,
 | 
| -	codes.Internal:           http.StatusInternalServerError,
 | 
|  	codes.Unavailable:        http.StatusServiceUnavailable,
 | 
| -	codes.DataLoss:           http.StatusInternalServerError,
 | 
| -}
 | 
| -
 | 
| -// ErrorStatus returns HTTP status for an error.
 | 
| -// In particular, it maps gRPC codes to HTTP statuses.
 | 
| -// Status of nil is 200.
 | 
| -//
 | 
| -// See also grpc.Code.
 | 
| -func ErrorStatus(err error) int {
 | 
| -	if err, ok := err.(*httpError); ok {
 | 
| -		return err.status
 | 
| -	}
 | 
| -
 | 
| -	status, ok := codeToStatus[grpc.Code(err)]
 | 
| -	if !ok {
 | 
| -		status = http.StatusInternalServerError
 | 
| -	}
 | 
| -	return status
 | 
| -}
 | 
| -
 | 
| -// ErrorDesc returns the error description of err if it was produced by pRPC or gRPC.
 | 
| -// Otherwise, it returns err.Error() or empty string when err is nil.
 | 
| -//
 | 
| -// See also grpc.ErrorDesc.
 | 
| -func ErrorDesc(err error) string {
 | 
| -	if err == nil {
 | 
| -		return ""
 | 
| -	}
 | 
| -	if e, ok := err.(*httpError); ok {
 | 
| -		err = e.err
 | 
| -	}
 | 
| -	return grpc.ErrorDesc(err)
 | 
| -}
 | 
| -
 | 
| -// writeError writes an error to an HTTP response.
 | 
| -//
 | 
| -// HTTP status is determined by ErrorStatus.
 | 
| -// If it is http.StatusInternalServerError, prints only "Internal server error",
 | 
| -// otherwise uses ErrorDesc.
 | 
| -//
 | 
| -// Logs all errors with status >= 500.
 | 
| -func writeError(c context.Context, w http.ResponseWriter, err error) {
 | 
| -	if err == nil {
 | 
| -		panic("err is nil")
 | 
| -	}
 | 
| -
 | 
| -	status := ErrorStatus(err)
 | 
| -	if status >= 500 {
 | 
| -		logging.Errorf(c, "HTTP %d: %s", status, ErrorDesc(err))
 | 
| -	}
 | 
| -
 | 
| -	w.Header().Set(headerContentType, "text/plain")
 | 
| -	w.WriteHeader(status)
 | 
| -
 | 
| -	var body string
 | 
| -	if status == http.StatusInternalServerError {
 | 
| -		body = "Internal server error"
 | 
| -	} else {
 | 
| -		body = ErrorDesc(err)
 | 
| -	}
 | 
| -	if _, err := io.WriteString(w, body+"\n"); err != nil {
 | 
| -		logging.Errorf(c, "could not write error: %s", err)
 | 
| -	}
 | 
|  }
 | 
|  
 | 
| -func assert(condition bool) {
 | 
| -	if !condition {
 | 
| -		panicf("assertion failed")
 | 
| +// codeStatus maps gRPC codes to HTTP status codes.
 | 
| +// Falls back to http.StatusInternalServerError.
 | 
| +func codeStatus(code codes.Code) int {
 | 
| +	if status, ok := codeToStatus[code]; ok {
 | 
| +		return status
 | 
|  	}
 | 
| +	return http.StatusInternalServerError
 | 
|  }
 | 
| 
 |