gofiber / fiber

⚡️ Express inspired web framework written in Go
https://gofiber.io
MIT License
33.93k stars 1.67k forks source link

🤗 [Question]: How can i upgrade websocket in fiber #2960

Closed wHoIsDReAmer closed 7 months ago

wHoIsDReAmer commented 7 months ago

Question Description

recently i tried link gqlgen with gofiber, but i can't implement subscription handling in gofiber.

client-side websocket got closed when client is connecting, without any headers here's my code

sorry for my bad english

Code Snippet (optional)

package domain

import (
    "gql-fiber/gql"
    "gql-fiber/gql/resolvers"
    "gql-fiber/service"
    "bufio"
    "context"
    "fmt"
    "net"
    "net/http"
    "time"

    "github.com/99designs/gqlgen/graphql/handler"
    "github.com/99designs/gqlgen/graphql/handler/extension"
    "github.com/99designs/gqlgen/graphql/handler/transport"
    "github.com/fasthttp/websocket"
    "github.com/gofiber/fiber/v2"
    "github.com/gofiber/fiber/v2/middleware/adaptor"
    websocket2 "github.com/gorilla/websocket"
    "github.com/valyala/fasthttp"
)

func GraphQLHandler(eventBus *service.EventBus) (fiber.Handler, fiber.Handler) {
    h := handler.NewDefaultServer(gql.NewExecutableSchema(gql.Config{Resolvers: &resolvers.Resolver{
        EventBus: eventBus,
    }}))

    gqlHandler := func(c *fiber.Ctx) error {
        httpHandler := adaptor.HTTPHandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            ctx := c.UserContext()

            h.ServeHTTP(w, r.WithContext(ctx))
        })

        return httpHandler(c)
    }

    wsh := handler.New(gql.NewExecutableSchema(gql.Config{Resolvers: &resolvers.Resolver{
        EventBus: eventBus,
    }}))

    wsh.AddTransport(transport.Websocket{
        KeepAlivePingInterval: 10 * time.Second,
        Upgrader: websocket2.Upgrader{
            ReadBufferSize:  1024,
            WriteBufferSize: 1024,
            CheckOrigin: func(ctx *http.Request) bool {
                return true
            },
        },
        InitFunc: func(ctx context.Context, initPayload transport.InitPayload) (context.Context, *transport.InitPayload, error) {
            fmt.Println("Hello world")
            return ctx, nil, nil
        },
    })

    wsh.Use(extension.Introspection{})

    gqlWsHandler := func(c *fiber.Ctx) error {
        httpHandler := adaptor.HTTPHandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            upgrader := websocket.FastHTTPUpgrader{
                ReadBufferSize:  1024,
                WriteBufferSize: 1024,
                CheckOrigin: func(r *fasthttp.RequestCtx) bool {
                    return true
                },
                Subprotocols: []string{"graphql-ws"},
            }

            ctx := c.UserContext()

            _ = upgrader.Upgrade(c.Context(), func(conn *websocket.Conn) {
                defer conn.Close()

                wsh.ServeHTTP(newWebsocketResponseWriter(conn, w.Header()), r.WithContext(ctx))
            })
        })

        return httpHandler(c)
    }

    return gqlHandler, gqlWsHandler
}

type websocketResponseWriter struct {
    conn   *websocket.Conn
    header http.Header
}

func newWebsocketResponseWriter(conn *websocket.Conn, header http.Header) *websocketResponseWriter {
    return &websocketResponseWriter{conn, header}
}

func (w *websocketResponseWriter) Header() http.Header {
    return w.header
}

func (w *websocketResponseWriter) Write(b []byte) (int, error) {
    err := w.conn.WriteMessage(websocket.TextMessage, b)
    if err != nil {
        w.conn.Close()
        return 0, err
    }
    return len(b), nil
}

func (w *websocketResponseWriter) WriteHeader(statusCode int) {}

