diff --git a/pkg/api/error.go b/pkg/api/error.go index 0cee0487a..e8ba7a535 100644 --- a/pkg/api/error.go +++ b/pkg/api/error.go @@ -16,12 +16,15 @@ package api import ( + "errors" "fmt" "net/http" "regexp" "github.com/go-openapi/runtime/middleware" "github.com/go-openapi/strfmt" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" "github.com/sigstore/rekor/pkg/generated/models" "github.com/sigstore/rekor/pkg/generated/restapi/operations/entries" @@ -31,6 +34,29 @@ import ( "github.com/sigstore/rekor/pkg/log" ) +func mapGRPCToHTTP(code int, err error) int { + // Only try to be smart if current code is a generic 500 + if code != http.StatusInternalServerError { + return code + } + + // Look for a GRPC error (even a wrapped one) to resolve a more useful HTTP code. + // The list of handled codes is intentionally limited to specific cases + for currErr := err; currErr != nil; currErr = errors.Unwrap(currErr) { + if st, ok := status.FromError(currErr); ok { + switch st.Code() { + case codes.Canceled: + return 499 // Client Closed Request + case codes.DeadlineExceeded: + return http.StatusGatewayTimeout + default: + return code + } + } + } + return code +} + const ( trillianCommunicationError = "unexpected error communicating with transparency log" trillianUnexpectedResult = "unexpected result from transparency log" @@ -60,6 +86,8 @@ func errorMsg(message string, code int) *models.Error { var re = regexp.MustCompile("^(.*)Params$") func handleRekorAPIError(params interface{}, code int, err error, message string, fields ...interface{}) middleware.Responder { + code = mapGRPCToHTTP(code, err) + if message == "" { message = http.StatusText(code) } diff --git a/pkg/api/error_test.go b/pkg/api/error_test.go new file mode 100644 index 000000000..c5e3b2425 --- /dev/null +++ b/pkg/api/error_test.go @@ -0,0 +1,42 @@ +// Copyright 2026 The Sigstore 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 api + +import ( + "net/http" + "testing" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func TestMapGRPCToHTTP(t *testing.T) { + tests := []struct { + code int + err error + want int + }{ + {http.StatusOK, status.Error(codes.Canceled, "context canceled"), http.StatusOK}, + {http.StatusInternalServerError, status.Error(codes.Canceled, "context canceled"), 499}, + {http.StatusInternalServerError, status.Error(codes.DeadlineExceeded, "deadline exceeded"), http.StatusGatewayTimeout}, + {http.StatusInternalServerError, status.Error(codes.DataLoss, "dataloss"), http.StatusInternalServerError}, + } + + for _, tt := range tests { + if got := mapGRPCToHTTP(tt.code, tt.err); got != tt.want { + t.Errorf("mapGRPCToHTTP(%v, %e) = %v, want %v", tt.code, tt.err, got, tt.want) + } + } +} diff --git a/pkg/generated/restapi/configure_rekor_server.go b/pkg/generated/restapi/configure_rekor_server.go index e72a36364..985f0a9e3 100644 --- a/pkg/generated/restapi/configure_rekor_server.go +++ b/pkg/generated/restapi/configure_rekor_server.go @@ -22,9 +22,12 @@ import ( "crypto/tls" go_errors "errors" "fmt" + "io" + "net" "net/http" "net/http/httputil" "strconv" + "syscall" "time" // using embed to add the static html page duing build time @@ -397,7 +400,22 @@ func recoverer(next http.Handler) http.Handler { fields = append(fields, zap.ByteString("request_headers", request)) } - log.ContextLogger(ctx).With(fields...).Errorf("panic detected: %v", rvr) + // Check if the panic is due to a connection issue: Don't log these + // cases as serious errors + isNetworkError := false + if err, ok := rvr.(error); ok { + if go_errors.Is(err, io.EOF) || go_errors.Is(err, syscall.EPIPE) || go_errors.Is(err, syscall.ECONNRESET) { + isNetworkError = true + } else if netErr, ok := err.(net.Error); ok && netErr.Timeout() { + isNetworkError = true + } + } + + if isNetworkError { + log.ContextLogger(ctx).With(fields...).Debugf("client connection closed: %v", rvr) + } else { + log.ContextLogger(ctx).With(fields...).Errorf("panic detected: %v", rvr) + } errors.ServeError(w, r, nil) }