这是indexloc提供的服务,不要输入任何密码
Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 87 additions & 53 deletions adapters/humafiber/humafiber.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,105 +15,129 @@ import (
"github.com/gofiber/fiber/v2"
)

// avoid race condition inside fasthttp need to cache Context().Done() and UserContext().Value
type contextAdapter struct {
*fiber.Ctx
done <-chan struct{}
user func(any) any
}

var _ context.Context = &contextAdapter{}
type fiberCtx struct {
op *huma.Operation
status int

func (ca *contextAdapter) Deadline() (deadline time.Time, ok bool) {
return ca.Ctx.Context().Deadline()
/*
* Web framework "fiber" https://gofiber.io/ uses high-performance zero-allocation "fasthttp" server https://github.com/valyala/fasthttp
*
* The underlying fasthttp server prohibits to use or refer to `*fasthttp.RequestCtx` outside handler
* The quote from documentation to fasthttp https://github.com/valyala/fasthttp/blob/master/README.md
*
* > VERY IMPORTANT! Fasthttp disallows holding references to RequestCtx or to its' members after returning from RequestHandler. Otherwise data races are inevitable. Carefully inspect all the net/http request handlers converted to fasthttp whether they retain references to RequestCtx or to its' members after returning
*
* As the result "fiber" prohibits to use or refer to `*fiber.Ctx` outside handler
* The quote from documentation to fiber https://docs.gofiber.io/#zero-allocation
*
* > Because fiber is optimized for high-performance, values returned from fiber.Ctx are not immutable by default and will be re-used across requests. As a rule of thumb, you must only use context values within the handler, and you must not keep any references. As soon as you return from the handler, any values you have obtained from the context will be re-used in future requests and will change below your feet
*
* To deal with these limitations, the contributor of to this adapter @excavador (Oleg Tsarev, email: oleg@tsarev.id, telegram: @oleg_tsarev) is clear variable explicitly in the end of huma.Adapter methods Handle and ServeHTTP
*
* You must NOT use member `unsafeFiberCtx` directly in adapter, but instead use `orig()` private method
*/
unsafeFiberCtx *fiber.Ctx
unsafeGolangCtx context.Context
}

func (ca *contextAdapter) Done() <-chan struct{} {
return ca.done
// check that fiberCtx implements huma.Context
var _ huma.Context = &fiberCtx{}
var _ context.Context = &fiberCtx{}

func (c *fiberCtx) orig() *fiber.Ctx {
var result = c.unsafeFiberCtx
select {
case <-c.unsafeGolangCtx.Done():
panic("handler was done already")
default:
return result
}
}

func (ca *contextAdapter) Err() error {
return ca.Ctx.Context().Err()
func (c *fiberCtx) Deadline() (deadline time.Time, ok bool) {
return c.unsafeGolangCtx.Deadline()
}

func (ca *contextAdapter) Value(key any) any {
var value = ca.user(key)
if value != nil {
return value
}
return ca.Ctx.Context().Value(key)
func (c *fiberCtx) Done() <-chan struct{} {
return c.unsafeGolangCtx.Done()
}

type fiberCtx struct {
op *huma.Operation
orig *fiber.Ctx
status int
func (c *fiberCtx) Err() error {
return c.unsafeGolangCtx.Err()
}

// check that fiberCtx implements huma.Context
var _ huma.Context = &fiberCtx{}
func (c *fiberCtx) Value(key any) any {
var orig = c.unsafeFiberCtx
select {
case <-c.unsafeGolangCtx.Done():
return nil
default:
var value = orig.UserContext().Value(key)
if value != nil {
return value
}
return orig.Context().Value(key)
}
}

func (c *fiberCtx) Operation() *huma.Operation {
return c.op
}

func (c *fiberCtx) Matched() string {
return c.orig.Route().Path
return c.orig().Route().Path
}

func (c *fiberCtx) Context() context.Context {
return &contextAdapter{
Ctx: c.orig,
done: c.orig.Context().Done(),
user: c.orig.UserContext().Value,
}
return c
}

func (c *fiberCtx) Method() string {
return c.orig.Method()
return c.orig().Method()
}

func (c *fiberCtx) Host() string {
return c.orig.Hostname()
return c.orig().Hostname()
}

func (c *fiberCtx) RemoteAddr() string {
return c.orig.Context().RemoteAddr().String()
return c.orig().Context().RemoteAddr().String()
}

func (c *fiberCtx) URL() url.URL {
u, _ := url.Parse(string(c.orig.Request().RequestURI()))
u, _ := url.Parse(string(c.orig().Request().RequestURI()))
return *u
}

func (c *fiberCtx) Param(name string) string {
return c.orig.Params(name)
return c.orig().Params(name)
}

func (c *fiberCtx) Query(name string) string {
return c.orig.Query(name)
return c.orig().Query(name)
}

func (c *fiberCtx) Header(name string) string {
return c.orig.Get(name)
return c.orig().Get(name)
}

func (c *fiberCtx) EachHeader(cb func(name, value string)) {
c.orig.Request().Header.VisitAll(func(k, v []byte) {
c.orig().Request().Header.VisitAll(func(k, v []byte) {
cb(string(k), string(v))
})
}

func (c *fiberCtx) BodyReader() io.Reader {
if c.orig.App().Server().StreamRequestBody {
var orig = c.orig()
if orig.App().Server().StreamRequestBody {
// Streaming is enabled, so send the reader.
return c.orig.Request().BodyStream()
return orig.Request().BodyStream()
}
return bytes.NewReader(c.orig.BodyRaw())
return bytes.NewReader(orig.BodyRaw())
}

func (c *fiberCtx) GetMultipartForm() (*multipart.Form, error) {
return c.orig.MultipartForm()
return c.orig().MultipartForm()
}

func (c *fiberCtx) SetReadDeadline(deadline time.Time) error {
Expand All @@ -122,36 +146,37 @@ func (c *fiberCtx) SetReadDeadline(deadline time.Time) error {
// 2. Set the Fiber app's `BodyLimit` to some small value like `1`
// Fiber will only call the request handler for streaming once the limit is
// reached. This is annoying but currently how things work.
return c.orig.Context().Conn().SetReadDeadline(deadline)
return c.orig().Context().Conn().SetReadDeadline(deadline)
}

func (c *fiberCtx) SetStatus(code int) {
var orig = c.orig()
c.status = code
c.orig.Status(code)
orig.Status(code)
}

func (c *fiberCtx) Status() int {
return c.status
}
func (c *fiberCtx) AppendHeader(name string, value string) {
c.orig.Append(name, value)
c.orig().Append(name, value)
}

func (c *fiberCtx) SetHeader(name string, value string) {
c.orig.Set(name, value)
c.orig().Set(name, value)
}

func (c *fiberCtx) BodyWriter() io.Writer {
return c.orig
return c.orig().Context()
}

func (c *fiberCtx) TLS() *tls.ConnectionState {
return c.orig.Context().TLSConnectionState()
return c.orig().Context().TLSConnectionState()
}

func (c *fiberCtx) Version() huma.ProtoVersion {
return huma.ProtoVersion{
Proto: c.orig.Protocol(),
Proto: c.orig().Protocol(),
}
}

Expand All @@ -174,8 +199,17 @@ func (a *fiberAdapter) Handle(op *huma.Operation, handler func(huma.Context)) {
path = strings.ReplaceAll(path, "{", ":")
path = strings.ReplaceAll(path, "}", "")
a.router.Add(op.Method, path, func(c *fiber.Ctx) error {
ctx := &fiberCtx{op: op, orig: c}
handler(ctx)
var ctx, cancel = context.WithCancel(context.Background())
var fc = &fiberCtx{
op: op,
unsafeFiberCtx: c,
unsafeGolangCtx: ctx,
}
defer func() {
cancel()
fc.unsafeFiberCtx = nil
}()
handler(fc)
return nil
})
}
Expand Down
Loading