gofiber / fiber

⚡️ Express inspired web framework written in Go

Home Page:https://gofiber.io

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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

wHoIsDReAmer opened this issue · comments

commented

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:

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

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

@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

commented

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

@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?

commented

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

commented

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,