Chromium Code Reviews| OLD | NEW | 
|---|---|
| 1 // Copyright 2016 The Chromium Authors. All rights reserved. | 1 // Copyright 2016 The Chromium Authors. All rights reserved. | 
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be | 
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. | 
| 4 | 4 | 
| 5 package prpc | 5 package prpc | 
| 6 | 6 | 
| 7 // This file implements encoding of RPC results to HTTP responses. | 7 // This file implements encoding of RPC results to HTTP responses. | 
| 8 | 8 | 
| 9 import ( | 9 import ( | 
| 10 "bytes" | 10 "bytes" | 
| 11 "io" | |
| 12 "net/http" | 11 "net/http" | 
| 13 "sort" | 12 "sort" | 
| 14 | 13 | 
| 15 "github.com/golang/protobuf/jsonpb" | 14 "github.com/golang/protobuf/jsonpb" | 
| 16 "github.com/golang/protobuf/proto" | 15 "github.com/golang/protobuf/proto" | 
| 17 "golang.org/x/net/context" | 16 "golang.org/x/net/context" | 
| 18 "google.golang.org/grpc" | 17 "google.golang.org/grpc" | 
| 19 "google.golang.org/grpc/codes" | 18 "google.golang.org/grpc/codes" | 
| 20 | |
| 21 "github.com/luci/luci-go/common/logging" | |
| 22 ) | 19 ) | 
| 23 | 20 | 
| 24 const ( | 21 const ( | 
| 25 headerAccept = "Accept" | 22 headerAccept = "Accept" | 
| 23 csrfPrefix = ")]}'" | |
| 
 
dnj (Google)
2016/01/26 16:13:56
I believe this should end in a newline, both for c
 
nodir
2016/01/26 18:01:06
Done.
 
 | |
| 26 ) | 24 ) | 
| 27 | 25 | 
| 28 // responseFormat returns the format to be used in a response. | 26 // responseFormat returns the format to be used in a response. | 
| 29 // Can return only formatBinary (preferred), formatJSONPB or formatText. | 27 // Can return only formatBinary (preferred), formatJSONPB or formatText. | 
| 30 // In case of an error, format is undefined and the error has an HTTP status. | 28 // In case of an error, format is undefined. | 
| 31 func responseFormat(acceptHeader string) (format, *httpError) { | 29 func responseFormat(acceptHeader string) (format, *protocolError) { | 
| 32 if acceptHeader == "" { | 30 if acceptHeader == "" { | 
| 33 return formatBinary, nil | 31 return formatBinary, nil | 
| 34 } | 32 } | 
| 35 | 33 | 
| 36 parsed, err := parseAccept(acceptHeader) | 34 parsed, err := parseAccept(acceptHeader) | 
| 37 if err != nil { | 35 if err != nil { | 
| 38 return formatBinary, errorf(http.StatusBadRequest, "Accept heade r: %s", err) | 36 return formatBinary, errorf(http.StatusBadRequest, "Accept heade r: %s", err) | 
| 39 } | 37 } | 
| 40 assert(len(parsed) > 0) | 38 assert(len(parsed) > 0) | 
| 
 
dnj (Google)
2016/01/26 16:13:56
This is just correctness of "parseAccept". No valu
 
nodir
2016/01/26 18:01:06
Done.
 
 | |
| 41 formats := make(acceptFormatSlice, 0, len(parsed)) | 39 formats := make(acceptFormatSlice, 0, len(parsed)) | 
| 42 for _, at := range parsed { | 40 for _, at := range parsed { | 
| 43 f, err := parseFormat(at.MediaType, at.MediaTypeParams) | 41 f, err := parseFormat(at.MediaType, at.MediaTypeParams) | 
| 44 if err != nil { | 42 if err != nil { | 
| 45 // Ignore invalid format. Check further. | 43 // Ignore invalid format. Check further. | 
| 46 continue | 44 continue | 
| 47 } | 45 } | 
| 48 switch f { | 46 switch f { | 
| 49 | 47 | 
| 50 case formatBinary, formatJSONPB, formatText: | 48 case formatBinary, formatJSONPB, formatText: | 
| 51 // fine | 49 // fine | 
| 52 | 50 | 
| 53 case formatUnspecified: | 51 case formatUnspecified: | 
| 54 f = formatBinary // prefer binary | 52 f = formatBinary // prefer binary | 
| 55 | 53 | 
| 56 case formatUnrecognized: | 54 case formatUnrecognized: | 
| 57 continue | 55 continue | 
| 58 | 56 | 
| 59 default: | 57 default: | 
| 60 panicf("cannot happen") | 58 panicf("cannot happen") | 
| 61 } | 59 } | 
| 62 | 60 | 
| 63 assert(f == formatBinary || f == formatJSONPB || f == formatText ) | 61 assert(f == formatBinary || f == formatJSONPB || f == formatText ) | 
| 
 
dnj (Google)
2016/01/26 16:13:56
This is just correctness. No value using panic to
 
nodir
2016/01/26 18:01:06
Done.
 
 | |
| 64 formats = append(formats, acceptFormat{f, at.QualityFactor}) | 62 formats = append(formats, acceptFormat{f, at.QualityFactor}) | 
| 65 } | 63 } | 
| 66 if len(formats) == 0 { | 64 if len(formats) == 0 { | 
| 67 return formatBinary, errorf( | 65 return formatBinary, errorf( | 
| 68 http.StatusNotAcceptable, | 66 http.StatusNotAcceptable, | 
| 69 "Accept header: specified media types are not not suppor ted. Supported types: %q, %q, %q, %q.", | 67 "Accept header: specified media types are not not suppor ted. Supported types: %q, %q, %q, %q.", | 
| 70 mtPRPCBinary, | 68 mtPRPCBinary, | 
| 71 mtPRPCJSNOPB, | 69 mtPRPCJSNOPB, | 
| 72 mtPRPCText, | 70 mtPRPCText, | 
| 73 mtJSON, | 71 mtJSON, | 
| 74 ) | 72 ) | 
| 75 } | 73 } | 
| 76 sort.Sort(formats) // order by quality factor and format preference. | 74 sort.Sort(formats) // order by quality factor and format preference. | 
| 77 return formats[0].Format, nil | 75 return formats[0].Format, nil | 
| 78 } | 76 } | 
| 79 | 77 | 
| 80 // writeMessage writes a protobuf message to response in the specified format. | 78 // respondMessage encodes msg to a response in the specified format. | 
| 81 func writeMessage(w http.ResponseWriter, msg proto.Message, format format) error { | 79 func respondMessage(msg proto.Message, format format) *response { | 
| 82 if msg == nil { | 80 if msg == nil { | 
| 83 » » panic("msg is nil") | 81 » » panicf("msg is nil") | 
| 
 
dnj (Google)
2016/01/26 16:13:56
nit: panic would work here, no need for "panicf".
 
nodir
2016/01/26 18:01:06
err, in another CL you said that we panic with err
 
dnj
2016/01/26 18:50:27
Ah sorry I meant something like: panic(errors.New(
 
 | |
| 84 } | 82 } | 
| 85 var ( | 83 var ( | 
| 86 » » contentType string | 84 » » res response | 
| 87 » » res []byte | 85 » » err error | 
| 88 » » err error | |
| 89 ) | 86 ) | 
| 90 switch format { | 87 switch format { | 
| 91 case formatBinary: | 88 case formatBinary: | 
| 92 » » contentType = mtPRPCBinary | 89 » » res.contentType = mtPRPCBinary | 
| 93 » » res, err = proto.Marshal(msg) | 90 » » res.body, err = proto.Marshal(msg) | 
| 94 | 91 | 
| 95 case formatJSONPB: | 92 case formatJSONPB: | 
| 96 » » contentType = mtPRPCJSNOPB | 93 » » res.contentType = mtPRPCJSNOPB | 
| 94 » » var buf bytes.Buffer | |
| 95 » » buf.WriteString(csrfPrefix) | |
| 97 m := jsonpb.Marshaler{Indent: "\t"} | 96 m := jsonpb.Marshaler{Indent: "\t"} | 
| 
 
dnj (Google)
2016/01/26 16:13:56
1) Don't we normally use spaces to indent JSON?
2)
 
nodir
2016/01/26 18:01:07
1) \t is less bytes
2) I used to curl in the past.
 
 | |
| 98 var buf bytes.Buffer | |
| 99 err = m.Marshal(&buf, msg) | 97 err = m.Marshal(&buf, msg) | 
| 100 » » buf.WriteString("\n") | 98 » » res.body = buf.Bytes() | 
| 101 » » res = buf.Bytes() | 99 » » res.newLine = true | 
| 102 | 100 | 
| 103 case formatText: | 101 case formatText: | 
| 104 » » contentType = mtPRPCText | 102 » » res.contentType = mtPRPCText | 
| 105 var buf bytes.Buffer | 103 var buf bytes.Buffer | 
| 106 err = proto.MarshalText(&buf, msg) | 104 err = proto.MarshalText(&buf, msg) | 
| 107 » » res = buf.Bytes() | 105 » » res.body = buf.Bytes() | 
| 108 } | 106 } | 
| 109 if err != nil { | 107 if err != nil { | 
| 110 » » return err | 108 » » return errResponse(codes.Internal, 0, err.Error()) | 
| 111 } | 109 } | 
| 112 » w.Header().Set(headerContentType, contentType) | 110 | 
| 113 » _, err = w.Write(res) | 111 » return &res | 
| 114 » return err | 112 } | 
| 113 | |
| 114 // respondProtocolError creates a response for a pRPC protocol error. | |
| 115 func respondProtocolError(err *protocolError) *response { | |
| 116 » return errResponse(codes.InvalidArgument, err.status, err.err.Error()) | |
| 117 } | |
| 118 | |
| 119 // errorCode returns a most appropriate gRPC code for an error | |
| 120 func errorCode(err error) codes.Code { | |
| 121 » switch err { | |
| 122 » case nil: | |
| 123 » » panicf("err is nil") | |
| 
 
dnj (Google)
2016/01/26 16:13:56
Default to something like Unknown if nil, rather t
 
nodir
2016/01/26 18:01:06
returned ok
 
 | |
| 124 » » fallthrough | |
| 125 | |
| 126 » case context.DeadlineExceeded: | |
| 127 » » return codes.DeadlineExceeded | |
| 128 | |
| 129 » case context.Canceled: | |
| 130 » » return codes.Canceled | |
| 131 | |
| 132 » default: | |
| 133 » » return grpc.Code(err) | |
| 134 » } | |
| 115 } | 135 } | 
| 116 | 136 | 
| 117 // codeToStatus maps gRPC codes to HTTP statuses. | 137 // codeToStatus maps gRPC codes to HTTP statuses. | 
| 118 // This map may need to be corrected when | 138 // This map may need to be corrected when | 
| 119 // https://github.com/grpc/grpc-common/issues/210 | 139 // https://github.com/grpc/grpc-common/issues/210 | 
| 120 // is closed. | 140 // is closed. | 
| 121 var codeToStatus = map[codes.Code]int{ | 141 var codeToStatus = map[codes.Code]int{ | 
| 122 codes.OK: http.StatusOK, | 142 codes.OK: http.StatusOK, | 
| 123 codes.Canceled: http.StatusNoContent, | 143 codes.Canceled: http.StatusNoContent, | 
| 124 codes.Unknown: http.StatusInternalServerError, | 144 codes.Unknown: http.StatusInternalServerError, | 
| 125 codes.InvalidArgument: http.StatusBadRequest, | 145 codes.InvalidArgument: http.StatusBadRequest, | 
| 126 codes.DeadlineExceeded: http.StatusServiceUnavailable, | 146 codes.DeadlineExceeded: http.StatusServiceUnavailable, | 
| 127 codes.NotFound: http.StatusNotFound, | 147 codes.NotFound: http.StatusNotFound, | 
| 128 codes.AlreadyExists: http.StatusConflict, | 148 codes.AlreadyExists: http.StatusConflict, | 
| 129 codes.PermissionDenied: http.StatusForbidden, | 149 codes.PermissionDenied: http.StatusForbidden, | 
| 130 codes.Unauthenticated: http.StatusUnauthorized, | 150 codes.Unauthenticated: http.StatusUnauthorized, | 
| 131 codes.ResourceExhausted: http.StatusServiceUnavailable, | 151 codes.ResourceExhausted: http.StatusServiceUnavailable, | 
| 132 codes.FailedPrecondition: http.StatusPreconditionFailed, | 152 codes.FailedPrecondition: http.StatusPreconditionFailed, | 
| 133 codes.Aborted: http.StatusInternalServerError, | 153 codes.Aborted: http.StatusInternalServerError, | 
| 134 codes.OutOfRange: http.StatusBadRequest, | 154 codes.OutOfRange: http.StatusBadRequest, | 
| 135 codes.Unimplemented: http.StatusNotImplemented, | 155 codes.Unimplemented: http.StatusNotImplemented, | 
| 136 codes.Internal: http.StatusInternalServerError, | 156 codes.Internal: http.StatusInternalServerError, | 
| 137 codes.Unavailable: http.StatusServiceUnavailable, | 157 codes.Unavailable: http.StatusServiceUnavailable, | 
| 138 codes.DataLoss: http.StatusInternalServerError, | 158 codes.DataLoss: http.StatusInternalServerError, | 
| 139 } | 159 } | 
| 140 | 160 | 
| 141 // ErrorStatus returns HTTP status for an error. | 161 // codeStatus maps gRPC codes to HTTP status codes. | 
| 142 // In particular, it maps gRPC codes to HTTP statuses. | 162 // Falls back to http.StatusInternalServerError. | 
| 143 // Status of nil is 200. | 163 func codeStatus(code codes.Code) int { | 
| 
 
dnj (Google)
2016/01/26 16:13:56
If it falls back to internal server error, you sho
 
nodir
2016/01/26 18:01:07
Done.
 
 | |
| 144 // | 164 » if status, ok := codeToStatus[code]; ok { | 
| 145 // See also grpc.Code. | 165 » » return status | 
| 146 func ErrorStatus(err error) int { | |
| 147 » if err, ok := err.(*httpError); ok { | |
| 148 » » return err.status | |
| 149 } | 166 } | 
| 150 | 167 » return http.StatusInternalServerError | 
| 151 » status, ok := codeToStatus[grpc.Code(err)] | |
| 152 » if !ok { | |
| 153 » » status = http.StatusInternalServerError | |
| 154 » } | |
| 155 » return status | |
| 156 } | 168 } | 
| 157 | |
| 158 // ErrorDesc returns the error description of err if it was produced by pRPC or gRPC. | |
| 159 // Otherwise, it returns err.Error() or empty string when err is nil. | |
| 160 // | |
| 161 // See also grpc.ErrorDesc. | |
| 162 func ErrorDesc(err error) string { | |
| 163 if err == nil { | |
| 164 return "" | |
| 165 } | |
| 166 if e, ok := err.(*httpError); ok { | |
| 167 err = e.err | |
| 168 } | |
| 169 return grpc.ErrorDesc(err) | |
| 170 } | |
| 171 | |
| 172 // writeError writes an error to an HTTP response. | |
| 173 // | |
| 174 // HTTP status is determined by ErrorStatus. | |
| 175 // If it is http.StatusInternalServerError, prints only "Internal server error", | |
| 176 // otherwise uses ErrorDesc. | |
| 177 // | |
| 178 // Logs all errors with status >= 500. | |
| 179 func writeError(c context.Context, w http.ResponseWriter, err error) { | |
| 180 if err == nil { | |
| 181 panic("err is nil") | |
| 182 } | |
| 183 | |
| 184 status := ErrorStatus(err) | |
| 185 if status >= 500 { | |
| 186 logging.Errorf(c, "HTTP %d: %s", status, ErrorDesc(err)) | |
| 187 } | |
| 188 | |
| 189 w.Header().Set(headerContentType, "text/plain") | |
| 190 w.WriteHeader(status) | |
| 191 | |
| 192 var body string | |
| 193 if status == http.StatusInternalServerError { | |
| 194 body = "Internal server error" | |
| 195 } else { | |
| 196 body = ErrorDesc(err) | |
| 197 } | |
| 198 if _, err := io.WriteString(w, body+"\n"); err != nil { | |
| 199 logging.Errorf(c, "could not write error: %s", err) | |
| 200 } | |
| 201 } | |
| 202 | |
| 203 func assert(condition bool) { | |
| 204 if !condition { | |
| 205 panicf("assertion failed") | |
| 206 } | |
| 207 } | |
| OLD | NEW |