Skip to content

🐛 [Bug]: UserContext no more accessible when adapting http.Handler to fiber.Handler #2711

Open
@tmarwen

Description

@tmarwen

Bug Description

When using the adaptor package to adapt an existing http.Handler to fiber.Handler, the fiber.Ctx#UserContext is no more accessible and all values already stored within are no more accessible as well.

Here down an example illustrating the issue:

package main

import (
	"context"
	"fmt"
	"net/http"

	"github.com/gofiber/fiber/v2"
	"github.com/gofiber/fiber/v2/middleware/adaptor"
)

func main() {
	userNameCtxKey := struct{}{}

	app := fiber.New()

	// Middleware enriching the UserContext
	app.Use(func(c *fiber.Ctx) error {
		userCtx := context.WithValue(c.UserContext(), userNameCtxKey, "go-fiber")
		c.SetUserContext(userCtx)
		return c.Next()
	})

	helloHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
		w.WriteHeader(http.StatusOK)
		fmt.Fprintf(w, "Hello %s", r.Context().Value(userNameCtxKey))
	})

	helloFiberHandler := adaptor.HTTPHandler(helloHandler)

	app.Get("/hello", helloFiberHandler)

	helloInternalHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
		w.WriteHeader(http.StatusOK)
		userCtx := r.Context().Value("__local_user_context__").(context.Context)
		fmt.Fprintf(w, "Hello %s", userCtx.Value(userNameCtxKey))
	})

	helloInternalFiberHandler := adaptor.HTTPHandler(helloInternalHandler)

	app.Get("/hello", helloFiberHandler)
	app.Get("/hello-internal", helloInternalFiberHandler)

	app.Listen(":8080")
}

As illustrated above, the context value with key userNameCtxKey initially stored in c.UserContext is not accessible through the raw http.Handler http.Request#Context since the latter use *fasthttp.RequestCtx as its Context value which still holds the user request context but hidded under the __local_user_context__ internal key as demonstrated within the second handler adaptor.

The above is for sure a simple illustration of the issue but the implications of this in real world scenarios are indeed impacting as context.Context is the main medium for cross-boundaries values passing.
One of the main use cases where this issue surfaced where propagation of incoming OpenTelemetry trace span when using https://github.com/gofiber/contrib/blob/main/otelfiber/:

  1. The incoming trace is properly extracted from header then a new span is created and its context is injected as the UserContext:
spanName := utils.CopyString(c.Path())
		ctx, span := tracer.Start(ctx, spanName, opts...)
		defer span.End()

		// pass the span through userContext
		c.SetUserContext(ctx)
  1. gqlgen HTTP handler is being used for GQL requests processing:
graphHandler := adaptor.HTTPHandler(gqlServer)
fiberApp.Post("/gqlapi", graphHandler)
  1. Parent span is not accessible in operation interceptor (and any other function using context.Context as argument)
type tracingOperationInterceptor struct {
}

func (i *tracingInterceptor) InterceptOperation(ctx context.Context, next graphql.OperationHandler) graphql.ResponseHandler {
	span := trace.SpanFormContext(ctx) // Span is not properly resolved
}

How to Reproduce

Steps to reproduce the behavior: Run below sample then cURL http://localhost:8080/hello the http://localhost:8080/hello-internal:

package main

import (
	"context"
	"fmt"
	"net/http"

	"github.com/gofiber/fiber/v2"
	"github.com/gofiber/fiber/v2/middleware/adaptor"
)

func main() {
	userNameCtxKey := struct{}{}

	app := fiber.New()

	// Middleware enriching the UserContext
	app.Use(func(c *fiber.Ctx) error {
		userCtx := context.WithValue(c.UserContext(), userNameCtxKey, "go-fiber")
		c.SetUserContext(userCtx)
		return c.Next()
	})

	helloHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
		w.WriteHeader(http.StatusOK)
		fmt.Fprintf(w, "Hello %s", r.Context().Value(userNameCtxKey))
	})

	helloFiberHandler := adaptor.HTTPHandler(helloHandler)

	app.Get("/hello", helloFiberHandler)

	helloInternalHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
		w.WriteHeader(http.StatusOK)
		userCtx := r.Context().Value("__local_user_context__").(context.Context)
		fmt.Fprintf(w, "Hello %s", userCtx.Value(userNameCtxKey))
	})

	helloInternalFiberHandler := adaptor.HTTPHandler(helloInternalHandler)

	app.Get("/hello", helloFiberHandler)
	app.Get("/hello-internal", helloInternalFiberHandler)

	app.Listen(":8080")
}

Expected Behavior

Fiber should either:

  1. Expose the #UserContext key, being __local_user_context__, publicly so it can be resolved from within any function as long as the incoming c.Context() is accessible
  2. Provide a safe mechanism to copy #UserContext into *fiber.Ctx#Context prior to adapting an http.Handler

Fiber Version

v2.50.0

Code Snippet (optional)

package main

import (
	"context"
	"fmt"
	"net/http"

	"github.com/gofiber/fiber/v2"
	"github.com/gofiber/fiber/v2/middleware/adaptor"
)

func main() {
	userNameCtxKey := struct{}{}

	app := fiber.New()

	// Middleware enriching the UserContext
	app.Use(func(c *fiber.Ctx) error {
		userCtx := context.WithValue(c.UserContext(), userNameCtxKey, "go-fiber")
		c.SetUserContext(userCtx)
		return c.Next()
	})

	helloHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
		w.WriteHeader(http.StatusOK)
		fmt.Fprintf(w, "Hello %s", r.Context().Value(userNameCtxKey))
	})

	helloFiberHandler := adaptor.HTTPHandler(helloHandler)

	app.Get("/hello", helloFiberHandler)

	helloInternalHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
		w.WriteHeader(http.StatusOK)
		userCtx := r.Context().Value("__local_user_context__").(context.Context)
		fmt.Fprintf(w, "Hello %s", userCtx.Value(userNameCtxKey))
	})

	helloInternalFiberHandler := adaptor.HTTPHandler(helloInternalHandler)

	app.Get("/hello", helloFiberHandler)
	app.Get("/hello-internal", helloInternalFiberHandler)

	app.Listen(":8080")
}

Checklist:

  • I agree to follow Fiber's Code of Conduct.
  • I have checked for existing issues that describe my problem prior to opening this one.
  • I understand that improperly formatted bug reports may be closed without explanation.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions