mirror of
https://github.com/openziti/ziti.git
synced 2026-09-10 08:45:41 +00:00
200 lines
4.9 KiB
Go
200 lines
4.9 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/go-openapi/runtime"
|
|
"github.com/michaelquigley/pfxlog"
|
|
"github.com/openziti/foundation/util/debugz"
|
|
"github.com/openziti/foundation/util/errorz"
|
|
"net/http"
|
|
"path"
|
|
goruntime "runtime"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// TimeoutHandler will create a http.Handler that wraps the given http.Handler. If the given timeout is reached, the
|
|
// supplied errorz.ApiError and ResponseMapper will be used to create an error response. This handler assumes JSON
|
|
// output.
|
|
//
|
|
// This handler functions by creating a proxy ResponseWriter that is passed to downstream http.Handlers.
|
|
// This proxy ResponseWriter is ignored on timeout and panics. On panic, a blank response is returned with a
|
|
// 500 Internal Error status code. Downstream handlers are encouraged to implement their own panic recovery.
|
|
func TimeoutHandler(next http.Handler, timeout time.Duration, apiErr *errorz.ApiError, mapper ResponseMapper) http.Handler {
|
|
return &timeoutHandler{
|
|
next: next,
|
|
apiError: apiErr,
|
|
timeout: timeout,
|
|
producer: runtime.JSONProducer(),
|
|
responseMapper: mapper,
|
|
}
|
|
}
|
|
|
|
type timeoutHandler struct {
|
|
next http.Handler
|
|
timeout time.Duration
|
|
apiError *errorz.ApiError
|
|
producer runtime.Producer
|
|
responseMapper ResponseMapper
|
|
}
|
|
|
|
func (h *timeoutHandler) errorBody(w http.ResponseWriter, r *http.Request) error {
|
|
if h.apiError != nil {
|
|
requestId := ""
|
|
rc, _ := GetRequestContextFromHttpContext(r)
|
|
if rc != nil {
|
|
requestId = rc.GetId()
|
|
}
|
|
apiError := h.responseMapper.MapApiError(requestId, h.apiError)
|
|
err := h.producer.Produce(w, apiError)
|
|
|
|
return err
|
|
}
|
|
return errors.New("no timout api error specified")
|
|
}
|
|
|
|
func (h *timeoutHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancelCtx := context.WithTimeout(r.Context(), h.timeout)
|
|
defer cancelCtx()
|
|
|
|
r = r.WithContext(ctx)
|
|
done := make(chan struct{})
|
|
tw := &timeoutWriter{
|
|
writer: w,
|
|
header: make(http.Header),
|
|
request: r,
|
|
}
|
|
panicChan := make(chan interface{}, 1)
|
|
go func() {
|
|
defer func() {
|
|
if p := recover(); p != nil {
|
|
pfxlog.Logger().Errorf("panic caught by timeout next: %v\n%v", p, debugz.GenerateLocalStack())
|
|
panicChan <- p
|
|
}
|
|
}()
|
|
h.next.ServeHTTP(tw, r)
|
|
close(done)
|
|
}()
|
|
select {
|
|
case <-panicChan:
|
|
tw.mu.Lock()
|
|
defer tw.mu.Unlock()
|
|
dst := w.Header()
|
|
for k, vv := range tw.header {
|
|
dst[k] = vv
|
|
}
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
case <-done:
|
|
tw.mu.Lock()
|
|
defer tw.mu.Unlock()
|
|
dst := w.Header()
|
|
for k, vv := range tw.header {
|
|
dst[k] = vv
|
|
}
|
|
if !tw.wroteHeader {
|
|
tw.code = http.StatusOK
|
|
}
|
|
w.WriteHeader(tw.code)
|
|
_, _ = w.Write(tw.buffer.Bytes())
|
|
case <-ctx.Done():
|
|
tw.mu.Lock()
|
|
defer tw.mu.Unlock()
|
|
|
|
pfxlog.Logger().WithFields(map[string]interface{}{
|
|
"url": r.URL,
|
|
"method": r.Method,
|
|
}).Errorf("timeout for request hit, returning Service Unavailable 503")
|
|
|
|
w.Header().Set("content-type", "application/json")
|
|
w.WriteHeader(http.StatusServiceUnavailable)
|
|
_ = h.errorBody(w, r)
|
|
tw.timedOut = true
|
|
}
|
|
}
|
|
|
|
// timeoutWriter is a proxy http.ResponseWriter that is used by TimeoutHandler.
|
|
type timeoutWriter struct {
|
|
writer http.ResponseWriter
|
|
header http.Header
|
|
buffer bytes.Buffer
|
|
request *http.Request
|
|
|
|
mu sync.Mutex
|
|
timedOut bool
|
|
wroteHeader bool
|
|
code int
|
|
}
|
|
|
|
var _ http.Pusher = (*timeoutWriter)(nil)
|
|
|
|
func (tw *timeoutWriter) Push(target string, opts *http.PushOptions) error {
|
|
if pusher, ok := tw.writer.(http.Pusher); ok {
|
|
return pusher.Push(target, opts)
|
|
}
|
|
return http.ErrNotSupported
|
|
}
|
|
|
|
func (tw *timeoutWriter) Header() http.Header { return tw.header }
|
|
|
|
func (tw *timeoutWriter) Write(p []byte) (int, error) {
|
|
tw.mu.Lock()
|
|
defer tw.mu.Unlock()
|
|
if tw.timedOut {
|
|
return 0, http.ErrHandlerTimeout
|
|
}
|
|
if !tw.wroteHeader {
|
|
tw.writeHeaderLocked(http.StatusOK)
|
|
}
|
|
return tw.buffer.Write(p)
|
|
}
|
|
|
|
func (tw *timeoutWriter) writeHeaderLocked(code int) {
|
|
checkWriteHeaderCode(code)
|
|
|
|
switch {
|
|
case tw.timedOut:
|
|
return
|
|
case tw.wroteHeader:
|
|
if tw.request != nil {
|
|
caller := relevantCaller()
|
|
pfxlog.Logger().Errorf("http: superfluous response.WriteHeader call from %s (%s:%d)", caller.Function, path.Base(caller.File), caller.Line)
|
|
}
|
|
default:
|
|
tw.wroteHeader = true
|
|
tw.code = code
|
|
}
|
|
}
|
|
|
|
func (tw *timeoutWriter) WriteHeader(code int) {
|
|
tw.mu.Lock()
|
|
defer tw.mu.Unlock()
|
|
tw.writeHeaderLocked(code)
|
|
}
|
|
|
|
func checkWriteHeaderCode(code int) {
|
|
if code < 100 || code > 999 {
|
|
panic(fmt.Sprintf("invalid WriteHeader code %v", code))
|
|
}
|
|
}
|
|
|
|
func relevantCaller() goruntime.Frame {
|
|
pc := make([]uintptr, 16)
|
|
n := goruntime.Callers(1, pc)
|
|
frames := goruntime.CallersFrames(pc[:n])
|
|
var frame goruntime.Frame
|
|
for {
|
|
frame, more := frames.Next()
|
|
if !strings.HasPrefix(frame.Function, "net/http.") && !strings.HasPrefix(frame.Function, "github.com/openziti/fabric/controller/api_impl/timeout") {
|
|
return frame
|
|
}
|
|
if !more {
|
|
break
|
|
}
|
|
}
|
|
return frame
|
|
}
|