Skip to content

Commit

Permalink
httptransport: implement apiError using the new details package
Browse files Browse the repository at this point in the history
Signed-off-by: Hank Donnay <[email protected]>
  • Loading branch information
hdonnay committed Jun 11, 2024
1 parent 7a89db0 commit d26c23c
Show file tree
Hide file tree
Showing 2 changed files with 24 additions and 73 deletions.
95 changes: 23 additions & 72 deletions httptransport/error.go
Original file line number Diff line number Diff line change
@@ -1,89 +1,40 @@
package httptransport

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"

"github.com/quay/zlog"
"github.com/quay/clair/v4/httptransport/internal/details"
)

// StatusClientClosedRequest is a nonstandard HTTP status code used when the
// client has gone away.
//
// This convention is cribbed from Nginx.
const statusClientClosedRequest = 499

// ApiError writes an untyped (that is, "application/json") error with the
// provided HTTP status code and message.
// ApiError writes an error with the provided HTTP status code and message.
//
// ApiError does not return, but instead causes the goroutine to exit.
//
// Deprecated: This is implemented via [details.Error], which provides a
// richer API.
func apiError(ctx context.Context, w http.ResponseWriter, code int, f string, v ...interface{}) {
const errheader = `Clair-Error`
disconnect := false
select {
case <-ctx.Done():
disconnect = true
default:
}
if ev := zlog.Debug(ctx); ev.Enabled() {
ev.
Bool("disconnect", disconnect).
Int("code", code).
Str("error", fmt.Sprintf(f, v...)).
Msg("http error response")
} else {
ev.Send()
}
if disconnect {
// Exit immediately if there's no client to read the response, anyway.
w.WriteHeader(statusClientClosedRequest)
panic(http.ErrAbortHandler)
err := genericError{
status: code,
err: fmt.Errorf(f, v...),
}
details.Error(ctx, w, &err)
}

type genericError struct {
status int
err error
}

h := w.Header()
h.Del("link")
h.Set("content-type", "application/json")
h.Set("x-content-type-options", "nosniff")
h.Set("trailer", errheader)
w.WriteHeader(code)
func (e *genericError) Error() string {
return e.err.Error()
}

var buf bytes.Buffer
buf.WriteString(`{"code":"`)
switch code {
case http.StatusBadRequest:
buf.WriteString("bad-request")
case http.StatusMethodNotAllowed:
buf.WriteString("method-not-allowed")
case http.StatusNotFound:
buf.WriteString("not-found")
case http.StatusTooManyRequests:
buf.WriteString("too-many-requests")
default:
buf.WriteString("internal-error")
}
buf.WriteByte('"')
if f != "" {
buf.WriteString(`,"message":`)
b, _ := json.Marshal(fmt.Sprintf(f, v...)) // OK use of encoding/json.
buf.Write(b)
}
buf.WriteByte('}')
func (e *genericError) Unwrap() error {
return e.err
}

if _, err := buf.WriteTo(w); err != nil {
h.Set(errheader, err.Error())
}
switch err := http.NewResponseController(w).Flush(); {
case errors.Is(err, nil):
case errors.Is(err, http.ErrNotSupported):
// Skip
default:
zlog.Warn(ctx).
Err(err).
Msg("unable to flush http response")
}
panic(http.ErrAbortHandler)
func (e *genericError) ErrorStatus() int {
return e.status
}
2 changes: 1 addition & 1 deletion httptransport/error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func TestClientDisconnect(t *testing.T) {
}

<-handlerDone
if got, want := status, statusClientClosedRequest; got != want {
if got, want := status, 499; got != want {
t.Errorf("bad status code recorded: got: %d, want: %d", got, want)
}
}

0 comments on commit d26c23c

Please sign in to comment.