func (w *websocketResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
    conn := w.conn.UnderlyingConn()
    reader := bufio.NewReader(conn)
    writer := bufio.NewWriter(conn)
    readWriter := bufio.NewReadWriter(reader, writer)
    return w.conn.UnderlyingConn(), readWriter, nil
}

Checklist:

welcome[bot] commented 7 months ago

Thanks for opening your first issue here! 🎉 Be sure to follow the issue template! If you need help or want to chat with us, join us on Discord https://gofiber.io/discord

gaby commented 7 months ago

@wHoIsDReAmer I would using the official websocket middleware. Using the gorilla one + adaptor defeats the purpose of using Fiber. We also got a socket.io middleware.

https://github.com/gofiber/contrib/tree/main/websocket https://github.com/gofiber/contrib/tree/main/socketio

wHoIsDReAmer commented 7 months ago

then I can't handle for gql. Can you present that how gql proceed?

gaby commented 7 months ago

@wHoIsDReAmer You can use it, just beware if performance implication given each request/response has to converted for that handler.

Would it be possible to use the Fiber Websocket instead of gorilla with gql?

wHoIsDReAmer commented 7 months ago

No. It can't be Fiber Websocket instead of gorilla with gql. because gqlgen handler require implement ServeHTTP if use another third-party library, but Fiber Websocket middleware doesn't provide http.ResponseWriter and *http.Request thus I used gorilla with gql inevitable

wHoIsDReAmer commented 7 months ago

Ok, I did some mistakes.

  1. after consume context to middleware, reused it
  2. implement ResponseWriter for Websocket

Here's solved code

func GraphQLHandler(eventBus *service.EventBus) (fiber.Handler, fiber.Handler) {
    h := handler.NewDefaultServer(gql.NewExecutableSchema(gql.Config{Resolvers: &resolvers.Resolver{
        EventBus: eventBus,
    }}))

    gqlHandler := func(c *fiber.Ctx) error {
        httpHandler := adaptor.HTTPHandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            ctx := c.UserContext()

            h.ServeHTTP(w, r.WithContext(ctx))
        })

        return httpHandler(c)
    }

    wsh := handler.New(gql.NewExecutableSchema(gql.Config{Resolvers: &resolvers.Resolver{
        EventBus: eventBus,
    }}))

    wsh.AddTransport(transport.Websocket{
        KeepAlivePingInterval: 10 * time.Second,
        Upgrader: websocket2.Upgrader{
            ReadBufferSize:  1024,
            WriteBufferSize: 1024,
            CheckOrigin: func(ctx *http.Request) bool {
                return true
            },
        },
    })

    wsh.Use(extension.Introspection{})

    gqlWsHandler := func(c *fiber.Ctx) error {
        ctx := c.UserContext()

        req := &http.Request{}
        fasthttpadaptor.ConvertRequest(c.Context(), req, false)
        crw := &commonResponseWriter{c.Context().Conn(), nil, 0}
        wsh.ServeHTTP(crw, req.WithContext(ctx))

        return nil
    }

    return gqlHandler, gqlWsHandler
}

type commonResponseWriter struct {
    conn   net.Conn
    header http.Header
    status int
}

func (w *commonResponseWriter) Header() http.Header {
    if w.header == nil {
        w.header = make(http.Header)
    }
    return w.header
}

func (w *commonResponseWriter) Write(b []byte) (int, error) {
    if w.status == 0 {
        w.status = http.StatusOK
    }
    return w.conn.Write(b)
}

func (w *commonResponseWriter) WriteHeader(statusCode int) {
    w.status = statusCode
    // Write headers to the connection
    fmt.Fprintf(w.conn, "HTTP/1.1 %d %s\r\n", statusCode, http.StatusText(statusCode))
    w.header.Write(w.conn)
    fmt.Fprint(w.conn, "\r\n")
}

func (w *commonResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
    reader := bufio.NewReader(w.conn)
    writer := bufio.NewWriter(w.conn)
    readWriter := bufio.NewReadWriter(reader, writer)

    return w.conn, readWriter, nil
}

I implemented Hijack for my own response writer wrapper then gql would proceed request,