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 |